From 3b51787032003b29bf19a00bb0bc77688f41be48 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 18:23:40 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC=2071=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_01Gdqi6Ru5nYSNEX485wYhh1 From c1e90dcf87183706cf746eadad610e48beb78000 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:02:00 +0000 Subject: [PATCH 02/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Twindragon=20=EA=B3=BC=20Mastermind?= 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_01Gdqi6Ru5nYSNEX485wYhh1 --- incoming/pattern-study/patterns/mastermind.js | 387 ++++++++++++++++++ incoming/pattern-study/patterns/twindragon.js | 140 +++++++ 2 files changed, 527 insertions(+) create mode 100644 incoming/pattern-study/patterns/mastermind.js create mode 100644 incoming/pattern-study/patterns/twindragon.js diff --git a/incoming/pattern-study/patterns/mastermind.js b/incoming/pattern-study/patterns/mastermind.js new file mode 100644 index 000000000..8dbe51a4b --- /dev/null +++ b/incoming/pattern-study/patterns/mastermind.js @@ -0,0 +1,387 @@ +// ===== Patternflow pattern ===== +// Title: Mastermind +// Family: algo +// Idea: All 1296 codes of four pegs in six colours stand as a 6 by 6 grid of 6 by 6 blocks at the top, and Knuth's minimax player plays real games on the board below: each guess splits the codes still possible into colours by the answer they would give, the answer comes, and every class but one fades out, so the grid empties to a single LED in at most five guesses. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Mastermind (Mordecai Meirowitz, 1970), minimax strategy (Donald E. Knuth, The Computer as Master Mind, 1976), expected size (Robert W. Irving, 1979), most parts (Barteld Kooi, 2005), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Strategy=0..3, Codemaker=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together: at 0 the codes still possible are warm white, the six peg colours start from red, and the 14 answers each have their own colour. +// Knob 2: Speed. Beats a second: 2 at 2, so one guess (search 1 beat, place 1, split 2, answer 1, fade 1.5) takes 3.25 s and a game about 17 s; 0.2 at 0.2, 3 at 3. +// Knob 3: Strategy. How the codebreaker picks a guess from all 1296 codes. 0 Knuth's minimax: the guess whose largest answer class is smallest, 4.476 guesses a game over all 1296 secrets. 1 Irving's expected size: the smallest sum of squared class sizes, 4.395. 2 Kooi's most parts: the most answer classes, 4.373. Ties go to a code still possible, then the lowest. 3 A random code still possible, about 4.66. Turning it takes effect from the next guess. +// Knob 4: Codemaker. 0 picks a secret at random and answers honestly. 1 cheats within the rules, behind a red shield: it keeps no secret and always gives the answer that leaves the most codes. Knuth's player still wins in 5 every time, Irving's in 5, Kooi's only in 6, the random one in 5 to 8. + +const W = 64, H = 128; +const { floor, round, min, max, sin, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} +const mix = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u]; + +// ---- Codes. A code is four pegs in six colours, c = 216 p1 + 36 p2 + 6 p3 + p4, +// so Knuth's 1122 is 7. The answer to guess g for secret s is b black pegs +// (right colour, right place) and w white (right colour, wrong place), kept as +// b * 5 + w. Of the 25 slots 14 occur; 3 black and 1 white cannot. +const N = 1296, DIG = new Uint8Array(N * 4), CNT = new Uint8Array(N * 6); +for (let c = 0; c < N; c++) + for (let j = 0; j < 4; j++) { + const d = floor(c / [216, 36, 6, 1][j]) % 6; + DIG[c * 4 + j] = d; + CNT[c * 6 + d]++; + } +function answer(g, s) { + let b = 0, t = 0; + for (let j = 0; j < 4; j++) if (DIG[g * 4 + j] === DIG[s * 4 + j]) b++; + for (let d = 0; d < 6; d++) t += min(CNT[g * 6 + d], CNT[s * 6 + d]); + return b * 5 + t - b; +} +const WIN = 20; +const ANS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 20], CLS = new Int8Array(25).fill(-1); +ANS.forEach((a, i) => (CLS[a] = i)); + +// The answer table, 1296 by 1296 bytes, built once on the first frame (about +// 30 ms in Node, the only heavy frame): each guess's score is then |S| lookups. +let FB = null; +function table() { + if (FB) return FB; + FB = new Uint8Array(N * N); + for (let g = 0; g < N; g++) + for (let s = g; s < N; s++) { + const a = answer(g, s); + FB[g * N + s] = a; + FB[s * N + g] = a; + } + return FB; +} + +// ---- Strategies. Every guess is chosen over all 1296 codes, those already +// ruled out too, by the sizes n_i of the classes it splits S into: +// 0 Knuth (1976), the smallest largest n_i; 1 Irving (1979), the smallest +// sum of n_i^2, which is the expected size of what is left; 2 Kooi (2005), the +// most classes. Ties go to a code in S, then the lowest. 3 picks a random code +// from S. Played offline against all 1296 secrets with this code: Knuth opens +// 1122 and takes 5801 guesses, 4.476 a game, never more than 5 (1, 6, 62, 533 +// and 694 games of 1 to 5 guesses, as in Knuth's paper); Irving opens 1123, +// 5696, 4.395, at most 6; Kooi opens 1123, 5668, 4.373, at most 6; random, +// over ten seeds, 4.626 to 4.693 a game, at most 8. +const counts = new Int32Array(25); +function score(strat, g, S, n) { + counts.fill(0); + const row = g * N; + for (let i = 0; i < n; i++) counts[FB[row + S[i]]]++; + let v = 0; + if (strat === 0) for (let a = 0; a < 25; a++) v = max(v, counts[a]); + else if (strat === 1) for (let a = 0; a < 25; a++) v += counts[a] * counts[a]; + else for (let a = 0; a < 25; a++) v -= counts[a] > 0; + return v; +} +// The first guess of a game sees all 1296 codes, so it and its scores are the +// same every game: kept after the first search. +const FIRST = [null, null, null]; + +// A search in progress: the candidates are scored in slices of about 120,000 +// lookups a frame, half a millisecond in Node, so the first guess of a game +// (1296 x 1296) spreads over 14 frames, inside its one beat at Speed 2. +function search(strat) { + return { strat, g: 0, best: -1, bs: 0, bin: false, done: false, sc: new Float32Array(N) }; +} +function step(G, q, budget) { + if (q.done) return; + const S = G.S, n = G.n; + if (q.strat === 3) { + q.best = S[floor(Math.random() * n)]; + q.g = N; + q.done = true; + return; + } + if (n === N && FIRST[q.strat]) { + const f = FIRST[q.strat]; + q.best = f.best; + q.bs = f.bs; + q.sc = f.sc; + q.g = N; + q.done = true; + return; + } + let work = 0; + while (q.g < N && work < budget) { + const g = q.g++, v = score(q.strat, g, S, n), inS = G.inS[g] === 1; + q.sc[g] = v; + if (q.best < 0 || v < q.bs || (v === q.bs && inS && !q.bin)) { + q.best = g; + q.bs = v; + q.bin = inS; + } + work += n; + } + if (q.g >= N) { + q.done = true; + if (n === N) FIRST[q.strat] = q; + } +} + +// The codemaker. Honest: the answer for the secret. Cheating: the answer with +// the most codes of S behind it, a random one of those on a tie, and 4 black +// only once S is that one code. It never has to commit, since every answer it +// gives fits some code still in S. Against it, in Node: Knuth 5 guesses and +// Irving 5 in every one of 300 games, Kooi 6 in every one, random 5 to 8, +// 6.25 on average over 2,000. +function reply(G, g, adv) { + if (!adv) return FB[g * N + G.secret]; + counts.fill(0); + for (let i = 0; i < G.n; i++) counts[FB[g * N + G.S[i]]]++; + if (G.n === 1 && counts[WIN]) return WIN; + let top = 0, k = 0, pick = WIN; + for (let a = 0; a < WIN; a++) top = max(top, counts[a]); + for (let a = 0; a < WIN; a++) if (counts[a] === top && Math.random() * ++k < 1) pick = a; + return pick; +} + +function newGame(S) { + const G = { + secret: floor(Math.random() * N), S: new Int16Array(N), n: N, inS: new Uint8Array(N).fill(1), + rows: [], phase: "choose", ph: 0, q: null, g: -1, ans: -1, + }; + for (let i = 0; i < N; i++) G.S[i] = i; + G.q = search(S.strat); + return G; +} +// Keep the codes of S that give answer a to guess g. +function cut(G, g, a) { + let m = 0; + for (let i = 0; i < G.n; i++) { + const s = G.S[i]; + if (FB[g * N + s] === a) G.S[m++] = s; + else G.inS[s] = 0; + } + G.n = m; +} +function settle(S, G) { + // The secret a cheating codemaker ends up with is the last code standing. + if (S.adv && G.n >= 1) G.secret = G.S[0]; + G.rows.push({ g: G.g, ans: G.ans, rem: G.n }); +} + +// ---- Timing, in beats (Speed beats a second): search at least 1, place 1, +// split 2, answer 1, fade 1.5; a solved board stays 3 beats, then 1.5 to clear. +const T = { choose: 1, place: 1, split: 2, answer: 1, fade: 1.5, reveal: 3, clear: 1.5 }; + +export function setup(params) { + params.s = { t: 0, hue: 0, speed: 2, strat: 0, adv: 0, G: null, rgb: new Float32Array(W * H * 3) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.strat = cl(round(k[2]), 0, 3); + const adv = cl(round(k[3]), 0, 1); + table(); + if (!S.G) { + // Open on a game in progress: the first guess is on the board and the + // 1296 codes are splitting by the answer they would give it. + S.adv = adv; + const G = (S.G = newGame(S)); + while (!G.q.done) step(G, G.q, 1e9); + G.g = G.q.best; + G.ans = reply(G, G.g, adv); + G.phase = "split"; + } + let G = S.G; + // Never left with no code standing; a fresh game if it ever were. + if (G.n === 0) G = S.G = newGame(S); + if (adv !== S.adv) { + S.adv = adv; + // An honest codemaker taking over needs a secret that fits every answer, + // the one on its way to the board too. + if (!adv) { + const fits = []; + for (let i = 0; i < G.n; i++) if (G.phase === "choose" || FB[G.g * N + G.S[i]] === G.ans) fits.push(G.S[i]); + if (fits.length) G.secret = fits[floor(Math.random() * fits.length)]; + } + } + if (G.phase === "choose" && G.q && G.q.strat !== S.strat && !G.q.done) G.q = search(S.strat); + if (G.phase === "choose") step(G, G.q, 120000); + G.ph += d * S.speed; + const end = T[G.phase]; + if (G.ph < end) return; + if (G.phase === "choose") { + if (!G.q.done) return; + G.g = G.q.best; + G.ans = reply(G, G.g, S.adv); + G.phase = "place"; + } else if (G.phase === "place") G.phase = "split"; + else if (G.phase === "split") G.phase = "answer"; + else if (G.phase === "answer") G.phase = "fade"; + else if (G.phase === "fade") { + cut(G, G.g, G.ans); + settle(S, G); + if (G.ans === WIN) G.phase = "reveal"; + else { + G.phase = "choose"; + G.q = search(S.strat); + } + } else if (G.phase === "reveal") G.phase = "clear"; + else if (G.phase === "clear") { + S.G = newGame(S); + return; + } + G.ph = 0; +} + +// ---- Layout. The grid: block row p1, block column p2, and inside a block row +// p3 and column p4, one LED a code, blocks 6 LEDs with 3 dark between: 51 by +// 51 at x 9, y 3. Bars of p1's colour run down the left, p2's along the top. +// The board below, played from the near end as on a real one: the first +// guess at the bottom (y 122), up to eight every 8 rows, each four 6 by 6 +// pegs, the key pegs in a 2 by 2 square and, at the right edge, how many +// codes the answer left. The secret sits behind its shield at the far end, +// y 57. A random player can need 8 guesses; Knuth's never more than 5. +const GX = 9, GY = 3, BP = 9, SY = 57, RY = 122, RP = 8, ROWS = 8, PX = 4, KX = 37; +const GLYPH = { + 0: "111101101101111", 1: "010110010010111", 2: "111001111100111", 3: "111001111001111", 4: "101101111001001", + 5: "111100111001111", 6: "111100111101111", 7: "111001010010010", 8: "111101111101111", 9: "111101111001111", +}; +const gx = (c) => GX + (floor(c / 36) % 6) * BP + (c % 6); +const gy = (c) => GY + floor(c / 216) * BP + (floor(c / 6) % 6); + +function put(rgb, x, y, c, v) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const p = (y * W + x) * 3; + rgb[p] += c[0] * v, rgb[p + 1] += c[1] * v, rgb[p + 2] += c[2] * v; +} +function peg(rgb, x, y, c, v) { + for (let j = 0; j < 6; j++) + for (let i = 0; i < 6; i++) { + const corner = (i === 0 || i === 5) && (j === 0 || j === 5); + put(rgb, x + i, y + j, c, corner ? v * 0.3 : v); + } +} +function number(rgb, n, xr, y, c, v) { + const s = String(n); + for (let i = 0; i < s.length; i++) { + const g = GLYPH[s[s.length - 1 - i]], x = xr - 3 - 4 * i; + for (let r = 0; r < 5; r++) for (let q = 0; q < 3; q++) if (g[r * 3 + q] === "1") put(rgb, x + q, y + r, c, v); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.G) return; + const G = S.G, rgb = S.rgb, hue = S.hue, ph = G.ph; + rgb.fill(0); + const tone = hsv(0.12 + hue, 0.22, 0.85), ghost = hsv(0.62 + hue, 0.45, 0.115), white = [1, 1, 1]; + const pegs = [0, 1, 2, 3, 4, 5].map((i) => hsv(0.02 + hue + i / 6, 0.85, 1)); + const cls = ANS.map((a, i) => hsv(0.55 + hue + i * 0.382, 0.75, 1)); + const kb = hsv(0.98 + hue, 0.8, 1), kw = hsv(0.6 + hue, 0.05, 1); + const live = G.phase === "place" || G.phase === "split" || G.phase === "answer" || G.phase === "fade"; + const truth = live ? CLS[G.ans] : -1; + + // The grid. While a guess is searched for, each code the search has scored + // shows how good a guess it would be: u is the best score so far over its + // own (for Kooi its parts over the most), and the LED runs from dim blue to + // gold as u^5, so only guesses near the best shine. When 1122 gets one white + // peg and leaves 256 codes, Knuth's best is a largest class of 44, met by 48 + // codes, and half of all 1296 do no better than 57. The heat fades through + // the place beat. + const q = G.q, heat = (G.phase === "choose" || G.phase === "place") && q && q.strat < 3; + const upto = G.phase === "choose" ? floor(min(ph / T.choose, q.g / N) * N) : N; + const keep = G.phase === "place" ? min(1, ph / 0.8) : 0; + for (let c = 0; c < N; c++) { + let col; + if (!G.inS[c]) col = G.phase === "clear" ? mix(ghost, tone, min(1, ph / 1.2)) : ghost; + else if (G.phase === "split" || G.phase === "answer" || G.phase === "fade") { + const k = CLS[FB[G.g * N + c]], cc = cls[k]; + if (G.phase === "split") col = mix(tone, cc, min(1, ph / 0.6)); + else if (G.phase === "answer") col = k === truth ? mix(cc, white, 0.25 + 0.25 * sin(ph * PI * 4)) : cc; + else col = k === truth ? mix(cc, tone, min(1, ph / 1.2)) : mix(cc, ghost, min(1, ph / 1.2)); + } else col = tone; + if (heat && c < upto) { + const u = q.strat === 2 ? q.sc[c] / q.bs : q.bs / q.sc[c], u5 = u ** 5; + col = mix(hsv(0.62 + hue - 0.47 * u, 0.85 - 0.45 * u5, 0.14 + 0.86 * u5), col, keep); + } + put(rgb, gx(c), gy(c), col, 1); + } + for (let b = 0; b < 6; b++) { + for (let j = 0; j < 6; j++) { + put(rgb, 4, GY + b * BP + j, pegs[b], 0.5); + put(rgb, 5, GY + b * BP + j, pegs[b], 0.5); + put(rgb, GX + b * BP + j, 0, pegs[b], 0.5); + } + } + // The search front runs over all 1296 codes, ruled out or not, and the best + // guess so far shines. A random player rolls among S instead. Then the + // guess blinks until its answer is in. + if (G.phase === "choose" && q) { + if (q.strat < 3) { + for (let i = 0; i < 8; i++) if (upto - i >= 0 && upto - i < N) put(rgb, gx(upto - i), gy(upto - i), white, 0.9 * (1 - i / 8)); + } else for (let i = 0; i < 6; i++) { + const c = G.S[floor(Math.random() * G.n)]; + put(rgb, gx(c), gy(c), white, 0.8); + } + if (q.best >= 0 && upto > 60) put(rgb, gx(q.best), gy(q.best), white, 0.8); + } else if (live) put(rgb, gx(G.g), gy(G.g), white, 0.55 + 0.45 * sin(S.t * PI * 6)); + + // The board. + const fadeAll = G.phase === "clear" ? max(0, 1 - ph / 1.2) : 1; + const rows = G.rows.slice(); + if (live) rows.push({ g: G.g, ans: G.ans, rem: -1, now: true }); + const off = max(0, rows.length - ROWS); + for (let r = 0; r < ROWS; r++) { + const y = RY - RP * r, R = rows[r + off]; + if (!R) { + for (let j = 0; j < 4; j++) for (let h = 0; h < 4; h++) put(rgb, PX + 8 * j + 2 + (h & 1), y + 2 + (h >> 1), ghost, 1.3); + for (let h = 0; h < 4; h++) put(rgb, KX + 3 * (h & 1), y + 3 * (h >> 1), ghost, 1.3); + continue; + } + // Pegs go in one by one through the place beat; keys through the answer. + const shown = R.now && G.phase === "place" ? floor(ph / 0.2) + 1 : 4; + for (let j = 0; j < 4; j++) { + if (j < shown) peg(rgb, PX + 8 * j, y, pegs[DIG[R.g * 4 + j]], 0.9 * fadeAll); + else for (let h = 0; h < 4; h++) put(rgb, PX + 8 * j + 2 + (h & 1), y + 2 + (h >> 1), ghost, 1.3); + } + const keys = R.now ? (G.phase === "answer" ? min(4, floor(ph / 0.2)) : G.phase === "fade" ? 4 : 0) : 4; + const b = floor(R.ans / 5), w = R.ans % 5; + for (let h = 0; h < 4; h++) { + const x = KX + 3 * (h & 1), yy = y + 3 * (h >> 1); + const kind = h < keys ? (h < b ? kb : h < b + w ? kw : null) : null; + if (kind) for (let e = 0; e < 4; e++) put(rgb, x + (e & 1), yy + (e >> 1), kind, fadeAll); + else put(rgb, x, yy, ghost, 1.3); + } + const rem = R.now ? (G.phase === "fade" && ph > 0.5 ? null : -1) : R.rem; + if (rem !== -1) number(rgb, rem === null ? countClass(G) : rem, 63, y + 1, tone, 0.75 * fadeAll); + } + + // The secret: behind a hatched shield until it is found. + if (G.phase === "reveal" || G.phase === "clear") { + const open = G.phase === "reveal" ? min(1, ph / 0.5) : 1; + for (let j = 0; j < 4; j++) peg(rgb, PX + 8 * j, SY, pegs[DIG[G.secret * 4 + j]], open * fadeAll); + if (G.phase === "reveal") put(rgb, gx(G.secret), gy(G.secret), white, 0.5 + 0.5 * sin(ph * PI * 3)); + } else { + const sh = S.adv ? kb : ghost, v = S.adv ? 0.3 : 2.2; + for (let j = 0; j < 6; j++) for (let x = PX; x < PX + 30; x++) put(rgb, x, SY + j, sh, (x + j) % 4 === 0 ? v * 1.6 : v * 0.7); + } + + for (let p = 0; p < W * H; p++) { + const r = rgb[p * 3], g = rgb[p * 3 + 1], b = rgb[p * 3 + 2]; + if (r + g + b > 0) display.setPixel(p % W, floor(p / W), min(255, 255 * r) | 0, min(255, 255 * g) | 0, min(255, 255 * b) | 0); + } +} + +// How many codes of S give the answer that came: shown as the fade runs. +function countClass(G) { + let m = 0; + for (let i = 0; i < G.n; i++) if (FB[G.g * N + G.S[i]] === G.ans) m++; + return m; +} diff --git a/incoming/pattern-study/patterns/twindragon.js b/incoming/pattern-study/patterns/twindragon.js new file mode 100644 index 000000000..7afda069d --- /dev/null +++ b/incoming/pattern-study/patterns/twindragon.js @@ -0,0 +1,140 @@ +// ===== Patternflow pattern ===== +// Title: Twindragon +// Family: fractal +// Idea: Every other LED is a Gaussian integer written in base -1+i, the panel is tiled with twindragons of 512 numbers each in its own dim hue, and a bright window counts through all of them at once, so its shape is always a few smaller twindragons that turn 135 degrees with every digit the count carries. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: base -1+i numeration (Walter Penney, 1965), twindragon (Chandler Davis and Donald Knuth, 1970), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Digits=9..12, Split=1..8 +// +// Knob 1: Hue. Turns every tile round the wheel together: at 0 the tiles are teal, blue and magenta, a little warmer towards the bottom, and the windows run from white at the newest number to the tile's own hue. +// Knob 2: Speed. How fast the count runs: at 2 one pass through the 512 numbers of a tile takes 8 s, 64 numbers a second; at 0.2 ten times slower, at 3 one and a half times faster. A tile of more digits counts faster, so a pass only grows by the square root of 2 a digit, as the tile's width does: 22.6 s at 12. +// Knob 3: Digits. K, the digits each number in a tile has, 9 to 12. A tile holds 2^K numbers and the panel's 4096 lattice LEDs hold exactly 4096 / 2^K tiles' worth, so every number shows 8 times at 9 (15 tiles touch the panel), 4 at 10, 2 at 11 and once at 12, where the pieces of six tiles add up to one. A digit more turns the tiles 135 degrees and makes them 1.41 times wider. +// Knob 4: Split. How many windows share the bright 18.5%, spread evenly through the count. At 1, 2, 4 and 8 each window runs through its own half, quarter or eighth of the tile, all the same shape: the twindragon is two, four or eight smaller ones. At 3, 5, 6 and 7 the gap between windows is not a power of two, so they start at different places in the count and come out in different shapes. + +const W = 64, H = 128; +const { floor, round, sqrt } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Base -1+i (Penney, 1965). Every Gaussian integer a+bi has one finite +// expansion sum d_k (-1+i)^k with digits 0 and 1. The low digit is (a+b) mod 2; +// take it off and divide by -1+i: (a+bi)/(-1+i) = ((b-a) + (-a-b)i)/2. +// Checked in Node: all 160,801 points with |a|, |b| <= 200 end in at most 22 +// digits and read back to themselves; the 2^22 strings of up to 22 digits give +// 2^22 different numbers, so none of those points has a second expansion; and +// for K = 2..12 the 2^K numbers of K digits are pairwise incongruent mod +// (-1+i)^K, so their copies moved by multiples of (-1+i)^K tile the lattice +// with no point twice. +// +// z = q (-1+i)^K + r, with r the K low digits. r read as a binary number is +// where z comes in the count; q names the tile. +// +// ---- On the panel z = a+bi sits at origin + (a-b, a+b), which is (1+i) z: the +// lattice is exactly the LEDs with x+y even, the others stay off. +const OX = 32, OY = 64; + +// ---- Levels. Each lit LED has four dark neighbours, so the map's detail is +// the mean lit level / 255 and its edges are the share of lit LEDs over 40. +// The windows hold the first numbers at or above 18.5% of the count: 95 of +// 512 at K = 9. The panel is a whole number of tiles' worth for every K from 9 +// to 12 (each number shows 2^(12-K) times), so 760 of the 4096 lattice LEDs +// are bright on every frame. Those sit at 255 and the other 81.45% dim at 40: +// detail = (0.8145 * 40 + 0.1855 * 255) / 255 = 0.313, edges = 0.186, inside +// the map's cell of edges under 0.21 and detail 0.297..0.375. Dim at 40 is +// still lit (over 24) and still a soft step (not over 40). Split and Digits +// move the bright share only between 18.5% and 18.8%. +const DIM = 40, SHARE = 0.185; + +function layout(K) { + const P = 1 << K, idx = new Int32Array(W * H).fill(-1), tile = new Int32Array(W * H); + const keys = new Map(); + const ty = [], tc = []; + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const dx = x - OX, dy = y - OY; + if ((dx + dy) & 1) continue; + let a = (dx + dy) / 2, b = (dy - dx) / 2, r = 0; + for (let k = 0; k < K; k++) { + const d = (a + b) & 1; + if (d) r |= 1 << k; + a -= d; + const na = (b - a) / 2; + b = (-a - b) / 2; + a = na; + } + const key = a * 4096 + b; + let t = keys.get(key); + if (t === undefined) { + t = keys.size; + keys.set(key, t); + ty.push([0, 0]); + tc.push((((a + b) % 3) + 3) % 3); + } + ty[t][0] += y; + ty[t][1]++; + idx[y * W + x] = r; + tile[y * W + x] = t; + } + // A tile's hue follows its middle row down the panel, plus a step of 0.16 + // by (qa + qb) mod 3. A tile's six neighbours sit at q +-1, +-i and + // +-(1+i), and none of those changes qa + qb by a multiple of 3, so two tiles + // that touch never share a hue. + const hue = ty.map(([s, n], t) => 0.12 * (s / n / H) + 0.16 * tc[t]); + return { K, P, idx, tile, hue }; +} + +export function setup(params) { + params.s = { t: 0, n: 0, L: null, hue: 0, split: 1 }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 9, 1], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3), K = cl(round(k[2]), 9, 12); + S.split = cl(round(k[3]), 1, 8); + if (!S.L || S.L.K !== K) { + // Keep the place in the pass when the tile size changes. + const u = S.L ? S.n / S.L.P : 0.25; + S.L = layout(K); + S.n = u * S.L.P; + } + // 64 numbers a second at Speed 2 for K = 9, times sqrt 2 a digit more. + const rate = 32 * speed * Math.pow(sqrt(2), K - 9); + S.n = (S.n + d * rate) % S.L.P; +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.L) return; + const { P, idx, tile, hue } = S.L, m = S.split, n = floor(S.n); + const cols = hue.map((h) => { + const c = hsv(0.48 + S.hue + h, 0.8, 1); + return [round(c[0] * DIM), round(c[1] * DIM), round(c[2] * DIM), 0.48 + S.hue + h]; + }); + for (let p = 0; p < W * H; p++) { + const r = idx[p]; + if (r < 0) continue; + const c = cols[tile[p]]; + // How far back in the count this number is, as a share of the pass. + const ph = (((n - r) % P) + P) % P / P, f = ph * m - floor(ph * m); + const x = p % W, y = (p - x) / W; + if (f < SHARE) { + // Bright: white at the newest number, the tile's own hue at the oldest, + // the brightest channel 255 all along. + const s = f / SHARE, b = hsv(c[3], 0.1 + 0.8 * s, 1); + display.setPixel(x, y, round(b[0] * 255), round(b[1] * 255), round(b[2] * 255)); + } else display.setPixel(x, y, c[0], c[1], c[2]); + } +} From 9866235081ac8cc2e7826653cfb033f6c537bcc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:06:24 +0000 Subject: [PATCH 03/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Sator=20Square=20=EA=B3=BC=20Benton=20Pantogr?= =?UTF-8?q?aph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../patterns/benton-pantograph.js | 510 ++++++++++++++++++ .../pattern-study/patterns/sator-square.js | 400 ++++++++++++++ 2 files changed, 910 insertions(+) create mode 100644 incoming/pattern-study/patterns/benton-pantograph.js create mode 100644 incoming/pattern-study/patterns/sator-square.js diff --git a/incoming/pattern-study/patterns/benton-pantograph.js b/incoming/pattern-study/patterns/benton-pantograph.js new file mode 100644 index 000000000..93281822e --- /dev/null +++ b/incoming/pattern-study/patterns/benton-pantograph.js @@ -0,0 +1,510 @@ +// ===== Patternflow pattern ===== +// Title: Benton Pantograph +// Family: type +// Idea: A stylus runs round the outline of a large pattern letter while a pantograph's bars carry its motion down to a cutter that engraves the same letter at a quarter of the size on a steel blank, slanted and condensed or extended by the knobs past what the plain pantograph would give, and the finished punches of BENTON, PUNCH, MATRIX and FAMILY stand in rows below. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Benton pantograph punch-cutter (Linn Boyd Benton, 1885), the pantograph (Christoph Scheiner, 1603), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Slant=0..0.45, Width=-1..1 +// +// Knob 1: Hue. Turns the brass of the pattern and the punches round the wheel together with the steel blue of the bars; at 0 the pattern is brass and the cut metal pale gold. +// Knob 2: Speed. At 2 the stylus runs 30 units a second round the outline (the cap height is 14 units), about 94 LEDs of the pattern, so tracing a B takes 3.0 s, BENTON 25.8 s and all four words 94 s; 0.2 is a tenth as fast, 3 half as fast again. +// Knob 3: Slant. Leans the cut letter by this much of its height: 0 is upright, 0.45 leans the top 24 degrees to the right. The pattern stays upright; only the letter being cut and the punches cut after the turn take it. +// Knob 4: Width. Condenses or extends the cut letter: -1 is 55% of the pattern's width, 0 as drawn, 1 is 145%. The punches in the rows keep the width they were cut at. + +const W = 64, H = 128, NP = W * H; +const { floor, ceil, min, max, sqrt, abs, sin, cos } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The pattern letters, a heavy gothic drawn as outlines: cap height 14 +// units, stems 3, y down from the cap line, x from the left side bearing. +// Each glyph is [width, contour, hole, ...]; filled even-odd. +const GLYPH = { + B: [10.5, [0, 0, 7.8, 0, 10, 2, 10, 5.2, 8.7, 6.6, 10.5, 8.2, 10.5, 12, 8.5, 14, 0, 14], [3, 2.8, 6.8, 2.8, 7.2, 3.3, 7.2, 5, 6.8, 5.5, 3, 5.5], [3, 8.3, 7.2, 8.3, 7.6, 8.8, 7.6, 10.7, 7.2, 11.2, 3, 11.2]], + E: [9.5, [0, 0, 9.5, 0, 9.5, 2.8, 3, 2.8, 3, 5.6, 8.2, 5.6, 8.2, 8.4, 3, 8.4, 3, 11.2, 9.5, 11.2, 9.5, 14, 0, 14]], + N: [11, [0, 0, 3.2, 0, 8, 8.6, 8, 0, 11, 0, 11, 14, 7.8, 14, 3, 5.4, 3, 14, 0, 14]], + T: [11, [0, 0, 11, 0, 11, 2.8, 7, 2.8, 7, 14, 4, 14, 4, 2.8, 0, 2.8]], + O: [11.5, [3.2, 0, 8.3, 0, 11.5, 3.2, 11.5, 10.8, 8.3, 14, 3.2, 14, 0, 10.8, 0, 3.2], [4.3, 2.8, 7.2, 2.8, 8.5, 4.1, 8.5, 9.9, 7.2, 11.2, 4.3, 11.2, 3, 9.9, 3, 4.1]], + P: [10.3, [0, 0, 7.8, 0, 10.3, 2.4, 10.3, 6.4, 7.8, 8.8, 3, 8.8, 3, 14, 0, 14], [3, 2.8, 6.9, 2.8, 7.4, 3.3, 7.4, 5.5, 6.9, 6, 3, 6]], + U: [11, [0, 0, 3, 0, 3, 10.3, 3.9, 11.2, 7.1, 11.2, 8, 10.3, 8, 0, 11, 0, 11, 11, 8, 14, 3, 14, 0, 11]], + C: [11, [3.2, 0, 8, 0, 11, 3, 11, 4.6, 8, 4.6, 8, 4.1, 6.7, 2.8, 4.3, 2.8, 3, 4.1, 3, 9.9, 4.3, 11.2, 6.7, 11.2, 8, 9.9, 8, 9.4, 11, 9.4, 11, 11, 8, 14, 3.2, 14, 0, 10.8, 0, 3.2]], + H: [11, [0, 0, 3, 0, 3, 5.6, 8, 5.6, 8, 0, 11, 0, 11, 14, 8, 14, 8, 8.4, 3, 8.4, 3, 14, 0, 14]], + M: [13, [0, 0, 3.6, 0, 6.5, 7.5, 9.4, 0, 13, 0, 13, 14, 10, 14, 10, 5.5, 7.8, 11.5, 5.2, 11.5, 3, 5.5, 3, 14, 0, 14]], + A: [12, [4, 0, 8, 0, 12, 14, 8.9, 14, 8.1, 11, 3.9, 11, 3.1, 14, 0, 14], [4.6, 8.4, 7.4, 8.4, 6.2, 3.8, 5.8, 3.8]], + R: [10.8, [0, 0, 7.8, 0, 10.3, 2.4, 10.3, 6.2, 8.4, 8.2, 10.8, 14, 7.6, 14, 5.6, 8.8, 3, 8.8, 3, 14, 0, 14], [3, 2.8, 6.9, 2.8, 7.4, 3.3, 7.4, 5.5, 6.9, 6, 3, 6]], + I: [3, [0, 0, 3, 0, 3, 14, 0, 14]], + X: [12, [0, 0, 3.4, 0, 6, 4.6, 8.6, 0, 12, 0, 7.8, 7, 12, 14, 8.6, 14, 6, 9.4, 3.4, 14, 0, 14, 4.2, 7]], + F: [9.5, [0, 0, 9.5, 0, 9.5, 2.8, 3, 2.8, 3, 5.8, 8.2, 5.8, 8.2, 8.6, 3, 8.6, 3, 14, 0, 14]], + L: [9, [0, 0, 3, 0, 3, 11.2, 9, 11.2, 9, 14, 0, 14]], + Y: [12, [0, 0, 3.4, 0, 6, 5.2, 8.6, 0, 12, 0, 7.5, 8.6, 7.5, 14, 4.5, 14, 4.5, 8.6]], +}; +// Arc length along each glyph's contours, for the stylus. +const PATH = {}; +for (const ch in GLYPH) { + const cs = GLYPH[ch].slice(1), out = []; + let total = 0; + for (const c of cs) { + const n = c.length / 2, cum = [0]; + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + cum.push(cum[i] + sqrt((c[2 * j] - c[2 * i]) ** 2 + (c[2 * j + 1] - c[2 * i + 1]) ** 2)); + } + out.push({ c, n, cum, len: cum[n] }); + total += cum[n]; + } + PATH[ch] = { cs: out, total }; +} +function along(ch, d) { + // The point at distance d along contour after contour. + for (const k of PATH[ch].cs) { + if (d <= k.len) { + let i = 0; + while (i < k.n - 1 && k.cum[i + 1] < d) i++; + const j = (i + 1) % k.n, f = (d - k.cum[i]) / max(1e-6, k.cum[i + 1] - k.cum[i]); + return [k.c[2 * i] + (k.c[2 * j] - k.c[2 * i]) * f, k.c[2 * i + 1] + (k.c[2 * j + 1] - k.c[2 * i + 1]) * f]; + } + d -= k.len; + } + const k = PATH[ch].cs[PATH[ch].cs.length - 1]; + return [k.c[0], k.c[1]]; +} + +// ---- The machine. The pattern's cap line at y = 5 and baseline at 49, so +// a unit is 44/14 = 3.14 LEDs, centred on x = 36. The fixed pivot O at +// (4, 90). Bars O-J of 52 and J-T of 50 LEDs, elbow to the right: over the +// whole pattern J stays inside x 31..56, y 46..101. Collars at A on O-J and +// D on J-T, a share s from O and from J, carry the links that meet at +// P = O + s (T - O), the plain pantograph's point: a copy s times the size, +// scaled about O, upright and as wide as drawn. Benton's machine added the +// motions that condense, extend and slant; here they are a stage between P +// and the cutter C, about the copy's baseline origin. +const PS = 44 / 14, PY = 5, PCX = 36, OX = 4, OY = 90, L1 = 52, L2 = 50; +const WORDS = ["BENTON", "PUNCH", "MATRIX", "FAMILY"]; +// Each word cut at its own ratio, so the collars slide between words: cap +// heights 11.4, 9.7, 13.2 and 10.6 LEDs. +const RATIO = [0.26, 0.22, 0.3, 0.24]; +const V = 30, SLIDE = 0.5, LIFT = 0.25, ROUT = 0.45, XFER = 0.6, HOLD = 1, SHIFT = 0.8; +// The rows of finished punches: the last word above, baselines 112 and 126. +const ROWY = [112, 126]; + +// ---- The timeline, in seconds at Speed 2: per letter the pattern slides +// in, the stylus traces, the cutter clears round the letter, and the punch +// goes to its row; per word a hold and the rows moving up. +const SEG = []; +let TOTAL = 0; +WORDS.forEach((w, wi) => { + for (let li = 0; li < w.length; li++) { + const ch = w[li], tr = PATH[ch].total / V + LIFT * (PATH[ch].cs.length - 1); + for (const [st, d] of [["slide", SLIDE], ["trace", tr], ["rout", ROUT], ["xfer", XFER]]) { + SEG.push({ wi, li, st, t0: TOTAL, d }); + TOTAL += d; + } + } + SEG.push({ wi, li: w.length, st: "hold", t0: TOTAL, d: HOLD }); + TOTAL += HOLD; + SEG.push({ wi, li: w.length, st: "shift", t0: TOTAL, d: SHIFT }); + TOTAL += SHIFT; +}); +// A fresh start comes in as the O of BENTON slides in, with B, E, N and T +// in the lower row and FAMILY above, all cut at the knobs as they are. +const START = (() => { + const g = SEG.find((s) => s.wi === 0 && s.li === 4 && s.st === "slide"); + return g.t0 + 0.2 * g.d; +})(); + +function elbow(tx, ty) { + const dx = tx - OX, dy = ty - OY, d = sqrt(dx * dx + dy * dy); + const a = (L1 * L1 - L2 * L2 + d * d) / (2 * d), h = sqrt(max(0, L1 * L1 - a * a)); + const ux = dx / d, uy = dy / d; + return [OX + a * ux - uy * h, OY + a * uy + ux * h]; +} + +export function setup(params) { + params.s = { + tau: START, hue: 0, speed: 2, slant: 0, width: 1, dt: 0, + rec: {}, cov: new Float32Array(NP), rgb: new Float32Array(NP * 3), + }; +} + +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.slant = cl(k[2], 0, 0.45); + S.width = 1 + 0.45 * cl(k[3], -1, 1); + S.tau += (d * S.speed) / 2; + S.dt = d; + // A punch keeps the slant and width it was cut at, from the moment the + // cutter has cleared round it. + const g = seg(S.tau); + if (g.st === "xfer") S.rec[g.wi + "," + g.li] = [S.slant, S.width]; +} + +function seg(tau) { + const t = ((tau % TOTAL) + TOTAL) % TOTAL; + let lo = 0, hi = SEG.length - 1; + while (lo < hi) { + const m = (lo + hi + 1) >> 1; + if (SEG[m].t0 <= t) lo = m; + else hi = m - 1; + } + const g = SEG[lo]; + return { ...g, e: cl((t - g.t0) / g.d, 0, 1), loc: t - g.t0 }; +} + +// ---- Drawing helpers. +function add(rgb, p, c, v) { + rgb[p * 3] += c[0] * v, rgb[p * 3 + 1] += c[1] * v, rgb[p * 3 + 2] += c[2] * v; +} +// A line, sampled every half LED and spread over the four nearest LEDs. +function line(rgb, x0, y0, x1, y1, c, v) { + const n = max(1, ceil(sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 2)); + for (let i = 0; i <= n; i++) { + const x = x0 + ((x1 - x0) * i) / n - 0.5, y = y0 + ((y1 - y0) * i) / n - 0.5; + const ix = floor(x), iy = floor(y), ax = x - ix, ay = y - iy, a = v / 2; + if (iy >= 0 && iy < H) { + if (ix >= 0 && ix < W) add(rgb, iy * W + ix, c, a * (1 - ax) * (1 - ay)); + if (ix + 1 >= 0 && ix + 1 < W) add(rgb, iy * W + ix + 1, c, a * ax * (1 - ay)); + } + if (iy + 1 >= 0 && iy + 1 < H) { + if (ix >= 0 && ix < W) add(rgb, (iy + 1) * W + ix, c, a * (1 - ax) * ay); + if (ix + 1 >= 0 && ix + 1 < W) add(rgb, (iy + 1) * W + ix + 1, c, a * ax * ay); + } + } +} +function dot(rgb, x, y, r, c, v) { + for (let j = max(0, floor(y - r - 1)); j <= min(H - 1, floor(y + r + 1)); j++) + for (let i = max(0, floor(x - r - 1)); i <= min(W - 1, floor(x + r + 1)); i++) { + const d = sqrt((i + 0.5 - x) ** 2 + (j + 0.5 - y) ** 2), a = cl(r + 0.5 - d, 0, 1); + if (a > 0) add(rgb, j * W + i, c, v * a); + } +} +// A glyph placed by its baseline origin (ox, oy), unit size u, width w and +// slant k: unit point (x, y) goes to (ox + u (w x + k (14 - y)), oy - u (14 - y)). +const place = (x, y, ox, oy, u, w, k) => [ox + u * (w * x + k * (14 - y)), oy - u * (14 - y)]; +function contours(ch, ox, oy, u, w, k) { + return GLYPH[ch].slice(1).map((c) => { + const o = []; + for (let i = 0; i < c.length; i += 2) o.push(...place(c[i], c[i + 1], ox, oy, u, w, k)); + return o; + }); +} +// Even-odd coverage of a set of contours, four sub-rows a row and exact +// along each sub-row. Returns the box it wrote, cleared first. +function fill(cov, cs) { + let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; + for (const c of cs) + for (let i = 0; i < c.length; i += 2) (x0 = min(x0, c[i])), (x1 = max(x1, c[i])), (y0 = min(y0, c[i + 1])), (y1 = max(y1, c[i + 1])); + const bx0 = max(0, floor(x0)), bx1 = min(W - 1, floor(x1)), by0 = max(0, floor(y0)), by1 = min(H - 1, floor(y1)); + for (let y = by0; y <= by1; y++) for (let x = bx0; x <= bx1; x++) cov[y * W + x] = 0; + const xs = []; + for (let y = by0; y <= by1; y++) + for (let sub = 0; sub < 4; sub++) { + const ys = y + (sub + 0.5) / 4; + xs.length = 0; + for (const c of cs) { + const n = c.length / 2; + for (let i = 0; i < n; i++) { + const j = (i + 1) % n, ya = c[2 * i + 1], yb = c[2 * j + 1]; + if ((ya <= ys && yb > ys) || (yb <= ys && ya > ys)) xs.push(c[2 * i] + ((ys - ya) * (c[2 * j] - c[2 * i])) / (yb - ya)); + } + } + xs.sort((a, b) => a - b); + for (let i = 0; i + 1 < xs.length; i += 2) { + const a = max(xs[i], bx0), b = min(xs[i + 1], bx1 + 1); + for (let x = floor(a); x < b && x <= bx1; x++) cov[y * W + x] += (min(x + 1, b) - max(x, a)) / 4; + } + } + return [bx0, bx1, by0, by1]; +} +// Fills that stay put from frame to frame (the pattern at rest, punches in +// their rows, the face being cleared) are kept by key and copied back. +const CACHE = new Map(); +function fillK(cov, key, cs) { + const hit = CACHE.get(key); + if (hit) { + const [x0, x1, y0, y1] = hit.r; + let i = 0; + for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) cov[y * W + x] = hit.v[i++]; + return hit.r; + } + const r = fill(cov, cs), v = new Float32Array(max(0, (r[1] - r[0] + 1) * (r[3] - r[2] + 1))); + let i = 0; + for (let y = r[2]; y <= r[3]; y++) for (let x = r[0]; x <= r[1]; x++) v[i++] = cov[y * W + x]; + if (CACHE.size > 96) CACHE.clear(); + CACHE.set(key, { r, v }); + return r; +} +function outline(rgb, cs, c, v) { + for (const k of cs) { + const n = k.length / 2; + for (let i = 0; i < n; i++) { + const j = (i + 1) % n; + line(rgb, k[2 * i], k[2 * i + 1], k[2 * j], k[2 * j + 1], c, v); + } + } +} +// The box a placed glyph needs, with a margin of m LEDs. +function box(ch, ox, oy, u, w, k, m) { + const gw = GLYPH[ch][0]; + return [ox - m, ox + u * (w * gw + k * 14) + m, oy - u * 14 - m, oy + m]; +} +// A punch: a steel blank with the letter's face standing bright on it. +function punch(S, ch, ox, oy, u, w, k, a, steel, face, still) { + const rgb = S.rgb, b = box(ch, ox, oy, u, w, k, 1.2); + for (let y = max(0, floor(b[2])); y <= min(H - 1, floor(b[3])); y++) + for (let x = max(0, floor(b[0])); x <= min(W - 1, floor(b[1])); x++) { + const edge = x === floor(b[0]) || y === floor(b[2]) ? 1.35 : x === floor(b[1]) || y === floor(b[3]) ? 0.6 : 1; + add(rgb, y * W + x, steel, 30 * edge * a); + } + const key = still ? [ch, ox, oy, u, w, k].map((v) => (typeof v === "number" ? v.toFixed(3) : v)).join() : null; + const cs = contours(ch, ox, oy, u, w, k), r = key ? fillK(S.cov, key, cs) : fill(S.cov, cs); + for (let y = r[2]; y <= r[3]; y++) + for (let x = r[0]; x <= r[1]; x++) { + const v = min(1, S.cov[y * W + x]); + if (v > 0) add(rgb, y * W + x, face, 200 * v * a); + } +} + +// Where a word's punches stand in a row: unit size and each origin, the +// blanks 1 LED apart and the row at most 62 LEDs. +function rowLayout(S, wi) { + const w = WORDS[wi], u0 = RATIO[wi] * PS; + const kw = []; + let sum = 0; + for (let i = 0; i < w.length; i++) { + const r = S.rec[wi + "," + i] || [S.slant, S.width]; + kw.push(r); + sum += r[1] * GLYPH[w[i]][0] + r[0] * 14; + } + const u = min(u0, (62 - 3.4 * w.length + 1) / sum); + let x = (W - (u * sum + 3.4 * w.length - 1)) / 2 + 1.2; + const o = []; + for (let i = 0; i < w.length; i++) { + o.push([x, kw[i][0], kw[i][1]]); + x += u * (kw[i][1] * GLYPH[w[i]][0] + kw[i][0] * 14) + 3.4; + } + return { u, o }; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const rgb = S.rgb; + rgb.fill(0); + const g = seg(S.tau), wi = g.wi, word = WORDS[wi]; + const brass = hsv(0.115 + S.hue, 0.62, 1), hot = hsv(0.13 + S.hue, 0.22, 1), steel = hsv(0.6 + S.hue, 0.28, 1), dim = hsv(0.62 + S.hue, 0.5, 1); + const prevW = (wi + WORDS.length - 1) % WORDS.length; + + // The ratio of the word being cut; the collars slide to the next one + // while the rows move up. + const nextR = RATIO[(wi + 1) % WORDS.length]; + const s = g.st === "shift" ? RATIO[wi] + (nextR - RATIO[wi]) * ss(0, 1, g.e) : RATIO[wi]; + + // ---- The pattern: cap line and baseline, the letter filled dim with its + // outline, the part the stylus has passed bright. + for (const y of [PY - 1, PY + 44]) + for (let x = 0; x < W; x++) add(rgb, y * W + x, dim, x % 4 < 2 ? 22 : 10); + const li = min(g.li, word.length - 1), ch = word[li], gw = GLYPH[ch][0]; + let off = 0; + if (g.st === "slide") off = 46 * (1 - ss(0, 1, g.e)); + else if (g.st === "xfer") off = -50 * ss(0, 1, g.e); + const showPattern = g.st !== "hold" && g.st !== "shift"; + const pox = PCX - (gw * PS) / 2 + off, poy = PY + 44; + const P = (x, y) => place(x, y, pox, poy, PS, 1, 0); + let done = 0; + if (g.st === "trace") done = g.loc * V - 0; + else if (g.st === "rout" || g.st === "xfer") done = 1e9; + if (showPattern) { + const cs = contours(ch, pox, poy, PS, 1, 0); + const r = off === 0 ? fillK(S.cov, "P" + ch, cs) : fill(S.cov, cs); + for (let y = r[2]; y <= r[3]; y++) + for (let x = r[0]; x <= r[1]; x++) { + const v = min(1, S.cov[y * W + x]); + if (v > 0) add(rgb, y * W + x, brass, 62 * v); + } + outline(rgb, cs, brass, 120); + // Traced so far: the distance along the outline, less the lifts. + if (done > 0) { + let left = g.st === "trace" ? 0 : 1e9; + if (g.st === "trace") { + let t = g.loc; + for (const k of PATH[ch].cs) { + const need = k.len / V; + if (t <= need) { + left += t * V; + break; + } + left += k.len; + t -= need + LIFT; + if (t < 0) break; + } + } + let acc = 0; + for (const k of PATH[ch].cs) { + for (let i = 0; i < k.n; i++) { + const a = k.cum[i], b = k.cum[i + 1]; + if (acc + a >= left) break; + const j = (i + 1) % k.n, f = min(1, (left - acc - a) / max(1e-6, b - a)); + const p0 = P(k.c[2 * i], k.c[2 * i + 1]), p1 = P(k.c[2 * j], k.c[2 * j + 1]); + line(rgb, p0[0], p0[1], p0[0] + (p1[0] - p0[0]) * f, p0[1] + (p1[1] - p0[1]) * f, hot, 150); + } + acc += k.len; + } + S.left = left; + } + } + + // ---- The stylus T over the time of this letter. + const REST = [60, 30]; + let T = REST, cutting = false; + const startPt = P(...along(ch, 0)); + if (g.st === "slide") { + const m = ss(0, 1, g.e); + T = [REST[0] + (startPt[0] - REST[0]) * m, REST[1] + (startPt[1] - REST[1]) * m]; + } else if (g.st === "trace") { + let t = g.loc, d = 0, lifted = null; + for (let c = 0; c < PATH[ch].cs.length; c++) { + const k = PATH[ch].cs[c], need = k.len / V; + if (t <= need) { + d += t * V; + break; + } + d += k.len; + t -= need; + if (c + 1 < PATH[ch].cs.length && t <= LIFT) { + lifted = [P(...along(ch, d - 1e-3)), P(...along(ch, d + 1e-3)), ss(0, 1, t / LIFT)]; + break; + } + t -= LIFT; + } + if (lifted) T = [lifted[0][0] + (lifted[1][0] - lifted[0][0]) * lifted[2], lifted[0][1] + (lifted[1][1] - lifted[0][1]) * lifted[2]]; + else (T = P(...along(ch, d))), (cutting = true); + } else if (g.st === "rout") { + // The cutter clears round the letter in six passes, top to bottom. + const pass = g.e * 6, i = floor(min(5.999, pass)), f = pass - i; + const xa = pox - 3, xb = pox + gw * PS + 3, y = PY - 2 + ((i + f) / 6) * 48; + T = [i % 2 ? xb + (xa - xb) * f : xa + (xb - xa) * f, y]; + cutting = true; + } else if (g.st === "xfer") { + // From where the clearing ended, lifted, back to rest. + const end = [pox - off - 3, PY + 46], m = ss(0, 1, g.e); + T = [end[0] + (REST[0] - end[0]) * m, end[1] + (REST[1] - end[1]) * m]; + } + + // ---- Linkage geometry. + const J = elbow(T[0], T[1]); + const A = [OX + s * (J[0] - OX), OY + s * (J[1] - OY)], D = [J[0] + s * (T[0] - J[0]), J[1] + s * (T[1] - J[1])]; + const Pp = [OX + s * (T[0] - OX), OY + s * (T[1] - OY)]; + // The copy's baseline origin under the plain pantograph, and the stage. + const bo = [OX + s * (pox - off - OX), OY + s * (poy - OY)], u = s * PS; + const k = S.slant, w = S.width; + const stage = (x, y) => [bo[0] + w * (x - bo[0]) + k * (bo[1] - y), y]; + const C = stage(Pp[0], Pp[1]); + + // ---- The copy on its blank: the plain pantograph's copy as a faint + // ghost, the cut outline so far, then the face as the cutter clears. + if (g.st === "slide" || g.st === "trace" || g.st === "rout") { + const fadeIn = g.st === "slide" ? ss(0.3, 1, g.e) : 1; + const b = box(ch, bo[0], bo[1], u, w, k, 1.5); + const clearY = g.st === "rout" ? C[1] : g.st === "trace" ? -1 : -1; + for (let y = max(0, floor(b[2])); y <= min(H - 1, floor(b[3])); y++) + for (let x = max(0, floor(b[0])); x <= min(W - 1, floor(b[1])); x++) add(rgb, y * W + x, steel, (y + 0.5 < clearY ? 16 : 34) * fadeIn); + outline(rgb, contours(ch, bo[0], bo[1], u, 1, 0), dim, 42 * fadeIn); + const cs = contours(ch, bo[0], bo[1], u, w, k); + if (g.st === "rout") { + const r = fillK(S.cov, ["F", ch, bo[0], bo[1], u, w, k].map((v) => (typeof v === "number" ? v.toFixed(3) : v)).join(), cs); + for (let y = r[2]; y <= r[3]; y++) + for (let x = r[0]; x <= r[1]; x++) { + const v = min(1, S.cov[y * W + x]) * (y + 0.5 < clearY ? 1 : 0.25); + if (v > 0) add(rgb, y * W + x, hot, 170 * v); + } + } + if (g.st === "trace" || g.st === "rout") { + const left = g.st === "rout" ? 1e9 : S.left || 0; + let acc = 0; + for (let c = 0; c < PATH[ch].cs.length; c++) { + const kk = PATH[ch].cs[c], pc = cs[c]; + for (let i = 0; i < kk.n; i++) { + const a = kk.cum[i], bb = kk.cum[i + 1]; + if (acc + a >= left) break; + const j = (i + 1) % kk.n, f = min(1, (left - acc - a) / max(1e-6, bb - a)); + line(rgb, pc[2 * i], pc[2 * i + 1], pc[2 * i] + (pc[2 * j] - pc[2 * i]) * f, pc[2 * i + 1] + (pc[2 * j + 1] - pc[2 * i + 1]) * f, hot, 190); + } + acc += kk.len; + } + } + } + + // ---- Rows of finished punches. During the shift the upper row rises + // and fades and the lower row moves up into its place. + const shift = g.st === "shift" ? ss(0, 1, g.e) : 0; + { + const L = rowLayout(S, prevW); + for (let i = 0; i < WORDS[prevW].length; i++) punch(S, WORDS[prevW][i], L.o[i][0], ROWY[0] - 16 * shift, L.u, L.o[i][2], L.o[i][1], 1 - shift, steel, brass, shift === 0); + } + { + const L = rowLayout(S, wi), n = g.st === "hold" || g.st === "shift" ? word.length : g.li; + for (let i = 0; i < n; i++) punch(S, word[i], L.o[i][0], ROWY[1] - (ROWY[1] - ROWY[0]) * shift, L.u, L.o[i][2], L.o[i][1], 1, steel, brass, shift === 0); + if (g.st === "xfer") { + // The finished punch flies from the cutter to its place in the row. + const m = ss(0, 1, g.e), r = S.rec[wi + "," + g.li] || [k, w]; + const tx = L.o[g.li][0], ty = ROWY[1]; + const ox = bo[0] + (tx - bo[0]) * m, oy = bo[1] + (ty - bo[1]) * m - 10 * sin(m * Math.PI); + punch(S, ch, ox, oy, u + (L.u - u) * m, r[1], r[0], 1, steel, brass); + } + } + + // ---- Bars, collars, pivot, stylus and cutter. + const bar = (p, q, v) => { + const dx = q[0] - p[0], dy = q[1] - p[1], l = sqrt(dx * dx + dy * dy) || 1, nx = (-dy / l) * 0.45, ny = (dx / l) * 0.45; + line(rgb, p[0] + nx, p[1] + ny, q[0] + nx, q[1] + ny, steel, v); + line(rgb, p[0] - nx, p[1] - ny, q[0] - nx, q[1] - ny, steel, v); + }; + bar([OX, OY], J, 95); + bar(J, T, 95); + bar(A, Pp, 75); + bar(D, Pp, 75); + dot(rgb, A[0], A[1], 1.2, steel, 150); + dot(rgb, D[0], D[1], 1.2, steel, 150); + dot(rgb, J[0], J[1], 1.1, steel, 140); + // The pivot on its bracket. + for (let y = OY + 1; y <= OY + 5; y++) { + const hw = (y - OY) * 0.8; + for (let x = max(0, floor(OX - hw)); x <= min(W - 1, floor(OX + hw)); x++) add(rgb, y * W + x, steel, 55); + } + for (let x = 0; x <= 9; x++) add(rgb, (OY + 6) * W + x, steel, x % 2 ? 90 : 45); + dot(rgb, OX, OY, 1.4, hot, 210); + // The stage: P where the plain pantograph would cut, a ring; the link to + // the cutter C. + if (abs(C[0] - Pp[0]) > 0.6) line(rgb, Pp[0], Pp[1], C[0], C[1], dim, 120); + dot(rgb, Pp[0], Pp[1], 1.1, steel, 120); + dot(rgb, T[0], T[1], 1.2, hot, 230); + line(rgb, T[0] - 3, T[1], T[0] + 3, T[1], hot, 60); + line(rgb, T[0], T[1] - 3, T[0], T[1] + 3, hot, 60); + if (cutting) { + dot(rgb, C[0], C[1], 0.9, hot, 255); + for (let i = 0; i < 3; i++) { + const a = Math.random() * 6.283, r = 0.8 + Math.random() * 2.2; + dot(rgb, C[0] + cos(a) * r, C[1] + sin(a) * r, 0.3, hot, 160 * Math.random()); + } + } else dot(rgb, C[0], C[1], 0.8, steel, 120); + + for (let p = 0; p < NP; p++) + display.setPixel(p % W, floor(p / W), min(255, rgb[p * 3]) | 0, min(255, rgb[p * 3 + 1]) | 0, min(255, rgb[p * 3 + 2]) | 0); +} diff --git a/incoming/pattern-study/patterns/sator-square.js b/incoming/pattern-study/patterns/sator-square.js new file mode 100644 index 000000000..2cd02ca10 --- /dev/null +++ b/incoming/pattern-study/patterns/sator-square.js @@ -0,0 +1,400 @@ +// ===== Patternflow pattern ===== +// Title: Sator Square +// Family: type +// Idea: The 25 letters of the word square found at Pompeii, cut in a stone tablet, are read by four lights at once (across, down, backwards and up, always standing on the same letter), then fly by the shortest matching to their places in Felix Grosser's Paternoster cross, which stands the full height of the panel with A and O at the four ends, hold while two lights read PATERNOSTER down and across to meet at the N, and fly home. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Sator square (Pompeii, before AD 79; Dura-Europos), Paternoster cross (Felix Grosser, 1926), the assignment problem solved by the Hungarian method (Harold Kuhn, 1955), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Assign=0..2, Weather=0..1 +// +// Knob 1: Hue. Turns the light round the wheel: at 0 the letters glow gold and the four letters of the cross's ends, A and O, glow pale azure on grey stone. +// Knob 2: Speed. One round of reading, flight, cross and flight home takes 14 s at 2, 9.3 s at 3 and 140 s at 0.2. +// Knob 3: Assign. Which letter flies to which place of its kind. 0 is the matching with the least total travel, 537.6 LEDs for the 25 flights; 1 sends the k-th A of the square, read across, to the k-th A of the cross, read down and then across, 864.9 LEDs; 2 is the matching with the most travel, 1083.3 LEDs, so the flights cross over each other. +// Knob 4: Weather. How long the tablet has stood out: at 0.06 the cuts are fresh; towards 1 the groove edges crumble, the stone pits and greys with lichen, and every letter carries its own worn patches on its flight. + +const W = 64, H = 128, NP = W * H; +const { floor, ceil, round, min, max, exp, sqrt, sin } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const ss = (a, b, v) => { + const t = cl((v - a) / (b - a), 0, 1); + return t * t * (3 - 2 * t); +}; + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Letters. Only eight are needed: S A T O R E P N. Three cuts of each: +// 9 by 9 with 2 LED strokes for the tablet, 5 by 7 for the cross's upright, +// 3 by 5 for its arm. The upright and the arm both need the same N, which +// sits at the crossing in the 5 by 7 cut, so the 3 by 5 set has no N. +const BIG = { + S: [".#######.", "##.....##", "##.......", "##.......", ".#######.", ".......##", ".......##", "##.....##", ".#######."], + A: ["...###...", "..##.##..", "..##.##..", ".##...##.", ".##...##.", ".#######.", "##.....##", "##.....##", "##.....##"], + T: ["#########", "#########", "...###...", "...###...", "...###...", "...###...", "...###...", "...###...", "...###..."], + O: ["..#####..", ".##...##.", "##.....##", "##.....##", "##.....##", "##.....##", "##.....##", ".##...##.", "..#####.."], + R: ["#######..", "##....##.", "##.....##", "##....##.", "#######..", "##..##...", "##...##..", "##....##.", "##.....##"], + E: ["#########", "##.......", "##.......", "##.......", "#######..", "##.......", "##.......", "##.......", "#########"], + P: ["#######..", "##....##.", "##.....##", "##....##.", "#######..", "##.......", "##.......", "##.......", "##......."], + N: ["##.....##", "###....##", "####...##", "##.##..##", "##..##.##", "##...####", "##....###", "##.....##", "##.....##"], +}; +const MID = { + A: [".###.", "#...#", "#...#", "#####", "#...#", "#...#", "#...#"], + P: ["####.", "#...#", "#...#", "####.", "#....", "#....", "#...."], + T: ["#####", "..#..", "..#..", "..#..", "..#..", "..#..", "..#.."], + E: ["#####", "#....", "#....", "####.", "#....", "#....", "#####"], + R: ["####.", "#...#", "#...#", "####.", "#.#..", "#..#.", "#...#"], + N: ["#...#", "##..#", "#.#.#", "#..##", "#...#", "#...#", "#...#"], + O: [".###.", "#...#", "#...#", "#...#", "#...#", "#...#", ".###."], + S: [".####", "#....", "#....", ".###.", "....#", "....#", "####."], +}; +const SMALL = { + A: [".#.", "#.#", "###", "#.#", "#.#"], + P: ["##.", "#.#", "##.", "#..", "#.."], + T: ["###", ".#.", ".#.", ".#.", ".#."], + E: ["###", "#..", "##.", "#..", "###"], + R: ["##.", "#.#", "##.", "#.#", "#.#"], + O: [".#.", "#.#", "#.#", "#.#", ".#."], + S: [".##", "#..", ".#.", "..#", "##."], +}; +const bits = (set) => { + const o = {}; + for (const k in set) o[k] = Uint8Array.from(set[k].join(""), (c) => (c === "#" ? 1 : 0)); + return o; +}; +const BIGB = bits(BIG), MIDB = bits(MID), SMALLB = bits(SMALL); + +// ---- The tablet. Rows SATOR AREPO TENET OPERA ROTAS read the same across +// and down, and the same backwards: the square is left as it is by the half +// turn and by reflection in either diagonal. Letters 9 by 9 on a 12 LED +// pitch round (32.5, 64.5), on a slab 61 by 63 from (2, 33). +const SQUARE = ["SATOR", "AREPO", "TENET", "OPERA", "ROTAS"]; +const SQ = []; +for (let r = 0; r < 5; r++) for (let c = 0; c < 5; c++) SQ.push({ ch: SQUARE[r][c], x: 32.5 + (c - 2) * 12, y: 64.5 + (r - 2) * 12 }); +const SX0 = 2, SX1 = 63, SY0 = 33, SY1 = 96; + +// The four readings as paths over the 25 cells: across (rows left to +// right), down (columns top to bottom), backwards (from the last letter) and +// up (columns from the bottom right). Whatever step k the four lights are +// on, they stand on the same letter; at k = 12 they meet on the N. A cell is +// first lit at the least of its four indices, so every cell is lit by k = 12. +const PATHS = [(k) => k, (k) => (k % 5) * 5 + floor(k / 5), (k) => 24 - k, (k) => 24 - ((k % 5) * 5 + floor(k / 5))]; +const FIRST = new Float32Array(25).fill(99); +for (const f of PATHS) for (let k = 0; k < 25; k++) FIRST[f(k)] = min(FIRST[f(k)], k); + +// ---- Grosser's cross. A-PATERNOSTER-O down the middle, 13 letters 5 by 7 +// on rows 128/13 apart, so it stands the whole height; the same 13 across +// row 64 in the 3 by 5 cut on a 5 LED pitch, sharing the N. 21 letters make +// PATERNOSTER twice; the A and O left over (alpha and omega) end the arms. +const CROSS = "APATERNOSTERO"; +const SLOT = []; +for (let j = 0; j < 13; j++) SLOT.push({ ch: CROSS[j], x: 32.5, y: floor(((j + 0.5) * H) / 13) + 0.5, w: 5, h: 7, bm: MIDB, k: j, ao: j === 0 || j === 12 }); +for (let i = 0; i < 13; i++) + if (i !== 6) SLOT.push({ ch: CROSS[i], x: i < 6 ? 27.5 - 5 * (5 - i) : 37.5 + 5 * (i - 7), y: 64.5, w: 3, h: 5, bm: SMALLB, k: i, ao: i === 0 || i === 12 }); + +// The count, checked here so the cross can never be built from letters the +// square does not have: square S2 A4 T4 O4 R4 E4 P2 N1; PATERNOSTER twice +// with one N is P2 A2 T4 E4 R4 N1 O2 S2, 21 letters, and A2 O2 are left. +{ + const n = {}; + for (const q of SQ) n[q.ch] = (n[q.ch] || 0) + 1; + for (const s of SLOT) n[s.ch]--; + for (const k in n) if (n[k] !== 0) throw new Error("Sator count is off at " + k); +} + +// ---- Which letter goes to which place of its kind. Up to four of a kind, +// so every permutation is tried (24 at most). Totals of the 25 straight +// distances, centre to centre: least 537.6 LEDs, reading order 864.9, most +// 1083.3. +function perms(n) { + if (n === 1) return [[0]]; + const out = []; + for (const p of perms(n - 1)) for (let i = 0; i < n; i++) out.push([...p.slice(0, i), n - 1, ...p.slice(i)]); + return out; +} +const dist = (q, s) => sqrt((SQ[q].x - SLOT[s].x) ** 2 + (SQ[q].y - SLOT[s].y) ** 2); +const ASSIGN = [0, 1, 2].map((mode) => { + const a = new Int8Array(25); + for (const ch of "SATORENP") { + const from = [], to = []; + SQ.forEach((q, i) => q.ch === ch && from.push(i)); + SLOT.forEach((s, i) => s.ch === ch && to.push(i)); + if (mode === 1) { + from.forEach((q, i) => (a[q] = to[i])); + continue; + } + let best = null, bv = mode === 0 ? 1e9 : -1; + for (const p of perms(from.length)) { + let v = 0; + p.forEach((j, i) => (v += dist(from[i], to[j]))); + if (mode === 0 ? v < bv : v > bv) (bv = v), (best = p); + } + best.forEach((j, i) => (a[from[i]] = to[j])); + } + return a; +}); + +// ---- Time, in seconds at Speed 2. The tablet 5 s (lights read from 0.6 s, +// 6.25 letters a second), out 2 s, the cross 5 s (read from 0.5 s, 5.2 +// letters a second), home 2 s. Each flight takes 1 s and starts 0.04 s after +// the one before it in reading order. A fresh start comes in 3.4 s into the +// tablet, at reading step 17.5, so the cross is up within 2.2 s. +const T_SQ = 5, T_OUT = 2, T_CR = 5, T_BACK = 2, CYCLE = T_SQ + T_OUT + T_CR + T_BACK; +const READ0 = 0.6, READ_RATE = 6.25, CREAD0 = 0.5, CREAD_RATE = 5.2, FLY = 1, STAG = 0.04, START = 3.4; + +// ---- Stone: grain, pits and lichen, fixed for the tablet. +function hash(x, y, s) { + let h = (x * 374761393 + y * 668265263 + s * 2147483647) >>> 0; + h = Math.imul(h ^ (h >>> 13), 1274126177) >>> 0; + return ((h ^ (h >>> 16)) >>> 0) / 4294967296; +} +function vnoise(x, y, sc, s) { + const fx = x / sc, fy = y / sc, ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy; + const sx = ax * ax * (3 - 2 * ax), sy = ay * ay * (3 - 2 * ay); + const a = hash(ix, iy, s), b = hash(ix + 1, iy, s), c = hash(ix, iy + 1, s), d = hash(ix + 1, iy + 1, s); + return (a + (b - a) * sx) * (1 - sy) + (c + (d - c) * sx) * sy; +} +const GRAIN = new Float32Array(NP), PIT = new Float32Array(NP), LICH = new Float32Array(NP), CHIP = new Float32Array(NP); +for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + GRAIN[p] = 0.55 * hash(x, y, 1) + 0.45 * vnoise(x, y, 5, 2) - 0.5; + PIT[p] = hash(x, y, 3); + LICH[p] = 0.65 * vnoise(x, y, 7, 4) + 0.35 * vnoise(x, y, 2.5, 5); + CHIP[p] = hash(x, y, 6); + } +// Carved grooves of the 25 cells, lit from the upper left: the wall under +// an upper or left edge faces away from the light, the wall above a lower +// or right edge faces it. 0 stone, 1 shadow wall, 2 lit wall, 3 floor. +const CUT = new Uint8Array(NP), NEAR = new Uint8Array(NP); +for (const q of SQ) { + const bm = BIGB[q.ch], x0 = round(q.x - 4.5), y0 = round(q.y - 4.5); + const m = (i, j) => (i < 0 || j < 0 || i > 8 || j > 8 ? 0 : bm[j * 9 + i]); + for (let j = 0; j < 9; j++) + for (let i = 0; i < 9; i++) { + const p = (y0 + j) * W + x0 + i; + if (!m(i, j)) { + if (m(i - 1, j) || m(i, j - 1) || m(i + 1, j) || m(i, j + 1)) NEAR[p] = 1; + continue; + } + const ul = !m(i - 1, j) || !m(i, j - 1) || !m(i - 1, j - 1), lr = !m(i + 1, j) || !m(i, j + 1) || !m(i + 1, j + 1); + CUT[p] = ul && !lr ? 1 : lr && !ul ? 2 : 3; + } +} + +export function setup(params) { + params.s = { + tau: START, hue: 0, speed: 2, mode: 0, wx: 0.06, dt: 0, + lr: new Float32Array(NP), lg: new Float32Array(NP), lb: new Float32Array(NP), + tr: new Float32Array(NP), tg: new Float32Array(NP), tb: new Float32Array(NP), tmp: new Float32Array(NP), + rgb: new Float32Array(NP * 3), + }; +} + +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.mode = cl(round(k[2]), 0, 2); + S.wx = cl(k[3], 0, 1); + S.tau += (d * S.speed) / 2; + S.dt = d; +} + +// Bilinear sample of a bitmap at bitmap coordinates. +function samp(bm, bw, bh, fx, fy) { + const ix = floor(fx), iy = floor(fy), ax = fx - ix, ay = fy - iy; + let v = 0; + if (iy >= 0 && iy < bh) { + if (ix >= 0 && ix < bw) v += bm[iy * bw + ix] * (1 - ax) * (1 - ay); + if (ix + 1 >= 0 && ix + 1 < bw) v += bm[iy * bw + ix + 1] * ax * (1 - ay); + } + if (iy + 1 >= 0 && iy + 1 < bh) { + if (ix >= 0 && ix < bw) v += bm[(iy + 1) * bw + ix] * (1 - ax) * ay; + if (ix + 1 >= 0 && ix + 1 < bw) v += bm[(iy + 1) * bw + ix + 1] * ax * ay; + } + return v; +} + +// One letter of light into the light buffer: the tablet cut, scaled to w by +// h, fading into the cross cut by mix, worn by letter q's own patches. +function letter(S, q, a, b, bw, bh, mix, cx, cy, w, h, col, inten, home) { + const x0 = max(0, floor(cx - w / 2)), x1 = min(W - 1, ceil(cx + w / 2) - 1); + const y0 = max(0, floor(cy - h / 2)), y1 = min(H - 1, ceil(cy + h / 2) - 1); + const wx = S.wx, sub = w < 8.5 ? 2 : 1; + for (let y = y0; y <= y1; y++) + for (let x = x0; x <= x1; x++) { + let va = 0, vb = 0; + for (let sy = 0; sy < sub; sy++) + for (let sx = 0; sx < sub; sx++) { + const u = (x + (sx + 0.5) / sub - (cx - w / 2)) / w, v = (y + (sy + 0.5) / sub - (cy - h / 2)) / h; + if (mix < 1) va += samp(a, 9, 9, u * 9 - 0.5, v * 9 - 0.5); + if (mix > 0) vb += samp(b, bw, bh, u * bw - 0.5, v * bh - 0.5); + } + let val = (va * (1 - mix) + vb * mix) / (sub * sub); + if (val < 0.02) continue; + const u = (x + 0.5 - (cx - w / 2)) / w, v = (y + 0.5 - (cy - h / 2)) / h; + const n = vnoise(u * 4 + q * 7.3, v * 4, 1, 40 + q); + val *= 1 - 0.8 * wx * ss(0.3, 0.6, n); + val *= inten; + const p = y * W + x; + // At home the light lies in the groove: least on the wall in shadow. + if (home) val *= CUT[p] === 1 ? 0.5 : CUT[p] === 2 ? 1 : 0.78; + if (col[0] * val > S.lr[p]) S.lr[p] = col[0] * val; + if (col[1] * val > S.lg[p]) S.lg[p] = col[1] * val; + if (col[2] * val > S.lb[p]) S.lb[p] = col[2] * val; + } +} + +function blur(src, tmp, out) { + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + tmp[p] = (6 * src[p] + 4 * ((x > 0 ? src[p - 1] : 0) + (x < W - 1 ? src[p + 1] : 0)) + (x > 1 ? src[p - 2] : 0) + (x < W - 2 ? src[p + 2] : 0)) / 16; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const p = y * W + x; + out[p] = (6 * tmp[p] + 4 * ((y > 0 ? tmp[p - W] : 0) + (y < H - 1 ? tmp[p + W] : 0)) + (y > 1 ? tmp[p - 2 * W] : 0) + (y < H - 2 ? tmp[p + 2 * W] : 0)) / 16; + } +} + +function spot(rgb, x, y, rad, a, col) { + const r = ceil(rad * 2.2); + for (let j = max(0, floor(y - r)); j <= min(H - 1, ceil(y + r)); j++) + for (let i = max(0, floor(x - r)); i <= min(W - 1, ceil(x + r)); i++) { + const dx = i + 0.5 - x, dy = j + 0.5 - y, v = a * exp(-(dx * dx + dy * dy) / (rad * rad)); + const p = (j * W + i) * 3; + rgb[p] += col[0] * v, rgb[p + 1] += col[1] * v, rgb[p + 2] += col[2] * v; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { rgb, lr, lg, lb } = S, wx = S.wx, assign = ASSIGN[S.mode]; + const tc = ((S.tau % CYCLE) + CYCLE) % CYCLE; + const gold = hsv(0.115 + S.hue, 0.6, 1), azure = hsv(0.55 + S.hue, 0.42, 1), warm = hsv(0.1 + S.hue, 0.25, 1); + + // Where each letter is: p is its flight, 0 at home in the tablet, 1 in + // the cross. + const fly = new Float32Array(25); + let gone = 0; + for (let q = 0; q < 25; q++) { + let p = 0; + if (tc >= T_SQ && tc < T_SQ + T_OUT) p = ss(0, 1, (tc - T_SQ - STAG * q) / FLY); + else if (tc >= T_SQ + T_OUT && tc < T_SQ + T_OUT + T_CR) p = 1; + else if (tc >= T_SQ + T_OUT + T_CR) p = 1 - ss(0, 1, (tc - T_SQ - T_OUT - T_CR - STAG * q) / FLY); + fly[q] = p; + gone += p / 25; + } + + // ---- Stone, dimmed to 38% while the letters' light is away. + const dim = 1 - 0.62 * gone; + rgb.fill(0); + const stone = [0.82, 0.86, 0.93], moss = [0.72, 0.8, 0.55]; + for (let y = SY0; y < SY1; y++) + for (let x = SX0; x < SX1; x++) { + const p = y * W + x; + let v = 62 * (1 + 0.32 * GRAIN[p]); + if (y === SY0 || x === SX0) v *= 1.45; + else if (y === SY1 - 1 || x === SX1 - 1) v *= 0.55; + const c = CUT[p]; + if (c) { + const chip = CHIP[p] < 0.45 * wx; + v *= chip ? 0.6 : c === 1 ? 0.3 : c === 2 ? 1.9 : 0.55; + } else if (NEAR[p] && CHIP[p] < 0.4 * wx) v *= 0.45; + if (PIT[p] < 0.1 * wx) v *= 0.35; + const l = ss(1 - 0.55 * wx, 1.05 - 0.55 * wx, LICH[p]) * min(1, wx * 2); + v *= dim; + const o = p * 3; + rgb[o] = v * (stone[0] + (moss[0] - stone[0]) * l); + rgb[o + 1] = v * (stone[1] + (moss[1] - stone[1]) * l); + rgb[o + 2] = v * (stone[2] + (moss[2] - stone[2]) * l); + } + + // ---- The cross's own faint body of light, there as far as its letters + // have arrived: a band 8 LEDs wide down the middle and one across row 64. + const body = gone * gone * 34; + if (body > 0.5) + for (let y = 0; y < H; y++) { + const by = exp(-(((y + 0.5 - 64.5) / 2.6) ** 2)), sh = 0.86 + 0.14 * sin(y * 0.35 - tc * 5); + for (let x = 0; x < W; x++) { + const v = body * max(exp(-(((x + 0.5 - 32.5) / 4) ** 2)) * sh, by); + if (v < 0.5) continue; + const o = (y * W + x) * 3; + rgb[o] += gold[0] * v, rgb[o + 1] += gold[1] * v, rgb[o + 2] += gold[2] * v; + } + } + + // ---- Light. Trails fade in about 0.07 s. + const decay = exp(-S.dt / 0.07); + for (let p = 0; p < NP; p++) (lr[p] *= decay), (lg[p] *= decay), (lb[p] *= decay); + + const inSq = tc < T_SQ, inCr = tc >= T_SQ + T_OUT && tc < T_SQ + T_OUT + T_CR; + const step = (tc - READ0) * READ_RATE; + const cstep = (tc - T_SQ - T_OUT - CREAD0) * CREAD_RATE; + for (let q = 0; q < 25; q++) { + const from = SQ[q], to = SLOT[assign[q]], p = fly[q]; + let glow = 1; + if (inSq) glow = max(1 - ss(0, 0.6, tc), ss(FIRST[q], FIRST[q] + 1.2, step)); + if (p <= 0) { + letter(S, q, BIGB[from.ch], null, 9, 9, 0, from.x, from.y, 9, 9, gold, 0.1 + 0.72 * glow, true); + continue; + } + // A quadratic arc, bowed a fifth of its length to the left of the way + // out, so the 25 flights turn together. + const dx = to.x - from.x, dy = to.y - from.y, cx = (from.x + to.x) / 2 - 0.2 * dy, cy = (from.y + to.y) / 2 + 0.2 * dx; + const e = p, f = 1 - e; + const px = f * f * from.x + 2 * e * f * cx + e * e * to.x, py = f * f * from.y + 2 * e * f * cy + e * e * to.y; + const w = 9 + (to.w - 9) * e, h = 9 + (to.h - 9) * e; + const mix = ss(0.35, 0.8, e); + const ao = to.ao ? e : 0; + const col = [gold[0] + (azure[0] - gold[0]) * ao, gold[1] + (azure[1] - gold[1]) * ao, gold[2] + (azure[2] - gold[2]) * ao]; + let inten = 0.95; + if (inCr) { + const fl = exp(-((cstep - to.k) ** 2) / 0.6); + inten = 0.78 + 0.4 * fl + (to.ao ? 0.14 * sin(tc * 3.1) : 0); + } + letter(S, q, BIGB[from.ch], to.bm[to.ch], to.w, to.h, mix, px, py, w, h, col, inten); + } + + blur(lr, S.tmp, S.tr), blur(lg, S.tmp, S.tg), blur(lb, S.tmp, S.tb); + for (let p = 0; p < NP; p++) { + const o = p * 3; + rgb[o] += 235 * lr[p] + 150 * S.tr[p]; + rgb[o + 1] += 235 * lg[p] + 150 * S.tg[p]; + rgb[o + 2] += 235 * lb[p] + 150 * S.tb[p]; + } + + // ---- The reading lights: four on the tablet, two on the cross. + if (inSq && step > -0.5 && step < 25) { + const k = floor(step), fr = step - k; + for (const f of PATHS) { + const a = SQ[f(cl(k, 0, 24))], b = SQ[f(cl(k + 1, 0, 24))]; + const sameLine = k >= 0 && k < 24 && (k % 5 !== 4); + const m = sameLine ? ss(0.25, 0.75, fr) : 0; + const fade = (k < 0 ? ss(-0.5, 0, step) : 1) * (sameLine || k >= 24 ? 1 : 1 - ss(0.55, 0.95, fr)) * (k > 0 && k % 5 === 0 ? ss(0, 0.2, fr) : 1); + spot(rgb, a.x + (b.x - a.x) * m, a.y + (b.y - a.y) * m, 4.2, 150 * fade, warm); + } + } + if (inCr && cstep > -0.5 && cstep < 13) { + const k = floor(cstep), fr = cstep - k, m = ss(0.2, 0.8, fr); + const up = (j) => SLOT[cl(j, 0, 12)], arm = (i) => SLOT[i === 6 ? 6 : i < 6 ? 13 + i : 12 + i]; + const fade = ss(-0.5, 0, cstep) * (1 - ss(12.4, 13, cstep)); + const a = up(k), b = up(k + 1); + spot(rgb, 32.5, a.y + (b.y - a.y) * m, 3.2, 110 * fade, warm); + const c = arm(cl(k, 0, 12)), d = arm(cl(k + 1, 0, 12)); + spot(rgb, c.x + (d.x - c.x) * m, 64.5, 2.4, 110 * fade, warm); + } + + for (let p = 0; p < NP; p++) + display.setPixel(p % W, floor(p / W), min(255, rgb[p * 3]) | 0, min(255, rgb[p * 3 + 1]) | 0, min(255, rgb[p * 3 + 2]) | 0); +} From 9f6642927d1956e5365ca650932d839641f4699e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:06:27 +0000 Subject: [PATCH 04/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Tensegrity=20=EA=B3=BC=20Problem=20of=20Apoll?= =?UTF-8?q?onius?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../patterns/apollonius-problem.js | 279 ++++++++++++++ incoming/pattern-study/patterns/tensegrity.js | 351 ++++++++++++++++++ 2 files changed, 630 insertions(+) create mode 100644 incoming/pattern-study/patterns/apollonius-problem.js create mode 100644 incoming/pattern-study/patterns/tensegrity.js diff --git a/incoming/pattern-study/patterns/apollonius-problem.js b/incoming/pattern-study/patterns/apollonius-problem.js new file mode 100644 index 000000000..55e11cffe --- /dev/null +++ b/incoming/pattern-study/patterns/apollonius-problem.js @@ -0,0 +1,279 @@ +// ===== Patternflow pattern ===== +// Title: Problem of Apollonius +// Family: geometry +// Idea: Three white circles drift over the panel and the up to eight circles that touch all three are drawn round them, each coloured by which of the three it touches from inside and which from outside, with a bright point at every touch; when two white circles cross or one nests in another, the eight merge in pairs and fade to four or none, and come back when they part. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: problem of Apollonius (Apollonius of Perga, c. 200 BC; solved by François ViÚte, 1600), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Pair=0..4, Size=-1..1 +// +// Knob 1: Hue. Turns the eight colours round the wheel together. Opposite signatures sit opposite on the wheel: at 0 the circle touching all three from outside is gold and the one holding all three is blue. +// Knob 2: Speed. How fast the white circles drift: at 2 each centre's x and y run on sines of 37 to 71 seconds and each radius breathes on 19 to 31, at 0.2 ten times slower, at 3 one and a half times faster. +// Knob 3: Pair. 0 shows all eight. 1 to 4 light one of the four sign systems: its two circles stay bright with a faint wash inside, the rest dim to a fifth, and a line runs through the two centres and the radical centre of the white circles, the white point every system's line passes through. +// Knob 4: Size. Scales the white circles by 2 to this power: at -1 they are 2.7 to 7.7 LEDs in radius and all eight are there 91% of the time; at 0.06, where it opens, 5.6 to 16, and 61%; at 1 11 to 31, and 13%, the rest mostly four. + +const W = 64, H = 128; +const { floor, min, max, sqrt, sin, cos, abs, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [[v, q, p, p, t, v][i], [t, v, v, q, p, p][i], [p, p, t, v, v, q][i]]; +} + +// ---- The three given circles. Radius at Size 0, breathing 18% on period +// rp; the centre's x swings across the panel's width and its y through its +// own band, y0 +- ay, each on its own sine and kept a radius from the edge. +// Periods in seconds at Speed 2. +const GIVEN = [ + { r: 9, y0: 34, ay: 30, px: 47, py: 61, ph: [0.3, 1.9], rp: 23 }, + { r: 13, y0: 64, ay: 36, px: 59, py: 43, ph: [2.2, 0.4], rp: 31 }, + { r: 6.5, y0: 94, ay: 30, px: 37, py: 71, ph: [4.1, 3.3], rp: 19 }, +]; + +// ---- Solving. Circle i is (xi, yi, ri). A circle (x, y, r) touching it +// satisfies (x - xi)^2 + (y - yi)^2 = (r + si ri)^2, si = +1 outside it and -1 +// on the other side of its rim. Subtracting circle 1's equation from 2's and +// 3's leaves two equations linear in x, y and r; two of the three unknowns are +// solved as linear in the third (the one whose 2 by 2 is largest, so three +// centres in a row do not break it) and put into circle 1's equation, which +// leaves a quadratic. s1 is held at +1: a root with r < 0 is the circle of +// radius -r for the opposite signs, so four sign systems give up to eight. +// The roots come back in order of r, and each system fills two slots. +// Checked over 30 minutes at Speed 2 and each of Size -1, 0.06, 0.5 and 1: +// the worst miss of a found circle against the three was 3.7e-9 LEDs, every +// touch point lay within 4e-11 of both rims, and each system's two centres +// stood in line with the radical centre to within 2e-12 radians. +// The count is 8, 4 or 0 and nothing else: 400,000 random triples gave 8 when +// no two cross or all three do, 4 when one or two pairs cross, 0 when one +// circle sits inside another and the third outside it. Eight is not always +// one of each signature: with the centres near a line a system's two roots +// can both be positive, two circles of one signature and none of its +// opposite. At the opening Size, of the frames with eight, 40% held all eight +// signatures. +function solve(C, k, out) { + const s = [1, k & 1 ? -1 : 1, k & 2 ? -1 : 1]; + const K = (i) => C[i][0] * C[i][0] + C[i][1] * C[i][1] - C[i][2] * C[i][2]; + const M = [1, 2].map((j) => [2 * (C[j][0] - C[0][0]), 2 * (C[j][1] - C[0][1]), 2 * (s[j] * C[j][2] - s[0] * C[0][2]), K(j) - K(0)]); + let best = -1, f = 0; + for (let g = 0; g < 3; g++) { + const p = (g + 1) % 3, q = (g + 2) % 3, d = abs(M[0][p] * M[1][q] - M[1][p] * M[0][q]); + if (d > best) (best = d), (f = g); + } + const p = (f + 1) % 3, q = (f + 2) % 3, det = M[0][p] * M[1][q] - M[1][p] * M[0][q]; + out.n = 0; + if (abs(det) < 1e-9) return; + // u = U0 + U1 t, t the free unknown. + const U0 = [0, 0, 0], U1 = [0, 0, 0]; + U1[f] = 1; + U0[p] = (M[1][q] * M[0][3] - M[0][q] * M[1][3]) / det; + U1[p] = (M[0][q] * M[1][f] - M[1][q] * M[0][f]) / det; + U0[q] = (M[0][p] * M[1][3] - M[1][p] * M[0][3]) / det; + U1[q] = (M[1][p] * M[0][f] - M[0][p] * M[1][f]) / det; + const ex = U0[0] - C[0][0], ey = U0[1] - C[0][1], er = U0[2] + C[0][2]; + const A = U1[0] * U1[0] + U1[1] * U1[1] - U1[2] * U1[2]; + const B = 2 * (ex * U1[0] + ey * U1[1] - er * U1[2]); + const Cc = ex * ex + ey * ey - er * er; + const roots = []; + if (abs(A) < 1e-12 * (abs(B) + abs(Cc) + 1e-30)) { + if (abs(B) > 1e-30) roots.push(-Cc / B); + } else { + const disc = B * B - 4 * A * Cc; + if (disc < 0) return; + const sq = sqrt(disc), qq = -0.5 * (B + (B < 0 ? -sq : sq)); + roots.push(qq / A); + if (qq !== 0) roots.push(Cc / qq); + } + const sol = roots.map((t) => [U0[0] + U1[0] * t, U0[1] + U1[1] * t, U0[2] + U1[2] * t]).filter((u) => isFinite(u[2]) && abs(u[2]) > 1e-9 && abs(u[2]) < 1e9); + sol.sort((a, b) => a[2] - b[2]); + for (const u of sol) { + const r = u[2], sg = r > 0 ? 1 : -1; + // Signature: bit i set when the circle touches circle i from its inner side. + let sig = 0; + for (let i = 0; i < 3; i++) if (s[i] * sg < 0) sig |= 4 >> i; + out.c[out.n++] = { x: u[0], y: u[1], r: abs(r), sig }; + } +} + +// The worst miss of a found circle against the three it should touch, in +// LEDs: the distance between centres against r + ri or |r - ri|. +function miss(C, c) { + let worst = 0; + for (let i = 0; i < 3; i++) { + const d = sqrt((c.x - C[i][0]) ** 2 + (c.y - C[i][1]) ** 2); + worst = max(worst, min(abs(d - (c.r + C[i][2])), abs(d - abs(c.r - C[i][2])))); + } + return worst; +} + +export function setup(params) { + params.s = { + // The clock starts 7 s in, where all eight stand for the next 17 s. + t: 7, hue: 0, pair: 0, size: 0, + // Eight slots, two to a sign system: the circle last seen and how lit it is. + slot: Array.from({ length: 8 }, () => ({ c: null, on: false, a: 0 })), + C: [[0, 0, 1], [0, 0, 1], [0, 0, 1]], rc: null, worst: 0, count: 0, + rgb: new Float32Array(W * H * 3), out: { n: 0, c: [null, null] }, + }; +} + +function place(S) { + const sc = 2 ** S.size; + for (let i = 0; i < 3; i++) { + const g = GIVEN[i], r = g.r * sc * (1 + 0.18 * sin((2 * PI * S.t) / g.rp + g.ph[0])); + const mx = W / 2 - r - 1, lo = max(r + 1, g.y0 - g.ay), hi = max(lo, min(H - 1 - r, g.y0 + g.ay)); + S.C[i][0] = W / 2 + max(0, mx) * sin((2 * PI * S.t) / g.px + g.ph[0]); + S.C[i][1] = (lo + hi) / 2 + ((hi - lo) / 2) * sin((2 * PI * S.t) / g.py + g.ph[1]); + S.C[i][2] = r; + } +} + +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.pair = cl(round(k[2]), 0, 4); + S.size = cl(k[3], -1, 1); + S.t += d * cl(k[1], 0.2, 3) * 0.5; + place(S); + let count = 0; + for (let sys = 0; sys < 4; sys++) { + solve(S.C, sys, S.out); + for (let j = 0; j < 2; j++) { + const sl = S.slot[2 * sys + j]; + // One root: the second slot is the circle that just became a line. + const c = S.out.n === 2 ? S.out.c[j] : S.out.n === 1 && j === 0 ? S.out.c[0] : null; + sl.on = !!c; + if (c) { + sl.c = c; + count++; + S.worst = max(S.worst, miss(S.C, c)); + } + // Fades: in over 0.4 s, out over 0.7 s, whatever the Speed. + sl.a = sl.on ? min(1, sl.a + d / 0.4) : max(0, sl.a - d / 0.7); + } + } + S.count = count; + // The radical centre: the point with equal power to all three circles, + // where r = 0 on every system's line of centres. + const C = S.C, a1 = 2 * (C[1][0] - C[0][0]), b1 = 2 * (C[1][1] - C[0][1]), a2 = 2 * (C[2][0] - C[0][0]), b2 = 2 * (C[2][1] - C[0][1]); + const K = (i) => C[i][0] * C[i][0] + C[i][1] * C[i][1] - C[i][2] * C[i][2], dd = a1 * b2 - a2 * b1; + S.rc = abs(dd) > 1e-6 ? [((K(1) - K(0)) * b2 - (K(2) - K(0)) * b1) / dd, (a1 * (K(2) - K(0)) - a2 * (K(1) - K(0))) / dd] : null; +} + +// ---- Drawing. Additive light into a float buffer. +function put(rgb, px, py, c, v) { + if (px < 0 || px >= W || py < 0 || py >= H || v <= 0) return; + const p = 3 * (py * W + px); + rgb[p] += c[0] * v, rgb[p + 1] += c[1] * v, rgb[p + 2] += c[2] * v; +} + +// An anti-aliased ring 1 LED wide, clipped to the panel. Rows the circle +// does not reach are skipped, and in each row only the columns near the two +// crossings are visited. +function ring(rgb, cx, cy, r, c, v) { + const y0 = max(0, floor(cy - r - 1)), y1 = min(H - 1, floor(cy + r + 1)); + for (let py = y0; py <= y1; py++) { + const dy = py + 0.5 - cy, ro = r + 1, ri = r - 1; + const xo = ro * ro - dy * dy; + if (xo < 0) continue; + const wo = sqrt(xo), wi = ri > 0 && ri * ri - dy * dy > 0 ? sqrt(ri * ri - dy * dy) : 0; + for (const sgn of [-1, 1]) { + const a = sgn < 0 ? cx - wo : cx + wi, b = sgn < 0 ? cx - wi : cx + wo; + const xa = max(0, floor(a)), xb = min(W - 1, floor(b)); + for (let px = xa; px <= xb; px++) { + const dx = px + 0.5 - cx, e = abs(sqrt(dx * dx + dy * dy) - r); + if (e < 1) put(rgb, px, py, c, v * (1 - e)); + } + } + } +} + +function dot(rgb, x, y, c, v, rad) { + for (let py = floor(y - rad - 1); py <= floor(y + rad + 1); py++) + for (let px = floor(x - rad - 1); px <= floor(x + rad + 1); px++) { + const e = sqrt((px + 0.5 - x) ** 2 + (py + 0.5 - y) ** 2); + if (e < rad + 0.5) put(rgb, px, py, c, v * min(1, rad + 0.5 - e)); + } +} + +function disc(rgb, cx, cy, r, c, v) { + const y0 = max(0, floor(cy - r)), y1 = min(H - 1, floor(cy + r)); + for (let py = y0; py <= y1; py++) { + const dy = py + 0.5 - cy, w = r * r - dy * dy; + if (w <= 0) continue; + const hw = sqrt(w), xa = max(0, floor(cx - hw + 0.5)), xb = min(W - 1, floor(cx + hw - 0.5)); + for (let px = xa; px <= xb; px++) put(rgb, px, py, c, v); + } +} + +function line(rgb, x0, y0, x1, y1, c, v) { + const n = max(abs(x1 - x0), abs(y1 - y0)) | 0; + for (let i = 0; i <= n; i++) { + const u = n ? i / n : 0; + put(rgb, floor(x0 + (x1 - x0) * u), floor(y0 + (y1 - y0) * u), c, v); + } +} + +// Hue of a signature: the pair from one quadratic, sig and 7 - sig, opposite. +const SIGHUE = [0, 1, 2, 3, 7, 6, 5, 4]; + +export function draw(display, params) { + const S = params.s; + if (!S || !S.C) return; + const rgb = S.rgb, C = S.C; + rgb.fill(0); + const white = [1, 1, 1]; + + // A highlighted system: its line of centres, from edge to edge. + if (S.pair && S.rc) { + const a = S.slot[2 * (S.pair - 1)], b = S.slot[2 * (S.pair - 1) + 1], ends = [a, b].filter((q) => q.a > 0 && q.c); + const far = ends.length ? ends.reduce((u, q) => (abs(q.c.x - S.rc[0]) + abs(q.c.y - S.rc[1]) > abs(u.c.x - S.rc[0]) + abs(u.c.y - S.rc[1]) ? q : u)) : null; + if (far) { + let ux = far.c.x - S.rc[0], uy = far.c.y - S.rc[1]; + const L = sqrt(ux * ux + uy * uy); + if (L > 1e-6) { + (ux /= L), (uy /= L); + line(rgb, S.rc[0] - ux * 300, S.rc[1] - uy * 300, S.rc[0] + ux * 300, S.rc[1] + uy * 300, white, 0.16); + } + } + } + + // The solutions, then the given circles over them, then the touches. + const touch = []; + for (let q = 0; q < 8; q++) { + const sl = S.slot[q]; + if (sl.a <= 0 || !sl.c) continue; + const c = sl.c, col = hsv(0.13 + S.hue + SIGHUE[c.sig] / 8, 0.78, 1); + const lit = S.pair && floor(q / 2) !== S.pair - 1 ? 0.2 : 1; + ring(rgb, c.x, c.y, c.r, col, 0.95 * sl.a * lit); + if (S.pair && lit === 1) { + // The lit pair: a faint wash inside each, and its centre. + disc(rgb, c.x, c.y, c.r, col, 0.11 * sl.a); + dot(rgb, c.x, c.y, col, 0.9 * sl.a, 0.7); + } + for (let i = 0; i < 3; i++) { + const dx = C[i][0] - c.x, dy = C[i][1] - c.y, dd = sqrt(dx * dx + dy * dy); + if (dd < 1e-9) continue; + // From the centre toward circle i, unless circle i holds this one. + const out = C[i][2] > c.r && dd < C[i][2] ? -1 : 1; + touch.push([c.x + (out * c.r * dx) / dd, c.y + (out * c.r * dy) / dd, col, sl.a * lit]); + } + } + for (let i = 0; i < 3; i++) ring(rgb, C[i][0], C[i][1], C[i][2], white, 0.62); + for (const [x, y, col, a] of touch) { + dot(rgb, x, y, col, 0.9 * a, 1.1); + dot(rgb, x, y, white, 0.8 * a, 0.45); + } + if (S.rc && S.pair) dot(rgb, S.rc[0], S.rc[1], white, 0.9, 0.8); + + for (let p = 0; p < W * H; p++) { + const r = rgb[3 * p], g = rgb[3 * p + 1], b = rgb[3 * p + 2]; + if (r + g + b > 0.004) display.setPixel(p % W, (p / W) | 0, min(255, 255 * r) | 0, min(255, 255 * g) | 0, min(255, 255 * b) | 0); + } +} diff --git a/incoming/pattern-study/patterns/tensegrity.js b/incoming/pattern-study/patterns/tensegrity.js new file mode 100644 index 000000000..ea1bea7c3 --- /dev/null +++ b/incoming/pattern-study/patterns/tensegrity.js @@ -0,0 +1,351 @@ +// ===== Patternflow pattern ===== +// Title: Tensegrity +// Family: physics +// Idea: A tower of struts that never touch one another stands the height of the panel, held up by a net of cables in tension and turning slowly, and a weight on its top squeezes it shorter and twists it, because in a tensegrity compression and twist come together. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: tensegrity (Kenneth Snelson, 1948; named by R. Buckminster Fuller), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Struts=3..6, Load=0..1 +// +// Knob 1: Hue. Turns every colour round the wheel together: at 0 the struts run from aqua at the foot to blue at the top, the cables are a dim pink and the weight is pale stone. +// Knob 2: Speed. How fast the tower turns and how fast its own time runs: at 2 one turn in 16 seconds, at 0.2 one in 160 seconds with a plucked cable swinging in slow motion, at 3 one in 11 seconds. +// Knob 3: Struts. Struts per stage, 3 to 6, five stages each time: 15 struts and 93 cables at 3, 30 and 186 at 6. Turning it builds the new tower and settles it before the next frame, which costs up to 7 ms. +// Knob 4: Load. A weight on a guide up the axis, pressing on the top: 0 to 700 units, 42 at the start, which leaves the tower 1% shorter and its top turned 5.5 degrees. At 1 the 3-strut tower is 17% shorter and its top has turned 74 degrees further round; the 6-strut one 16% and 92.5 degrees. +// Buttons: K1 tap plucks one of the longer cables on the near side: it swings as a string for two seconds at a pitch set by its tension, and if the tower is still its two ends are kicked so the whole tower shivers. + +const W = 64, H = 128; +const { floor, min, max, sqrt, sin, cos, abs, atan2, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [[v, q, p, p, t, v][i], [t, v, v, q, p, p][i], [p, p, t, v, v, q][i]]; +} + +// ---- The tower. Stage k is a tensegrity prism: n struts from its bottom +// n-gon to its top n-gon, strut i from bottom node i to top node i + 1, a +// side cable from bottom node i to top node i, and a ring of cables round +// each n-gon. The prism alone settles with its top turned pi/2 - pi/n against +// its bottom along the side cables, so pi/2 + pi/n along the struts. Run on +// its own with this code's springs from 20 or 70 degrees round, cables 10% +// short, it came to 30.0000, 45.0000, 54.0000 and 60.0000 degrees for n = 3, +// 4, 5 and 6 (from 5 degrees too for 3 and 4; 5 and 6 went slack from there). +// Stages overlap by a quarter of their height: stage k + 1's bottom n-gon +// sits below stage k's top one, turned pi/n so its nodes fall between, and +// hangs from it by a zigzag of 2n saddle cables; bottom-to-bottom and +// top-to-top cables between the two stages pull the other way. Stacked, the +// stages twist less than a lone prism: 20 to 24 degrees for n = 3, 33 to 38 +// for 4, 41 to 46 for 5, 46 to 52 for 6. No two struts share a node, and the +// closest two pass 8.4 units apart at n = 3, 3.4 at n = 6 (3.0 under Load 1). +// Units: the bare tower stands 143 to 150 units tall and is drawn 121 LEDs. +const STAGES = 5, HS = 34, OV = 9, R0 = 24, R1 = 17; +const ALL = 0.08; // every cable 8% shorter than the span it is built on: the prestress +const EA = 1800; // cable stiffness: tension = EA * stretch / rest length +const FMAX = 700; // the weight at Load 1, shared out over the top nodes +const KICK = 40; // a pluck, units a second sideways at each end of the cable +const HSUB = 1 / 120; // physics step while it runs, seconds of the tower's own time + +function build(n) { + const P = [], fix = [], struts = [], cables = [], B = [], T = [], stage = []; + const alpha = PI / 2 - PI / n; + let phi = 0; + for (let k = 0; k < STAGES; k++) { + const r = R0 + ((R1 - R0) * k) / (STAGES - 1), zb = k * (HS - OV), zt = zb + HS; + B.push([]); + T.push([]); + for (let i = 0; i < n; i++) { + const a = phi + (2 * PI * i) / n; + B[k].push(P.length / 3); + P.push(r * cos(a), zb, r * sin(a)); + fix.push(k === 0); + } + for (let i = 0; i < n; i++) { + const a = phi + alpha + (2 * PI * i) / n; + T[k].push(P.length / 3); + P.push(r * cos(a), zt, r * sin(a)); + fix.push(false); + } + for (let i = 0; i < n; i++) { + struts.push(B[k][i], T[k][(i + 1) % n]); + stage.push(k); + cables.push(B[k][i], T[k][i]); + cables.push(B[k][i], B[k][(i + 1) % n], T[k][i], T[k][(i + 1) % n]); + } + phi += alpha + PI / n; + } + for (let k = 0; k + 1 < STAGES; k++) + for (let j = 0; j < n; j++) { + cables.push(B[k + 1][j], T[k][j], B[k + 1][j], T[k][(j + 1) % n]); + cables.push(B[k + 1][j], B[k][j], T[k][j], T[k + 1][j]); + } + const N = P.length / 3, x = Float64Array.from(P); + const len = (a, b) => sqrt((x[3 * a] - x[3 * b]) ** 2 + (x[3 * a + 1] - x[3 * b + 1]) ** 2 + (x[3 * a + 2] - x[3 * b + 2]) ** 2); + const SL = new Float64Array(struts.length / 2), CL = new Float64Array(cables.length / 2); + for (let q = 0; q < SL.length; q++) SL[q] = len(struts[2 * q], struts[2 * q + 1]); + for (let q = 0; q < CL.length; q++) CL[q] = len(cables[2 * q], cables[2 * q + 1]) * (1 - ALL); + return { + n, N, x, v: new Float64Array(3 * N), xp: new Float64Array(3 * N), f: new Float64Array(3 * N), + fix, struts, cables, SL, CL, ten: new Float64Array(CL.length), top: T[STAGES - 1], stage, + }; +} + +// One step: cables pull (tension only), the weight presses, velocities damp, +// then the struts are put back to length, four passes. +function step(m, load, damp, h) { + const { x, v, xp, f, cables, CL, ten, struts, SL, fix, N } = m; + f.fill(0); + const w = load / m.top.length; + for (const i of m.top) f[3 * i + 1] -= w; + for (let q = 0; q < CL.length; q++) { + const a = 3 * cables[2 * q], b = 3 * cables[2 * q + 1]; + const dx = x[b] - x[a], dy = x[b + 1] - x[a + 1], dz = x[b + 2] - x[a + 2], L = sqrt(dx * dx + dy * dy + dz * dz); + const t = L > CL[q] ? (EA * (L - CL[q])) / CL[q] : 0; + ten[q] = t; + if (!t) continue; + const s = t / L; + f[a] += dx * s, f[a + 1] += dy * s, f[a + 2] += dz * s; + f[b] -= dx * s, f[b + 1] -= dy * s, f[b + 2] -= dz * s; + } + const keep = 1 - damp * h; + for (let i = 0; i < N; i++) { + if (fix[i]) continue; + for (let d = 3 * i; d < 3 * i + 3; d++) { + xp[d] = x[d]; + v[d] = (v[d] + h * f[d]) * keep; + x[d] += h * v[d]; + } + } + for (let it = 0; it < 4; it++) + for (let q = 0; q < SL.length; q++) { + const ia = struts[2 * q], ib = struts[2 * q + 1], a = 3 * ia, b = 3 * ib; + const dx = x[b] - x[a], dy = x[b + 1] - x[a + 1], dz = x[b + 2] - x[a + 2], L = sqrt(dx * dx + dy * dy + dz * dz); + const wa = fix[ia] ? 0 : 1, wb = fix[ib] ? 0 : 1, e = (L - SL[q]) / L / (wa + wb); + x[a] += dx * e * wa, x[a + 1] += dy * e * wa, x[a + 2] += dz * e * wa; + x[b] -= dx * e * wb, x[b + 1] -= dy * e * wb, x[b + 2] -= dz * e * wb; + } + // The weight runs on a guide up the axis, so the top nodes' centre stays on + // it. Without the guide the tower bowed sideways and folded under 150 to + // 200 units, while only 5 units shorter; with it, 900 left every cable taut. + let gx = 0, gz = 0; + for (const i of m.top) (gx += x[3 * i] / m.top.length), (gz += x[3 * i + 2] / m.top.length); + for (const i of m.top) (x[3 * i] -= gx), (x[3 * i + 2] -= gz); + for (let i = 0; i < N; i++) if (!fix[i]) for (let d = 3 * i; d < 3 * i + 3; d++) v[d] = (x[d] - xp[d]) / h; +} + +// Settling before the first frame: 1/30 s steps, damped near critical for +// the tower's slowest sway, until no node moves 0.002 units in 50 steps. +// The bare tower takes 600 to 650 steps, under its weight 400 to 1,300 more. +function relax(m, load, damp) { + const was = Float64Array.from(m.x); + let s = 0; + for (; s < 4000; s++) { + step(m, load, damp, 1 / 30); + if (s % 50 === 49) { + let moved = 0; + for (let d = 0; d < was.length; d++) (moved = max(moved, abs(m.x[d] - was[d]))), (was[d] = m.x[d]); + if (moved < 0.002) break; + } + } + m.v.fill(0); + return s + 1; +} + +export function setup(params) { + params.s = { + t: 0, turn: 0.6, n: 0, m: null, hue: 0, load: 0.06, lnow: 0.06, acc: 0, pl: -1, plT: -9, + span: [0, 1], rgb: new Float32Array(W * H * 3), z: new Float32Array(W * H), order: [], + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 3, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const speed = cl(k[1], 0.2, 3), n = cl(round(k[2]), 3, 6); + S.load = cl(k[3], 0, 1); + if (n !== S.n) { + // A new tower: settle it bare to fix the scale, then under its weight, + // overdamped so it does not overshoot. At the start knobs the first frame + // runs 650 + 500 steps, 1.5 ms in Node. + S.n = n; + S.m = build(n); + S.steps = relax(S.m, 0, 1.2); + const x = S.m.x; + let lo = 1e9, hi = -1e9; + for (let i = 0; i < S.m.N; i++) (lo = min(lo, x[3 * i + 1])), (hi = max(hi, x[3 * i + 1])); + S.span = [lo, hi]; + S.lnow = S.load; + S.steps += relax(S.m, S.lnow * FMAX, 2.5); + } + const m = S.m; + if (input.btnPressed && input.btnPressed[0]) { + // Pluck one of the longer cables on the side facing the camera. Its ends + // are kicked apart only once the last pluck has died down: kicks every + // 0.3 s used to pump the tower until it folded. + const c = cos(S.turn), sn = sin(S.turn), pick = []; + let lmax = 0; + for (let q = 0; q < m.CL.length; q++) lmax = max(lmax, m.CL[q]); + for (let q = 0; q < m.CL.length; q++) { + const a = 3 * m.cables[2 * q], b = 3 * m.cables[2 * q + 1]; + if (m.fix[a / 3] && m.fix[b / 3]) continue; + if ((m.x[a] + m.x[b]) * sn + (m.x[a + 2] + m.x[b + 2]) * c > 12 && m.CL[q] > 0.5 * lmax) pick.push(q); + } + const q = pick.length ? pick[floor(Math.random() * pick.length)] : 0; + const a = 3 * m.cables[2 * q], b = 3 * m.cables[2 * q + 1]; + let ke = 0; + for (let j = 0; j < m.v.length; j++) ke += m.v[j] * m.v[j]; + const ux = m.x[b + 2] - m.x[a + 2], uz = m.x[a] - m.x[b], L = sqrt(ux * ux + uz * uz) || 1; + if (ke < 0.5 * KICK * KICK) + for (const [j, sg] of [[a, 1], [b, -1]]) if (!m.fix[j / 3]) (m.v[j] += (sg * KICK * ux) / L), (m.v[j + 2] += (sg * KICK * uz) / L); + S.pl = q; + S.plT = S.t; + } + // The tower's own time; a turned Load knob reaches it over half a second. + const sim = d * speed * 0.5; + S.lnow += (S.load - S.lnow) * min(1, sim / 0.5); + S.t += sim; + S.turn += sim * ((2 * PI) / 16); + S.acc += sim; + for (let i = 0; S.acc >= HSUB && i < 40; i++) { + step(m, S.lnow * FMAX, 1.2, HSUB); + S.acc -= HSUB; + } + if (S.acc > HSUB) S.acc = 0; +} + +// ---- Drawing. Orthographic, the camera 10 degrees above the horizon, the +// tower turning about its axis. Each LED keeps the depth of what it shows. +const TILT = 0.17, TC = cos(TILT), TS = sin(TILT); + +// A 1 LED line from (x0, y0) to (x1, y1), bowed sideways by bow LEDs at +// its middle, each sample shared between the two LEDs it falls across. +function cable(rgb, zb, x0, y0, z0, x1, y1, z1, bow, col, v) { + const ex = x1 - x0, ey = y1 - y0, L = sqrt(ex * ex + ey * ey) || 1, nx = -ey / L, ny = ex / L; + const along = abs(ex) >= abs(ey), steps = (max(abs(ex), abs(ey)) + abs(bow) * 2) | 0; + for (let i = 0; i <= steps; i++) { + const u = steps ? i / steps : 0, o = bow * sin(PI * u); + const fx = x0 + ex * u + nx * o, fy = y0 + ey * u + ny * o, z = z0 + (z1 - z0) * u; + const g = along ? fy - 0.5 : fx - 0.5, g0 = floor(g), w1 = g - g0; + for (let e = 0; e < 2; e++) { + const px = along ? floor(fx) : g0 + e, py = along ? g0 + e : floor(fy); + if (px < 0 || px >= W || py < 0 || py >= H) continue; + const p = py * W + px, wv = (e ? w1 : 1 - w1) * v; + if (z > zb[p] - 0.8) continue; + for (let j = 0; j < 3; j++) rgb[3 * p + j] = max(rgb[3 * p + j], col[j] * wv); + } + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.m) return; + const m = S.m, x = m.x, rgb = S.rgb, zb = S.z, n = m.n; + rgb.fill(0); + zb.fill(1e9); + const scale = 121 / (S.span[1] - S.span[0]), yc = (S.span[0] + S.span[1]) / 2; + const c = cos(S.turn), s = sin(S.turn); + const N = m.N, sx = new Float32Array(N), sy = new Float32Array(N), sz = new Float32Array(N); + for (let i = 0; i < N; i++) { + const X = x[3 * i] * c - x[3 * i + 2] * s, Z = x[3 * i] * s + x[3 * i + 2] * c, Y = x[3 * i + 1] - yc; + sx[i] = 32 + scale * X; + sy[i] = 64.5 - scale * (Y * TC - Z * TS); + sz[i] = -Z; // smaller is nearer + } + + // The weight: a disc on the top nodes, its thickness growing with Load. + if (S.load > 0.001) { + let cx = 0, cy = 0, cz = 0, rr = 0; + for (const i of m.top) (cx += x[3 * i] / n), (cy += x[3 * i + 1] / n), (cz += x[3 * i + 2] / n); + for (const i of m.top) rr = max(rr, sqrt((x[3 * i] - cx) ** 2 + (x[3 * i + 2] - cz) ** 2)); + const R = rr * 0.72, th = 1.5 + 9 * S.load, X0 = cx * c - cz * s, Z0 = cx * s + cz * c, Y0 = cy + 0.5 - yc; + const col = hsv(0.08 + S.hue, 0.18, 1); + // The guide the weight runs on, up the axis to the top edge. + const rx = round(32 + scale * X0 - 0.5); + for (let py = 0; py < 64.5 - scale * ((Y0 + th) * TC - Z0 * TS); py++) + if (rx >= 0 && rx < W) (rgb[3 * (py * W + rx)] = 0.3 * col[0]), (rgb[3 * (py * W + rx) + 1] = 0.3 * col[1]), (rgb[3 * (py * W + rx) + 2] = 0.3 * col[2]); + for (let py = 0; py < 30; py++) + for (let px = 0; px < W; px++) { + const X = (px + 0.5 - 32) / scale - X0; + if (abs(X) >= R) continue; + const hz = sqrt(R * R - X * X), up = (64.5 - (py + 0.5)) / scale; + // Top face: the point at height Y0 + th whose screen row this is. + const zt = ((Y0 + th) * TC - up) / TS - Z0; + let v = 0, depth = 0; + if (abs(zt) <= hz) (v = 0.5 + 0.2 * (zt / hz)), (depth = -(Z0 + zt)); + else { + const ys = (up + (Z0 + hz) * TS) / TC; + if (ys >= Y0 && ys <= Y0 + th) (v = 0.3 + 0.18 * (hz / R)), (depth = -(Z0 + hz)); + } + if (!v) continue; + const p = py * W + px; + zb[p] = depth; + rgb[3 * p] = col[0] * v, rgb[3 * p + 1] = col[1] * v, rgb[3 * p + 2] = col[2] * v; + } + } + + // Struts, far ones first: a bar 2.4 LEDs across, brighter at the near end + // and down its middle, with a dark rim that cuts whatever passes behind. + const ns = m.SL.length, ord = S.order; + ord.length = ns; + for (let q = 0; q < ns; q++) ord[q] = q; + const mid = (q) => sz[m.struts[2 * q]] + sz[m.struts[2 * q + 1]]; + ord.sort((a, b) => mid(b) - mid(a)); + const RAD = 1.2, HALO = 1.5, DEEP = R0 + 2; + for (const q of ord) { + const a = m.struts[2 * q], b = m.struts[2 * q + 1]; + const x0 = sx[a], y0 = sy[a], x1 = sx[b], y1 = sy[b], z0 = sz[a], z1 = sz[b]; + const ex = x1 - x0, ey = y1 - y0, ll = ex * ex + ey * ey || 1; + const col = hsv(0.47 + S.hue + 0.04 * m.stage[q], 0.62, 1); + const reach = RAD + HALO + 0.5; + const X0 = max(0, floor(min(x0, x1) - reach)), X1 = min(W - 1, floor(max(x0, x1) + reach)); + const Y0 = max(0, floor(min(y0, y1) - reach)), Y1 = min(H - 1, floor(max(y0, y1) + reach)); + for (let py = Y0; py <= Y1; py++) + for (let px = X0; px <= X1; px++) { + const u = cl(((px + 0.5 - x0) * ex + (py + 0.5 - y0) * ey) / ll, 0, 1); + const dx = px + 0.5 - x0 - ex * u, dy = py + 0.5 - y0 - ey * u, dd = sqrt(dx * dx + dy * dy); + if (dd > reach) continue; + const z = z0 + (z1 - z0) * u, p = py * W + px; + if (dd < RAD + 0.5) { + if (z >= zb[p]) continue; + const cov = min(1, RAD + 0.5 - dd), lit = (0.42 + 0.58 * cl((DEEP - z) / (2 * DEEP), 0, 1)) * (1 - 0.35 * (dd / (RAD + 0.5)) ** 2); + for (let j = 0; j < 3; j++) rgb[3 * p + j] = rgb[3 * p + j] * (1 - cov) + col[j] * lit * cov; + if (cov > 0.4) zb[p] = z; + } else if (z + 1.5 < zb[p]) { + const k = 0.2 + 0.8 * min(1, (dd - RAD - 0.5) / HALO) ** 2; + for (let j = 0; j < 3; j++) rgb[3 * p + j] *= k; + zb[p] = z + 1.5; + } + } + } + + // Cables: 1 LED lines, brighter the harder they pull, hidden behind struts. + // A plucked one swings as a string for two seconds, at a pitch that goes + // with the square root of its tension over its length, its envelope dim. + const nc = m.CL.length, pc = hsv(0.92 + S.hue, 0.4, 1), fl = hsv(0.13 + S.hue, 0.3, 1); + let tmax = 1, tsum = 0, lsum = 0; + for (let q = 0; q < nc; q++) (tmax = max(tmax, m.ten[q])), (tsum += m.ten[q]), (lsum += m.CL[q]); + const age = S.t - S.plT; + for (let q = 0; q < nc; q++) { + const a = m.cables[2 * q], b = m.cables[2 * q + 1]; + const lv = 0.26 + 0.34 * (m.ten[q] / tmax); + if (q !== S.pl || age > 2) { + cable(rgb, zb, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], 0, pc, lv); + continue; + } + const pk = 1 - age / 2, col = [0, 1, 2].map((j) => pc[j] + (fl[j] - pc[j]) * pk), v = lv + pk * (1 - lv); + const amp = 3.2 * pk * pk, w = 2 * PI * 2.2 * sqrt(m.ten[q] / (tsum / nc + 1e-9)) * (lsum / nc / m.CL[q]); + cable(rgb, zb, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], amp, col, v * 0.35); + cable(rgb, zb, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], -amp, col, v * 0.35); + cable(rgb, zb, sx[a], sy[a], sz[a], sx[b], sy[b], sz[b], amp * cos(w * age), col, v); + } + + for (let p = 0; p < W * H; p++) { + const r = rgb[3 * p], g = rgb[3 * p + 1], b = rgb[3 * p + 2]; + if (r + g + b > 0.004) display.setPixel(p % W, (p / W) | 0, min(255, 255 * r) | 0, min(255, 255 * g) | 0, min(255, 255 * b) | 0); + } +} From 1eef202f600994ca0f8e5c5258b8144273218743 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:10:20 +0000 Subject: [PATCH 05/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Sator=20Square=20=EB=A8=B8=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=80=20=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- incoming/pattern-study/patterns/sator-square.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/sator-square.js b/incoming/pattern-study/patterns/sator-square.js index 2cd02ca10..941ced6c9 100644 --- a/incoming/pattern-study/patterns/sator-square.js +++ b/incoming/pattern-study/patterns/sator-square.js @@ -5,7 +5,7 @@ // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: Sator square (Pompeii, before AD 79; Dura-Europos), Paternoster cross (Felix Grosser, 1926), the assignment problem solved by the Hungarian method (Harold Kuhn, 1955), pattern round 71 +// Lineage: Sator square (Pompeii, before AD 79; Dura-Europos), Paternoster cross (Felix Grosser, 1926), the assignment problem (Harold Kuhn, 1955), pattern round 71 // Round: 71 // =============================== // @matrix 64x128 @@ -14,7 +14,7 @@ // Knob 1: Hue. Turns the light round the wheel: at 0 the letters glow gold and the four letters of the cross's ends, A and O, glow pale azure on grey stone. // Knob 2: Speed. One round of reading, flight, cross and flight home takes 14 s at 2, 9.3 s at 3 and 140 s at 0.2. // Knob 3: Assign. Which letter flies to which place of its kind. 0 is the matching with the least total travel, 537.6 LEDs for the 25 flights; 1 sends the k-th A of the square, read across, to the k-th A of the cross, read down and then across, 864.9 LEDs; 2 is the matching with the most travel, 1083.3 LEDs, so the flights cross over each other. -// Knob 4: Weather. How long the tablet has stood out: at 0.06 the cuts are fresh; towards 1 the groove edges crumble, the stone pits and greys with lichen, and every letter carries its own worn patches on its flight. +// Knob 4: Weather. How long the tablet has stood out: at 0.06 the cuts are fresh; towards 1 the groove edges crumble and the stone pits and greys with lichen, and every letter keeps its own worn patches all through its flight. const W = 64, H = 128, NP = W * H; const { floor, ceil, round, min, max, exp, sqrt, sin } = Math; From e13b56cf78eebbea9b25d2a57b13ff47c76e7acb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:11:26 +0000 Subject: [PATCH 06/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Book=20from=20the=20Sky=20=EA=B3=BC=20Lorem?= =?UTF-8?q?=20Ipsum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../patterns/book-from-the-sky.js | 359 +++++++++++++++++ .../pattern-study/patterns/lorem-ipsum.js | 366 ++++++++++++++++++ 2 files changed, 725 insertions(+) create mode 100644 incoming/pattern-study/patterns/book-from-the-sky.js create mode 100644 incoming/pattern-study/patterns/lorem-ipsum.js diff --git a/incoming/pattern-study/patterns/book-from-the-sky.js b/incoming/pattern-study/patterns/book-from-the-sky.js new file mode 100644 index 000000000..92c51d3d7 --- /dev/null +++ b/incoming/pattern-study/patterns/book-from-the-sky.js @@ -0,0 +1,359 @@ +// ===== Patternflow pattern ===== +// Title: Book from the Sky +// Family: type +// Idea: A page of invented characters in vertical columns, each put together from stock radicals and parts in the frames real characters use, is printed over again a column at a time from right to left as a thin new sheet is drawn across it, so a page that looks like Chinese and cannot be read keeps renewing itself. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Book from the Sky (Xu Bing, 1987-1991), about 4,000 invented characters cut as pearwood movable type in a Song-style face; the page after Chinese thread-bound books (ruled columns read top to bottom, right to left, a wider top margin than bottom); pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Ink=0..1, Speed=0.2..3, Strokes=-1..1, Notes=0..1 +// +// Knob 1: Ink. Paper and ink: at 0 black ink on ivory paper; 0.25 indigo, as in the blue trial impressions of Qing printing; 0.5 vermilion, as in the red ones; 0.75 a rubbing, white characters on black; 1 gold on indigo paper. +// Knob 2: Speed. How fast the new sheet is drawn across and printed: at 2 a column every 2.4 s, a page in 13.4 s and a 2 s rest before the next sheet; ten times slower at 0.2, half again as fast at 3. +// Knob 3: Strokes. How many parts a character is built from. At 0 a quarter of the characters have a part split in two again, 7% are one part with a dot added, and a character has 7 pen lines on average (a line runs from pen down to pen up, so 口 is one); at -1 nothing is split, 23% are one part, 5.8 lines; at 1 85% are split, 10.9 lines. +// Knob 4: Notes. How much of the page is commentary set as two columns of small characters inside one column, four to a place, as Chinese books set their notes: about 7% of the 55 places at 0.06, none at 0, nearly all at 1. Turning Strokes or Notes sets the columns of the new sheet that are not printed yet again. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// ---- Components, as stroke lists. Each is "wh" (its own grid, w by h +// points) and strokes, each stroke a run of x,y digit pairs joined by +// straight lines; a pair given twice is a dot. Placed in a box, a component +// is stretched to it, so a 5 by 5 part fills a 5 by 9 half as a tall one. +const LEFT = { + // Radicals in their narrow left-hand forms, 3 by 9. + ren: "39 2003 1218", shui: "39 0011 0314 0826", shou: "39 0222 101807 0624", mu: "39 0222 1018 1306 2424", + kou: "39 0323260603", ri: "39 0121270701 0424", yan: "39 1010 0121 0323 0525280805", jin: "39 1001 1021 0323 0525 1318 0827", + wang: "39 0121 0424 1117 0827", tu: "39 0323 1117 0827", he: "39 2001 0222 1118 1306 2424", mu2: "39 0020280800 0323 0525", + fu: "39 0008 00202112232415", xin: "39 1018 0203 2121", shi: "39 0121 1104 0525280805", huo: "39 0203 2122 101508 1627", +}; +const TOP = { + // Heads, 9 by 3: grass, roof, bamboo, net, lid, person, mountain, cover. + cao: "93 0181 2022 6062", mian: "93 4040 02018182", zhu: "93 2002 1131 2222 7052 6181 7272", wang: "93 0080820200 3032 5052", + tou: "93 4041 0282", ren: "93 4002 4082", shan: "93 0002 4042 8082 0282", mi: "93 02018182", +}; +const BOT = { + // Feet, 9 by 3: heart, fire dots, legs, dish, one. + xin: "93 0101 21327281 4040 6070", huo: "93 0211 3132 5152 7182", er: "93 303102 50528281", min: "93 1070 1012 3032 5052 7072 0282", yi: "93 0181", +}; +const GEN = { + // Parts that go anywhere, small grids stretched to their box. + kou: "33 0020220200", ri: "35 0020240400 0222", mu: "37 0020260600 0222 0424", tian: "55 0040440400 2024 0242", + wang: "55 0040 0242 0444 2024", tu: "54 1131 0343 2023", gong: "53 1030 2022 0242", shi: "55 0242 2024", + mu4: "55 0141 2024 2204 2244", da: "55 0141 202204 2244", ren: "55 202104 2244", nv: "55 201244 312304 0242", + zi: "55 003021 212414 0242", you: "55 00403104 1144", li: "55 01414434 202204", cun: "55 0141 303424 1213", + ge: "55 0141 1044 3214 3030", jin: "55 4011 111304 1242 3234", qian: "55 1002 114132 2204 2244", pu: "55 1002 1141 3114 1244", + huo: "55 0112 4132 202204 2244", mi: "55 0011 4031 0242 2024 2204 2244", he: "55 4011 0242 2124 2204 2244", shan: "54 0103 2023 4143 0343", + bai: "46 2011 0131350501 0333", ji: "55 00404202044443", yi: "55 0040044443", ji2: "55 101304 1030334443", + shi2: "55 1030 0242 222414 1304 3344", yang: "57 1010 3030 0141 1333 0545 2126", bei: "57 0040440400 0242 1506 3546", ding: "54 0040 202313", + gong2: "47 0030320204343626", chong: "57 2025 0141434301 0646 4545", +}; +const RIGHT = { + // Right-hand parts that need the whole 5 by 9 half: short-tailed bird, head. + zhui: "59 2003 1218 3030 2141 2343 2545 2848 3138", ye: "59 0040 2011 1232361612 1434 1708 3748", +}; +const NARROW = { + // Narrow right-hand parts, 3 by 9: knife, ear, seal. + dao: "39 0105 202817", fu: "39 0008 00202112232415", jie: "39 0020202515 0008", +}; +// Enclosures drawn on the whole 9 by 9, with the box left inside them: +// mouth, broad, cliff, walk, borders, corpse. +const ENC = [ + ["0080880800", 2, 2, 5, 5], ["4040 1181 111608", 3, 3, 6, 6], ["1080 101608", 3, 2, 6, 7], + ["0011 032316273888", 3, 0, 6, 6], ["0800808877", 2, 2, 5, 7], ["10707212 101608", 3, 4, 6, 5], +]; + +function comp(s) { + const [wh, ...st] = s.trim().split(" "); + return { w: +wh[0], h: +wh[1], st: st.map((t) => Array.from(t, Number)) }; +} +const set = (o) => Object.keys(o).map((k) => Object.assign(comp(o[k]), { k })); +const SL = set(LEFT), ST = set(TOP), SB = set(BOT), SG = set(GEN), SR = set(RIGHT), SN = set(NARROW); +const SE = ENC.map(([s, x, y, w, h]) => Object.assign(comp("99 " + s), { box: [x, y, w, h], k: "e" + s.length })); +const one = (a) => a[floor(Math.random() * a.length)]; + +// Real characters that a frame and stock parts would make (䌑, 林, 字, 因, +// 叀), 78 of them, drawn again when they come up: at Strokes 0 that is 3.3% +// of the characters drawn. Only these are checked. The Kangxi dictionary +// (1716) holds 47,035 characters and none of it is here, so a page can +// still carry a real one, which Xu Bing made sure his 4,000 never were. +const REAL = new Set( + ("LR:ren+mu4 LR:ren+zi LR:ren+cun LR:ren+shi LR:ren+bai LR:ren+ding LR:ren+ge LR:shui+gong LR:shui+shi LR:shui+yang " + + "LR:shui+ding LR:shui+mu4 LR:shui+bai LR:shou+ding LR:shou+gong LR:shou+zhui LR:mu+cun LR:mu+tu LR:mu+mu4 LR:mu+zhui " + + "LR:kou+tu LR:kou+ding LR:kou+shi LR:kou+zhui LR:yan+shi LR:yan+yang LR:yan+ding LR:yan+zhui LR:yan+cun LR:jin+shi " + + "LR:jin+ding LR:jin+zhui LR:ri+wang LR:tu+zhui LR:xin+zhui LR:shi+ding LR:he+zhui LR:wang+li LR2:nv+zi LR2:mu4+mu4 " + + "LR2:nv+nv LR2:ren+ren TB:mian+zi TB:mian+nv TB:cao+tian TB:ren+wang TB2:kou+kou TB2:ri+ri TB2:tu+tu TB2:huo+huo " + + "TB2:shi+kou TBb:ri+yi TBb:tian+xin TBb:mu4+huo TBb:kou+er TB:mian+mu4 LRn:he+dao LRn:gong+fu LRn:you+fu " + + "LR:shui+ri LR:kou+kou LR:ren+li E0:ren E0:da E0:mu4 E0:kou E0:wang E0:zi " + + "E1:mu4 E1:tu E2:ding E2:li E3:da E3:cun E3:li E3:shi E4:ren E4:kou").split(" "), +); + +// Bresenham into a bitmap gw wide. +function line(g, gw, x0, y0, x1, y1) { + const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1; + let e = dx + dy; + for (;;) { + g[y0 * gw + x0] = 1; + if (x0 === x1 && y0 === y1) return; + const e2 = 2 * e; + if (e2 >= dy) (e += dy), (x0 += sx); + if (e2 <= dx) (e += dx), (y0 += sy); + } +} +// Stroke component c into the box; returns its stroke count. +function put(g, gw, c, bx, by, bw, bh) { + const sx = c.w > 1 ? (bw - 1) / (c.w - 1) : 0, sy = c.h > 1 ? (bh - 1) / (c.h - 1) : 0; + for (const s of c.st) { + let px = bx + round(s[0] * sx), py = by + round(s[1] * sy); + for (let i = 2; i < s.length; i += 2) { + const qx = bx + round(s[i] * sx), qy = by + round(s[i + 1] * sy); + line(g, gw, px, py, qx, qy); + (px = qx), (py = qy); + } + } + return c.st.length; +} + +// A part in a box: a stock part, or at the next level the box split in two. +// Small parts are not blown up past two points bigger than their grid. +function part(g, box, nest, keys, depth) { + const [x, y, w, h] = box; + if (depth < 2 && Math.random() < nest) { + keys.push("+"); + if (w >= 5 && (w >= h || h < 5)) { + const lw = floor((w - 1) / 2); + return part(g, [x, y, lw, h], nest * 0.6, keys, depth + 1) + part(g, [x + lw + 1, y, w - lw - 1, h], nest * 0.6, keys, depth + 1); + } + if (h >= 5) { + const th = floor((h - 1) / 2); + return part(g, [x, y, w, th], nest * 0.6, keys, depth + 1) + part(g, [x, y + th + 1, w, h - th - 1], nest * 0.6, keys, depth + 1); + } + } + const pool = w >= 5 && h >= 9 && Math.random() < 0.12 ? SR : SG; + let c = one(pool); + for (let i = 0; i < 6 && (c.w > w + 1 || c.h > h + 1); i++) c = one(pool); + keys.push(c.k); + const bw = c.w < 5 && w > c.w ? min(w, c.w + 2) : w, bh = c.h < 5 && h > c.h ? min(h, c.h + 2) : h; + return put(g, 9, c, x + floor((w - bw) / 2), y + floor((h - bh) / 2), bw, bh); +} + +// One invented character into the 9 by 9 bitmap g. u is Strokes moved to +// 0..1. Frames and their shares at u = 0.5: one part 7%, left radical and +// right part 36%, two halves 7%, left part and narrow right 6%, head and +// body 16%, two stacked 6%, body and foot 8%, enclosure 14%. +function makeChar(g, u) { + const nest = u < 0.5 ? 0.6 * u : 0.3 + 1.4 * (u - 0.5); + const single = u < 0.5 ? 0.22 - 0.3 * u : 0.07 - 0.1 * (u - 0.5); + for (let tries = 0; ; tries++) { + g.fill(0); + const keys = []; + let r = Math.random(), n = 0, f; + const sh = (1 - single) / 0.93; + if (r < single) { + // Every stock part alone is a real character (朚, 力, ç±³), so a single + // part gets one dot more where there is room for it. + (f = "S"), (n = part(g, [0, 0, 9, 9], 0, keys, 2) + 1); + for (const i of [10, 16, 64, 70, 4, 36, 44].sort(() => Math.random() - 0.5)) { + let free = true; + for (let dy = -9; dy <= 9; dy += 9) for (let dx = -1; dx <= 1; dx++) if (g[i + dy + dx]) free = false; + if (free) { + g[i] = 1; + break; + } + } + } else if ((r = (r - single) / sh) < 0.36) { + const c = one(SL); + (f = "LR"), keys.push(c.k), (n = put(g, 9, c, 0, 0, 3, 9) + part(g, [4, 0, 5, 9], nest, keys, 0)); + } else if (r < 0.43) (f = "LR2"), (n = part(g, [0, 0, 4, 9], 0, keys, 2) + part(g, [5, 0, 4, 9], 0, keys, 2)); + else if (r < 0.49) { + n = part(g, [0, 0, 5, 9], nest, keys, 0); + const c = one(SN); + (f = "LRn"), keys.push(c.k), (n += put(g, 9, c, 6, 0, 3, 9)); + } else if (r < 0.65) { + const c = one(ST); + (f = "TB"), keys.push(c.k), (n = put(g, 9, c, 0, 0, 9, 3) + part(g, [0, 4, 9, 5], nest, keys, 0)); + } else if (r < 0.71) (f = "TB2"), (n = part(g, [0, 0, 9, 4], 0, keys, 2) + part(g, [0, 5, 9, 4], 0, keys, 2)); + else if (r < 0.79) { + n = part(g, [0, 0, 9, 5], nest, keys, 0); + const c = one(SB); + (f = "TBb"), keys.push(c.k), (n += put(g, 9, c, 0, 6, 9, 3)); + } else { + const i = floor(Math.random() * SE.length), c = SE[i]; + (f = "E" + i), (n = put(g, 9, c, 0, 0, 9, 9) + part(g, c.box, nest, keys, 0)); + } + if (tries < 8 && REAL.has(f + ":" + keys.join("+"))) continue; + return n; + } +} + +// ---- The page. A frame from x 1 to 62 and y 10 to 122, five columns 11 +// wide with a rule between each, eleven characters a column on a 10-row +// pitch: 55 places. The top margin is 10 rows and the bottom 5, the wider +// one above as in Chinese books. Columns read right to left, so the +// printing strips run from x 64 down to 0, split at the rules. +const TOPY = 10, BOTY = 122, RULES = [13, 25, 37, 49], EDGES = [64, 49, 37, 25, 13, 0]; +const STRIP = new Uint8Array(W); +for (let x = 0; x < W; x++) STRIP[x] = x >= 49 ? 0 : x >= 37 ? 1 : x >= 25 ? 2 : x >= 13 ? 3 : 4; + +function makePage(u, notes) { + const ink = new Uint8Array(N), halo = new Uint8Array(N); + for (let x = 1; x <= 62; x++) ink[TOPY * W + x] = ink[BOTY * W + x] = 1; + for (let y = TOPY; y <= BOTY; y++) { + ink[y * W + 1] = ink[y * W + 62] = 1; + for (const rx of RULES) ink[y * W + rx] = 1; + } + const g = new Uint8Array(81), sg = new Uint8Array(16); + let strokes = 0, chars = 0, note = false; + const start = 0.6 * notes, keep = 0.5 + 0.5 * notes; + for (let j = 0; j < 5; j++) + for (let k = 0; k < 11; k++) { + const x0 = 3 + 12 * j, y0 = 12 + 10 * k; + note = Math.random() < (note ? keep : start); + if (note) { + // Four small characters, two side by side, two down. + for (let q = 0; q < 4; q++) { + sg.fill(0); + let c = one(SG); + while (c.w > 5 || c.h > 5) c = one(SG); + put(sg, 4, c, 0, 0, 4, 4); + const ox = x0 + (q & 1 ? 0 : 5), oy = y0 + (q >> 1) * 5; + for (let i = 0; i < 16; i++) if (sg[i]) ink[(oy + (i >> 2)) * W + ox + (i & 3)] = 1; + } + continue; + } + strokes += makeChar(g, u); + chars++; + // Song faces cut horizontals thin and verticals thick. An LED cannot + // be thinner than one, so a pixel that only runs sideways is inked + // lighter (2) and the uprights stay full (1). + for (let i = 0; i < 81; i++) { + if (!g[i]) continue; + const gx = i % 9, gy = floor(i / 9); + const side = (gx > 0 && g[i - 1]) || (gx < 8 && g[i + 1]), up = (gy > 0 && g[i - 9]) || (gy < 8 && g[i + 9]); + ink[(y0 + gy) * W + x0 + gx] = side && !up ? 2 : 1; + } + } + for (let y = 1; y < H - 1; y++) + for (let x = 1; x < W - 1; x++) { + const p = y * W + x; + if (!ink[p] && (ink[p - 1] || ink[p + 1] || ink[p - W] || ink[p + W])) halo[p] = 1; + } + return { ink, halo, t0: new Float32Array(5).fill(1e9), strokes, chars }; +} + +// Paper grain for two sheets, alternating, so each sheet keeps its own. +const GRAIN = [new Float32Array(N), new Float32Array(N)]; +for (let s = 0; s < 2; s++) + for (let p = 0; p < N; p++) { + let h = Math.imul(p + 7919 * (s + 1), 2654435761) >>> 0; + h = Math.imul(h ^ (h >>> 15), 2246822519) >>> 0; + GRAIN[s][p] = ((h ^ (h >>> 13)) >>> 0) / 4294967296 - 0.5; + } + +// Paper and ink along Ink: black on ivory, indigo, vermilion, a rubbing +// (the paper inked black, the cut strokes left white), gold on indigo. +const PAL = [ + [[238, 229, 205], [24, 22, 24]], + [[238, 229, 205], [30, 52, 128]], + [[238, 229, 205], [192, 48, 26]], + [[26, 26, 30], [228, 222, 206]], + [[26, 36, 96], [224, 182, 92]], +]; +// Each holds for 0.175 either side of its stop, so the knob rests on whole +// palettes and only passes through the grey between dark and light paper. +function palette(k) { + const f = cl(k, 0, 1) * 4, i = min(3, floor(f)), a = PAL[i], b = PAL[i + 1]; + let t = cl((f - i - 0.35) / 0.3, 0, 1); + t = t * t * (3 - 2 * t); + return [0, 1].map((j) => a[j].map((v, c) => v + (b[j][c] - v) * t)); +} + +// Timing. The new sheet's edge moves 2.5 × Speed LEDs a second; a strip +// is printed 0.4/Speed s after the sheet has covered it, the impression +// running down the strip in 2/Speed s (0.2 s and 1 s at Speed 2). Fresh +// ink is 10% further from the paper than dry ink and darkens the paper +// beside it by 12% while wet; both dry off in about a second. The page +// underneath shows through the sheet at 13%, and horizontals are inked +// at 72%. +const FRESH = 0.1, WET = 0.12, DRY = 0.9, SPREAD = 0.4, GHOST = 0.13, THIN = 0.72; + +export function setup(params) { + params.s = { t: 0, ready: false, e: 67, rest: 0, sheet: 0, pages: null, u: 0.5, notes: 0.06, speed: 2, ink: 0, key: "" }; +} + +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.ink = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.u = (cl(k[2], -1, 1) + 1) / 2; + S.notes = cl(k[3], 0, 1); + if (!S.ready) { + // The page is full from the first frame: the sheet on top printed + // long ago, one under it showing through, and the next one waiting. + const older = makePage(S.u, S.notes), old = makePage(S.u, S.notes); + older.t0.fill(-1e3), old.t0.fill(-1e3); + S.pages = [older, old, makePage(S.u, S.notes)]; + S.ready = true; + S.key = S.u.toFixed(2) + S.notes.toFixed(2); + } + const key = S.u.toFixed(2) + S.notes.toFixed(2); + if (key !== S.key) { + // Strokes or Notes turned: the columns of the new sheet not printed yet + // are set again, so the change shows from the next column on. + S.key = key; + const nw = S.pages[2], re = makePage(S.u, S.notes); + for (let s = 0; s < 5; s++) + if (nw.t0[s] > 1e8) + for (let y = 0; y < H; y++) + for (let x = EDGES[s + 1]; x < EDGES[s]; x++) (nw.ink[y * W + x] = re.ink[y * W + x]), (nw.halo[y * W + x] = re.halo[y * W + x]); + } + S.t += d; + const [, , nw] = S.pages; + if (S.e > 0) { + // A page turn is a wipe 67 LEDs long, 13.4 s at Speed 2, longer than + // the six seconds the map measures over; motion comes from a column + // changing at a time, never the page at once. + S.e = max(0, S.e - d * 2.5 * S.speed); + for (let s = 0; s < 5; s++) if (nw.t0[s] > 1e8 && S.e <= EDGES[s + 1]) nw.t0[s] = S.t + 0.4 / S.speed; + if (S.e === 0) S.rest = 4 / S.speed; + } else if ((S.rest -= d) <= 0) { + S.pages = [S.pages[1], nw, makePage(S.u, S.notes)]; + S.sheet ^= 1; + S.e = 67; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + const [older, old, nw] = S.pages, t = S.t, pass = 2 / S.speed; + const [paper, ink] = palette(S.ink); + const dr = ink[0] - paper[0], dg = ink[1] - paper[1], db = ink[2] - paper[2]; + const ga = GRAIN[S.sheet], gb = GRAIN[S.sheet ^ 1]; + const e = S.e, shadowX = e < 64 ? Math.ceil(e) - 1 : -1; + for (let y = 0; y < H; y++) { + const rowF = cl((y - TOPY) / (BOTY - TOPY), 0, 1) * pass; + for (let x = 0; x < W; x++) { + const p = y * W + x, top = x >= e; + const pg = top ? nw : old, under = top ? old : older; + const gr = top ? gb[p] : ga[p]; + // Paper, its grain, and the page under it. + let a = 0.035 * gr; + if (under.ink[p]) a += GHOST; + const ti = pg.t0[STRIP[x]] + rowF; + if (t >= ti) { + const age = t - ti; + if (pg.ink[p]) a = (pg.ink[p] === 2 ? THIN : 1) + FRESH * exp(-age / DRY); + else if (pg.halo[p]) a += WET * exp(-age / SPREAD); + } + const sh = x === shadowX ? 0.97 : 1; + const r = (paper[0] + dr * a) * sh, g = (paper[1] + dg * a) * sh, b = (paper[2] + db * a) * sh; + display.setPixel(x, y, cl(r, 0, 255) | 0, cl(g, 0, 255) | 0, cl(b, 0, 255) | 0); + } + } +} diff --git a/incoming/pattern-study/patterns/lorem-ipsum.js b/incoming/pattern-study/patterns/lorem-ipsum.js new file mode 100644 index 000000000..e83d7e9ab --- /dev/null +++ b/incoming/pattern-study/patterns/lorem-ipsum.js @@ -0,0 +1,366 @@ +// ===== Patternflow pattern ===== +// Title: Lorem Ipsum +// Family: type +// Idea: Cicero's sentence from De finibus is set as a justified block of small type and turned letter by letter into the placeholder it became: an alignment computed at the start says which letters stay, the cut ones drop out of their lines and pile up at the foot of the panel, the kept ones slide into the new justified block, and the missing ones leap back out of the heap or fall in from above. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: lorem ipsum, from Cicero, De finibus bonorum et malorum 1.32-33 (45 BC), the source traced by Richard McClintock (Hampden-Sydney College); the alignment is a longest common subsequence by dynamic programming, the table of Robert Wagner and Michael Fischer's string-to-string correction (1974); pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Passage=0..3, Gravity=-1..1 +// +// Knob 1: Hue. Turns the colours round the wheel together: at 0 the type is cream, letters on their way out turn slate blue and dim in the heap, and letters coming in are mint until they settle. +// Knob 2: Speed. The clock of a turn. At 2 a turn takes 8.1 s: the cut letters fall from 0.35 s to about 2.7 s, the kept ones slide from 1.2 s to 2.6 s, the missing ones arrive from 2.4 s to about 4 s, and the new block holds 3.5 s before it turns back. The first turn starts 0.8 s in. Ten times slower at 0.2, half again as fast at 3. +// Knob 3: Passage. Which sentence and the placeholder made from it: 0 Neque porro quisquam (1.32) and Lorem ipsum, 1 Ut enim ad minima (1.32) and Ut enim ad minim, 2 Quis autem vel eum (1.32) and Duis aute irure, 3 excepturi sint occaecati (1.33) and Excepteur sint occaecat. Turning it sends the block to the new sentence at the next turn. +// Knob 4: Gravity. 240 LEDs/s² at 0, a third of that at -1, three times it at 1. A letter cut from the top line reaches the heap in about 0.9 s at 0, 1.6 s at -1 and 0.55 s at 1, and a letter leaping back out of the heap rises on the same gravity. + +const W = 64, H = 128; +const { floor, round, min, max, sqrt, abs } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const hsv = (h, s, v) => [5, 3, 1].map((n) => ((k) => 255 * v * (1 - s * max(0, min(k, 4 - k, 1))))((n + (h - floor(h)) * 6) % 6)); +const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t); + +// ---- The type: 6 rows, the top one for ascenders, four for the x-height +// and one for descenders; capitals stand on the top five. Rows from the top, comma between +// rows, a missing row blank. Only the letters the eight passages use. +const FONT = { + a: ",.##,#.#,#.#,.##", b: "#..,##.,#.#,#.#,##.", c: ",.##,#..,#..,.##", d: "..#,.##,#.#,#.#,.##", e: ",.#.,###,#..,.##", + f: ".##,.#.,###,.#.,.#.", g: ",.##,#.#,#.#,.##,##.", h: "#..,##.,#.#,#.#,#.#", i: "#,,#,#,#", l: "#,#,#,#,#", + m: ",####.,#.#.#,#.#.#,#.#.#", n: ",##.,#.#,#.#,#.#", o: ",.#.,#.#,#.#,.#.", p: ",##.,#.#,#.#,##.,#..", q: ",.##,#.#,#.#,.##,..#", + r: ",#.#,##.,#..,#..", s: ",.##,#..,..#,##.", t: ".#.,###,.#.,.#.,..#", u: ",#.#,#.#,#.#,.##", v: ",#.#,#.#,#.#,.#.", + x: ",#.#,.#.,.#.,#.#", N: "#..#,##.#,#.##,#..#,#..#", L: "#..,#..,#..,#..,###", U: "#.#,#.#,#.#,#.#,###", + Q: ".##.,#..#,#..#,#..#,.##.,...#", D: "##.,#.#,#.#,#.#,##.", E: "###,#..,##.,#..,###", ",": ",,,,#,#", ".": ",,,,#", "?": "##.,..#,.#.,,.#.", +}; +// Each glyph in four turns (0 upright, 1 a quarter turn clockwise, 2 a +// half, 3 a quarter back), with each column's top and bottom ink row, so a +// letter lying on its side in the heap rests on its own strokes. +const GL = {}; +for (const ch in FONT) { + const rows = FONT[ch].split(","), w = max(...rows.map((r) => r.length)); + const b0 = new Uint8Array(w * 6); + rows.forEach((r, y) => [...r].forEach((v, x) => (b0[y * w + x] = v === "#"))); + GL[ch] = [0, 1, 2, 3].map((t) => { + const tw = t & 1 ? 6 : w, th = t & 1 ? w : 6, b = new Uint8Array(tw * th); + for (let y = 0; y < 6; y++) + for (let x = 0; x < w; x++) { + if (!b0[y * w + x]) continue; + const [nx, ny] = t === 0 ? [x, y] : t === 1 ? [5 - y, x] : t === 2 ? [w - 1 - x, 5 - y] : [y, w - 1 - x]; + b[ny * tw + nx] = 1; + } + const top = new Int8Array(tw).fill(-1), bot = new Int8Array(tw).fill(-1); + for (let x = 0; x < tw; x++) + for (let y = 0; y < th; y++) + if (b[y * tw + x]) { + if (top[x] < 0) top[x] = y; + bot[x] = y; + } + return { w: tw, h: th, b, top, bot }; + }); +} + +// ---- The passages: Cicero's sentence, and the placeholder as it is set +// today. The fourth starts in the middle of Cicero's sentence. +const PAIRS = [ + [ + "Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ], + [ + "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + ], + [ + "Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?", + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + ], + [ + "excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.", + "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + ], +]; + +// The alignment: a longest common subsequence of the two strings, case +// ignored, by the usual table filled from the ends (196 by 123 cells for +// the first pair). Reading the table back gives "=" for a kept character, +// "-" for one cut from a and "+" for one added from b; on a tie it cuts +// before it adds, so a run of cuts stays together. +function align(a, b) { + const n = a.length, m = b.length, L = new Int32Array((n + 1) * (m + 1)), M = m + 1; + const eq = (i, j) => a[i].toLowerCase() === b[j].toLowerCase(); + for (let i = n - 1; i >= 0; i--) + for (let j = m - 1; j >= 0; j--) L[i * M + j] = eq(i, j) ? L[(i + 1) * M + j + 1] + 1 : max(L[(i + 1) * M + j], L[i * M + j + 1]); + const ops = []; + let i = 0, j = 0; + while (i < n && j < m) { + if (eq(i, j) && L[i * M + j] === L[(i + 1) * M + j + 1] + 1) ops.push("="), i++, j++; + else if (L[(i + 1) * M + j] >= L[i * M + j + 1]) ops.push("-"), i++; + else ops.push("+"), j++; + } + while (i++ < n) ops.push("-"); + while (j++ < m) ops.push("+"); + return ops; +} + +// ---- Setting. Words are filled greedily into a 62 LED measure from x 1, +// one LED between letters and three between words; every line but the +// last is justified. The extra goes to the word spaces first, up to 4 more +// each, then one LED at a time into the letter spaces from the left. A +// line that would need more than that is left short, as a compositor would +// have hyphenated it. Lines are 7 rows apart from y 2. Returns each +// character's place, null for a space. +const MEAS = 62, X0 = 1, Y0 = 2, LP = 7; +function layout(str) { + const words = []; + let at = 0; + for (const w of str.split(" ")) words.push({ w, at }), (at += w.length + 1); + const wl = (w) => [...w].reduce((s, c) => s + GL[c][0].w + 1, -1); + const lines = []; + let cur = [], len = 0; + for (const wd of words) { + const add = wl(wd.w) + (cur.length ? 3 : 0); + if (cur.length && len + add > MEAS) lines.push(cur), (cur = []), (len = 0); + len += wl(wd.w) + (cur.length ? 3 : 0); + cur.push(wd); + } + lines.push(cur); + const pos = new Array(str.length).fill(null); + lines.forEach((ln, li) => { + const nat = ln.reduce((s, wd) => s + wl(wd.w), 0) + 3 * (ln.length - 1); + let extra = li < lines.length - 1 ? MEAS - nat : 0; + const ws = new Array(ln.length).fill(0), lg = ln.map((wd) => new Array(wd.w.length).fill(0)); + const nWs = ln.length - 1, nLg = ln.reduce((s, wd) => s + wd.w.length - 1, 0); + if (nWs) for (let k = 0; k < nWs && extra > 0; k++) { + const give = min(4, floor(extra / (nWs - k))); + (ws[k] = give), (extra -= give); + } + for (let q = 0; q < ln.length && extra > 0; q++) for (let c = 0; c < ln[q].w.length - 1 && extra > 0; c++) lg[q][c]++, extra--; + let x = X0; + ln.forEach((wd, q) => { + for (let c = 0; c < wd.w.length; c++) { + pos[wd.at + c] = { x, y: Y0 + li * LP, line: li }; + x += GL[wd.w[c]][0].w + 1 + lg[q][c]; + } + x += 2 + ws[q]; + }); + }); + return pos; +} + +// ---- Letters are bodies. Modes: +const LINE = 0, LOOSE = 1, FALL = 2, HEAP = 3, SLIDE = 4, DROP = 5, JUMP = 6, WAIT = 7; +// Times in turn seconds (real seconds at Speed 2) after a turn starts. +const CUE = 0.35, CUT = 1.4, SLIDE_AT = 1.2, SLIDE_T = 1.0, IN_AT = 2.4, IN_SPREAD = 0.8, DONE = 4.6, HOLD = 3.5; +// A heap letter fades after this long unless a later turn takes it back. +const LIFE = 28; + +export function setup(params) { + params.s = { t: 0, ready: false, pair: 0, side: 0, text: "", line: [], bodies: [], next: 0, hue: 0, speed: 2, g: 240, turns: 0, stats: null }; +} + +function body(ch, x, y, mode) { + return { ch, rot: 0, x, y, vx: 0, vy: 0, mode, t0: 0, t1: 0, x0: x, y0: y, x1: x, y1: y, to: ch, born: 0, landed: -9, inAt: -9, fresh: false, spin: 0 }; +} + +function setText(S, str) { + const pos = layout(str); + S.text = str; + S.line = pos.map((p, i) => (p ? body(str[i], p.x, p.y, LINE) : null)); + S.bodies = S.line.filter(Boolean); +} + +// Start a turn from the text on the panel to str. +function turn(S, str) { + const a = S.text, ops = align(a, str), pa = layout(a), pb = layout(str), t = S.t; + const nl = new Array(str.length).fill(null); + let nCut = 0, nIn = 0; + for (let k = 0, i = 0, j = 0; k < ops.length; k++) { + if (ops[k] === "-" && pa[i]) nCut++; + if (ops[k] === "+" && pb[j]) nIn++; + if (ops[k] !== "+") i++; + if (ops[k] !== "-") j++; + } + let cut = 0, inn = 0, kept = 0; + for (let k = 0, i = 0, j = 0; k < ops.length; k++) { + const o = ops[k]; + if (o === "=") { + const b = S.line[i]; + if (b && pb[j]) { + Object.assign(b, { mode: SLIDE, t0: t + SLIDE_AT + 0.03 * pa[i].line, x0: b.x, y0: b.y, x1: pb[j].x, y1: pb[j].y, to: str[j] }); + b.t1 = b.t0 + SLIDE_T; + nl[j] = b; + kept++; + } + i++, j++; + } else if (o === "-") { + const b = S.line[i]; + // Cut letters go in reading order, the first CUE s before it falls + // turning towards the colour of the cut. + if (b) Object.assign(b, { mode: LOOSE, t0: t + CUE + (CUT * cut++) / max(1, nCut - 1) }); + i++; + } else { + if (pb[j]) { + const b = body(str[j], pb[j].x, -8, WAIT); + Object.assign(b, { t0: t + IN_AT + (IN_SPREAD * inn++) / max(1, nIn - 1), x1: pb[j].x, y1: pb[j].y }); + nl[j] = b; + S.bodies.push(b); + } + j++; + } + } + S.text = str; + S.line = nl; + S.next = t + DONE + HOLD; + S.stats = { kept, cut: nCut, ins: nIn }; +} + +// Where a sprite at column x would come to rest on the surface. +function restY(surf, sp, x) { + let y = 1e9; + for (let c = 0; c < sp.w; c++) if (sp.bot[c] >= 0) y = min(y, surf[x + c] - 1 - sp.bot[c]); + return y; +} + +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.g = 240 * Math.pow(3, cl(k[3], -1, 1)); + const pair = cl(round(k[2]), 0, 3); + const sd = (d * S.speed) / 2; + if (!S.ready) { + S.pair = pair; + setText(S, PAIRS[pair][0]); + S.next = 0.8; + S.ready = true; + } + S.t += sd; + const t = S.t, g = S.g; + if (t >= S.next) { + // Turn to the other side of the pair, or to Cicero's side of a new one. + if (pair !== S.pair) (S.pair = pair), (S.side = 0); + else S.side ^= 1; + turn(S, PAIRS[S.pair][S.side]); + S.turns++; + } + + // Timed modes. + const heap = S.bodies.filter((b) => b.mode === HEAP); + for (const b of S.bodies) { + if (b.mode === LOOSE && t >= b.t0) { + // A cut letter tips onto its side a third of the time each way. + const r = Math.random(); + Object.assign(b, { mode: FALL, vy: 0, vx: 0, y0: b.y, spin: r < 0.25 ? 1 : r < 0.5 ? 3 : 0, born: t }); + } else if (b.mode === SLIDE) { + const u = cl((t - b.t0) / (b.t1 - b.t0), 0, 1), e = u * u * (3 - 2 * u); + b.x = b.x0 + (b.x1 - b.x0) * e; + b.y = b.y0 + (b.y1 - b.y0) * e; + if (u > 0.5) b.ch = b.to; + if (u >= 1) b.mode = LINE; + } else if (b.mode === WAIT && t >= b.t0) { + // A letter that is needed comes back out of the heap if one like it + // lies there, the highest first; otherwise it falls in from above. + let best = null; + for (const h of heap) if (h.mode === HEAP && h.ch.toLowerCase() === b.ch.toLowerCase() && (!best || h.y < best.y)) best = h; + b.inAt = t; + b.fresh = true; + if (best) { + best.mode = -1; + const dy = max(4, best.y - b.y1), T = sqrt((2 * dy) / g); + Object.assign(b, { mode: JUMP, x0: best.x, y0: best.y, x: best.x, y: best.y, rot: best.rot, t0: t, t1: t + T, vy: -g * T }); + } else Object.assign(b, { mode: DROP, x: b.x1, y: -8 - 10 * Math.random(), vy: 0 }); + } else if (b.mode === DROP) { + b.vy += g * sd; + b.y += b.vy * sd; + if (b.y >= b.y1) Object.assign(b, { y: b.y1, mode: LINE, landed: t }); + } else if (b.mode === JUMP) { + const s = min(t - b.t0, b.t1 - b.t0), T = b.t1 - b.t0; + b.x = b.x0 + ((b.x1 - b.x0) * s) / T; + b.y = b.y0 + b.vy * s + 0.5 * g * s * s; + if (s > T * 0.4) b.rot = 0; + if (t >= b.t1) Object.assign(b, { x: b.x1, y: b.y1, mode: LINE, landed: t, rot: 0 }); + } + } + S.bodies = S.bodies.filter((b) => b.mode !== -1 && !(b.mode === HEAP && t - b.landed > LIFE + 2)); + + // The heap. Resting letters are stacked from the bottom up on a surface + // of the highest ink in each column; one whose support has gone falls + // again. A falling letter lands on the surface, unless the column beside + // it lets it rest two rows lower, when it slides that way first. + const surf = new Float32Array(W).fill(H); + const rest = S.bodies.filter((b) => b.mode === HEAP).sort((p, q) => q.y - p.y); + for (const b of rest) { + const sp = GL[b.ch][b.rot], ry = restY(surf, sp, b.x); + if (b.y < ry - 0.5) { + b.mode = FALL; + b.vy = 0; + continue; + } + b.y = ry; + for (let c = 0; c < sp.w; c++) if (sp.top[c] >= 0) surf[b.x + c] = min(surf[b.x + c], ry + sp.top[c]); + } + const falling = S.bodies.filter((b) => b.mode === FALL).sort((p, q) => q.y - p.y); + for (const b of falling) { + if (b.spin && b.y - b.y0 > 4) (b.rot = b.spin), (b.spin = 0), (b.x = cl(round(b.x) - 1, 0, W - 6)); + const sp = GL[b.ch][b.rot]; + b.x = cl(round(b.x), 0, W - sp.w); + b.vy += g * sd; + b.y += b.vy * sd; + let ry = restY(surf, sp, b.x); + if (b.y < ry) continue; + // Look one and two columns either way; step towards the lowest rest + // if it is 2 rows lower at one column or 3 at two. + let go = 0, low = ry; + for (const s of [-1, 1, -2, 2]) { + const nx = b.x + s; + if (nx < 0 || nx > W - sp.w) continue; + const v = restY(surf, sp, nx); + if (v >= ry + 1 + abs(s) && v > low) (low = v), (go = s > 0 ? 1 : -1); + } + if (go) { + b.x += go; + b.y = ry; + b.vy *= 0.5; + continue; + } + Object.assign(b, { y: ry, vy: 0, mode: HEAP, landed: t }); + for (let c = 0; c < sp.w; c++) if (sp.top[c] >= 0) surf[b.x + c] = min(surf[b.x + c], ry + sp.top[c]); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || !S.ready) return; + const t = S.t, h = S.hue; + const cream = hsv(0.11 + h, 0.4, 0.95), slate = hsv(0.6 + h, 0.42, 0.8), mint = hsv(0.45 + h, 0.55, 1); + const buf = new Float32Array(W * H * 3); + const order = [LINE, SLIDE, LOOSE, HEAP, FALL, WAIT, DROP, JUMP]; + for (const m of order) + for (const b of S.bodies) { + if (b.mode !== m || b.mode === WAIT) continue; + let c = cream, a = 1; + if (m === LOOSE) c = mix(cream, slate, cl(1 - (b.t0 - t) / CUE, 0, 1)); + else if (m === FALL) c = slate; + else if (m === HEAP) { + const age = t - b.landed; + a = 1 - 0.45 * cl(age / 2, 0, 1); + if (age > LIFE) a *= cl(1 - (age - LIFE) / 2, 0, 1); + c = slate; + } else if (m === DROP || m === JUMP) c = mint; + else if (b.fresh && m === LINE) c = mix(mint, cream, cl((t - b.landed) / 1.5, 0, 1)); + const sp = GL[b.ch][b.rot], bx = round(b.x), by = round(b.y); + for (let y = 0; y < sp.h; y++) { + const py = by + y; + if (py < 0 || py >= H) continue; + for (let x = 0; x < sp.w; x++) { + const px = bx + x; + if (px < 0 || px >= W || !sp.b[y * sp.w + x]) continue; + const p = (py * W + px) * 3; + buf[p] = c[0] * a, buf[p + 1] = c[1] * a, buf[p + 2] = c[2] * a; + } + } + } + for (let p = 0; p < W * H; p++) display.setPixel(p % W, floor(p / W), buf[p * 3] | 0, buf[p * 3 + 1] | 0, buf[p * 3 + 2] | 0); +} From 0fbfd023143588414b7cf2598e191d2d39ec955a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:13:57 +0000 Subject: [PATCH 07/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Catoptric=20Anamorphosis=20=EA=B3=BC=20Platyc?= =?UTF-8?q?osms?= 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_01Gdqi6Ru5nYSNEX485wYhh1 --- .../patterns/catoptric-anamorphosis.js | 363 ++++++++++++++++++ incoming/pattern-study/patterns/platycosms.js | 305 +++++++++++++++ 2 files changed, 668 insertions(+) create mode 100644 incoming/pattern-study/patterns/catoptric-anamorphosis.js create mode 100644 incoming/pattern-study/patterns/platycosms.js diff --git a/incoming/pattern-study/patterns/catoptric-anamorphosis.js b/incoming/pattern-study/patterns/catoptric-anamorphosis.js new file mode 100644 index 000000000..ab48094c2 --- /dev/null +++ b/incoming/pattern-study/patterns/catoptric-anamorphosis.js @@ -0,0 +1,363 @@ +// ===== Patternflow pattern ===== +// Title: Catoptric Anamorphosis +// Family: type +// Idea: A word is drawn on the floor round a mirror cylinder, warped into rings that wrap the cylinder's foot, and only in the cylinder's reflection, seen from the one eye the drawing was traced for, does it stand up straight; the camera sways round that eye, so the word reads and smears while the rings on the floor stay strange. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: catoptric (cylinder mirror) anamorphosis (Jean-François Niceron, La Perspective curieuse, 1638), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..3, Away=0..1 +// +// Knob 1: Hue. Turns the ink round the wheel, each letter a sixth of the wheel from the next: at 0 EYE is azure, violet and pink on grey paper. +// Knob 2: Speed. The camera's sway and the turn of the words: at 2 the sway takes 14 s and a new word is drawn every 7 s; ten times slower at 0.2, half as fast again at 3. +// Knob 3: Word. The first word, then the others in turn: EYE, 눈 (eye), SEE, YOU. +// Knob 4: Away. How far the camera wanders from the eye the floor was drawn for, on a figure of eight round the cylinder up to 60 degrees each way and up and down by 14 degrees. At the opening 0.06 it stays within 3.6 degrees and the word stands straight; at 0.3 it swings 18 degrees and the word slides round the mirror and leans; from 0.5 the word runs off the mirror's edge, while the rings on the floor only turn. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, sin, cos, atan2, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +// ---- The scene, in units of the cylinder's radius: the mirror stands on +// the floor y = 0 round the axis x = z = 0, 3 high. The design eye E looks +// at a point on the floor 1.6 in front of the axis, with a focal length of +// 160 LEDs, so the cylinder fills rows 5 to 56, 36 LEDs across at its top +// and 30 at its foot. The first frame traces the paper's grid (about 25 ms +// in Node) and each word's first frame its ink (25 to 30 ms); after that a +// frame is one ray per LED, off the mirror at most once, about 1.5 ms. +const RC = 1, HC = 3; +const E = [0, 6.2, 7.6], AIM = [0, 0, 1.6]; +const F = 160; + +// ---- Letters as strokes in a unit box, y down: L a line, A an elliptic +// arc (centre, radii, start and end angle in degrees, clockwise on screen). +const GLYPHS = { + E: [["L", 0, 0, 0, 1], ["L", 0, 0, 1, 0], ["L", 0, 0.5, 0.82, 0.5], ["L", 0, 1, 1, 1]], + Y: [["L", 0, 0, 0.5, 0.5], ["L", 1, 0, 0.5, 0.5], ["L", 0.5, 0.5, 0.5, 1]], + S: [["A", 0.5, 0.26, 0.5, 0.26, -25, -270], ["A", 0.5, 0.74, 0.5, 0.26, -90, 155]], + O: [["A", 0.5, 0.5, 0.5, 0.5, 0, 360]], + U: [["L", 0, 0, 0, 0.5], ["A", 0.5, 0.5, 0.5, 0.5, 180, 0], ["L", 1, 0.5, 1, 0]], + // 눈: ㄎ over ㅜ over ㄎ + "눈": [["L", 0.1, 0, 0.1, 0.3], ["L", 0.1, 0.3, 0.9, 0.3], ["L", 0, 0.52, 1, 0.52], ["L", 0.5, 0.52, 0.5, 0.68], ["L", 0.1, 0.72, 0.1, 1], ["L", 0.1, 1, 0.9, 1]], +}; +const WORDS = [["E", "Y", "E"], ["눈"], ["S", "E", "E"], ["Y", "O", "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; +} + +// A camera: position, forward, right and up, from its place and its aim. +function camera(pos, aim, out) { + out.o = pos; + let fx = aim[0] - pos[0], fy = aim[1] - pos[1], fz = aim[2] - pos[2]; + const l = sqrt(fx * fx + fy * fy + fz * fz); + fx /= l; fy /= l; fz /= l; + const rl = sqrt(fx * fx + fz * fz); + const rx = -fz / rl, rz = fx / rl; + out.fw = [fx, fy, fz]; + out.rt = [rx, 0, rz]; + out.up = [-rz * fy, rz * fx - rx * fz, rx * fy]; + return out; +} +// The ray through LED (sx, sy) (fractional, from the top left) into D. +const D = [0, 0, 0]; +function ray(cam, sx, sy) { + const u = sx - W / 2, v = H / 2 - sy, fw = cam.fw, rt = cam.rt, up = cam.up; + const x = fw[0] * F + rt[0] * u + up[0] * v, y = fw[1] * F + rt[1] * u + up[1] * v, z = fw[2] * F + rt[2] * u + up[2] * v; + const l = 1 / sqrt(x * x + y * y + z * z); + D[0] = x * l; D[1] = y * l; D[2] = z * l; +} +// Where o + t D meets the cylinder's side (front), or -1. +function hitCyl(ox, oy, oz, dx, dy, dz) { + const a = dx * dx + dz * dz, b = ox * dx + oz * dz, c = ox * ox + oz * oz - RC * RC, disc = b * b - a * c; + if (disc < 0 || a < 1e-12) return -1; + const t = (-b - sqrt(disc)) / a, y = oy + t * dy; + return t > 1e-6 && y >= 0 && y <= HC ? t : -1; +} +// From E through LED (sx, sy): off the mirror and down to the floor. Writes +// the floor point's angle round the axis (0 towards E) and radius to FP; +// false if the ray misses the mirror or the reflection rises. +const FP = [0, 0]; +function design(cam, sx, sy) { + ray(cam, sx, sy); + const o = cam.o, t = hitCyl(o[0], o[1], o[2], D[0], D[1], D[2]); + if (t < 0) return false; + const px = o[0] + t * D[0], py = o[1] + t * D[1], pz = o[2] + t * D[2]; + const dn = D[0] * px + D[2] * pz; // normal (px, 0, pz) / RC + const rx = D[0] - 2 * dn * px, ry = D[1], rz = D[2] - 2 * dn * pz; + if (ry >= -1e-6) return false; + const s = -py / ry, qx = px + s * rx, qz = pz + s * rz; + FP[0] = atan2(qx, qz); + FP[1] = sqrt(qx * qx + qz * qz); + return true; +} + +// ---- The floor drawing, kept in polar texels round the axis: angle +// -126..126 degrees in 600 steps, radius 1..6.5 in 220. +const TH = 2.2, RA = 1, RB = 6.5, NT = 600, NR = 220; +const PAPER = 6.2; // the sheet under the drawing, out to radius 6.2 +// The word's place in E's picture: the rows whose reflection lands between +// radius 1.32 and 6.0 straight in front, and 86% of the mirror's width, +// which comes to rows 12.75 to 52.5 and columns 16.7 to 47.3. +function region(cam) { + let v0 = 0, v1 = H; + for (let y = 0; y < H; y += 0.25) { + if (!design(cam, W / 2, y)) continue; + if (FP[1] > 6.0) v0 = y + 0.25; + else if (FP[1] >= 1.32) v1 = y; + } + let half = 0; + const vm = (v0 + v1) / 2; + for (let x = W / 2; x < W; x += 0.25) if (design(cam, x, vm)) half = x - W / 2; + return { u0: W / 2 - 0.86 * half, u1: W / 2 + 0.86 * half, v0, v1 }; +} + +function segDist(px, py, s) { + const dx = s[2] - s[0], dy = s[3] - s[1], l = dx * dx + dy * dy; + const t = l > 0 ? cl(((px - s[0]) * dx + (py - s[1]) * dy) / l, 0, 1) : 0; + const ex = s[0] + t * dx - px, ey = s[1] + t * dy - py; + return sqrt(ex * ex + ey * ey); +} + +// The word as E should see it: ink coverage on a grid four to an LED over +// the region, letters stacked from the top, and which letter each texel is. +const MS = 4; +function wordMask(wi, reg) { + const word = WORDS[wi], n = word.length; + const MU = Math.ceil((reg.u1 - reg.u0) * MS) + 2, MV = Math.ceil((reg.v1 - reg.v0) * MS) + 2; + const mask = new Float32Array(MU * MV), id = new Uint8Array(MU * MV); + const gap = 2.5, cw = reg.u1 - reg.u0, ch = (reg.v1 - reg.v0 - (n - 1) * gap) / n; + const pen = n === 1 ? 3.4 : 2.6, hw = pen / 2; + for (let k = 0; k < n; k++) { + const x0 = reg.u0, y0 = reg.v0 + k * (ch + gap), sx = cw - pen, sy = ch - pen; + const segs = []; + const P = (u, v) => [x0 + hw + u * sx, y0 + hw + v * sy]; + for (const st of GLYPHS[word[k]]) { + if (st[0] === "L") segs.push([...P(st[1], st[2]), ...P(st[3], st[4])]); + else { + const [, cx, cy, rx, ry, a0, a1] = st, m = max(6, Math.ceil(abs(a1 - a0) / 8)); + for (let i = 0; i < m; i++) { + const b0 = ((a0 + ((a1 - a0) * i) / m) * PI) / 180, b1 = ((a0 + ((a1 - a0) * (i + 1)) / m) * PI) / 180; + segs.push([...P(cx + rx * cos(b0), cy + ry * sin(b0)), ...P(cx + rx * cos(b1), cy + ry * sin(b1))]); + } + } + } + // each stroke marks the texels within half a pen of it + for (const sg of segs) { + const xa = max(0, floor((min(sg[0], sg[2]) - hw - reg.u0) * MS) - 1), xb = min(MU - 1, Math.ceil((max(sg[0], sg[2]) + hw - reg.u0) * MS) + 1); + const ya = max(0, floor((min(sg[1], sg[3]) - hw - reg.v0) * MS) - 1), yb = min(MV - 1, Math.ceil((max(sg[1], sg[3]) + hw - reg.v0) * MS) + 1); + for (let my = ya; my <= yb; my++) + for (let mx = xa; mx <= xb; mx++) { + const o = my * MU + mx; + if (mask[o] >= 1) continue; + const cov = cl((hw - segDist(reg.u0 + (mx + 0.5) / MS, reg.v0 + (my + 0.5) / MS, sg)) * MS + 0.5, 0, 1); + if (cov > mask[o]) { mask[o] = cov; id[o] = k; } + } + } + } + return { mask, id, MU, MV, n }; +} + +// Trace a picture onto the floor: E's picture is cut into cells st LEDs +// wide, each corner is sent off the mirror to the floor, and each cell's +// two triangles are filled into the polar texels, every texel reading the +// picture where its barycentric place falls in E's picture. look(u, v) +// returns the picture there; skip(u, v) lets a cell with nothing in it go. +function trace(cam, reg, st, look, put, skip) { + const nu = Math.ceil((reg.u1 - reg.u0) / st) + 1, nv = Math.ceil((reg.v1 - reg.v0) / st) + 1; + const A = new Float32Array(nu * nv), B = new Float32Array(nu * nv), ok = new Uint8Array(nu * nv); + for (let j = 0; j < nv; j++) + for (let i = 0; i < nu; i++) + if (design(cam, reg.u0 + i * st, reg.v0 + j * st)) { + const n = j * nu + i; + ok[n] = 1; + A[n] = ((FP[0] + TH) / (2 * TH)) * (NT - 1); + B[n] = ((FP[1] - RA) / (RB - RA)) * (NR - 1); + } + const tri = (a0, b0, u0, v0, a1, b1, u1, v1, a2, b2, u2, v2) => { + const det = (b1 - b2) * (a0 - a2) + (a2 - a1) * (b0 - b2); + if (abs(det) < 1e-9) return; + const xa = max(0, Math.ceil(min(a0, a1, a2))), xb = min(NT - 1, floor(max(a0, a1, a2))); + const ya = max(0, Math.ceil(min(b0, b1, b2))), yb = min(NR - 1, floor(max(b0, b1, b2))); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const w0 = ((b1 - b2) * (x - a2) + (a2 - a1) * (y - b2)) / det, w1 = ((b2 - b0) * (x - a2) + (a0 - a2) * (y - b2)) / det, w2 = 1 - w0 - w1; + if (w0 < -1e-6 || w1 < -1e-6 || w2 < -1e-6) continue; + const u = w0 * u0 + w1 * u1 + w2 * u2, v = w0 * v0 + w1 * v1 + w2 * v2; + put(y * NT + x, u, v, look(u, v)); + } + }; + for (let j = 0; j + 1 < nv; j++) + for (let i = 0; i + 1 < nu; i++) { + const n = j * nu + i; + if (!ok[n] || !ok[n + 1] || !ok[n + nu] || !ok[n + nu + 1]) continue; + const u = reg.u0 + i * st, v = reg.v0 + j * st; + if (skip && skip(u, v, st)) continue; + tri(A[n], B[n], u, v, A[n + 1], B[n + 1], u + st, v, A[n + nu], B[n + nu], u, v + st); + tri(A[n + 1], B[n + 1], u + st, v, A[n + nu + 1], B[n + nu + 1], u + st, v + st, A[n + nu], B[n + nu], u, v + st); + } +} + +function floorInk(wi, cam, reg) { + const wm = wordMask(wi, reg), ink = new Float32Array(NT * NR), lid = new Uint8Array(NT * NR); + const sample = (u, v) => { + const mx = (u - reg.u0) * MS - 0.5, my = (v - reg.v0) * MS - 0.5; + const ix = floor(mx), iy = floor(my); + if (ix < 0 || iy < 0 || ix >= wm.MU - 1 || iy >= wm.MV - 1) return 0; + const fx = mx - ix, fy = my - iy, o = iy * wm.MU + ix, m = wm.mask; + return (m[o] * (1 - fx) + m[o + 1] * fx) * (1 - fy) + (m[o + wm.MU] * (1 - fx) + m[o + wm.MU + 1] * fx) * fy; + }; + trace(cam, reg, 0.25, sample, (o, u, v, c) => { + if (c <= ink[o]) return; + ink[o] = c; + const mx = cl(round((u - reg.u0) * MS - 0.5), 0, wm.MU - 1), my = cl(round((v - reg.v0) * MS - 0.5), 0, wm.MV - 1); + lid[o] = wm.id[my * wm.MU + mx]; + }, (u, v, st) => sample(u + st / 2, v + st / 2) === 0 && sample(u, v) === 0 && sample(u + st, v + st) === 0 && sample(u + st, v) === 0 && sample(u, v + st) === 0); + return { ink, lid }; +} + +// Niceron's craticule: a square grid laid over the word in E's picture, 6 +// cells across and 9 down, traced onto the paper with it, so the paper +// carries the bent grid the drawing was made on. +function craticule(cam, reg) { + const g = new Float32Array(NT * NR), cw = (reg.u1 - reg.u0) / 6, ch = (reg.v1 - reg.v0) / 9; + trace(cam, reg, 0.5, (u, v) => { + const a = abs((u - reg.u0) / cw - round((u - reg.u0) / cw)) * cw, b = abs((v - reg.v0) / ch - round((v - reg.v0) / ch)) * ch; + return cl(0.45 - min(a, b), 0, 0.25) * 4; + }, (o, u, v, c) => { if (c > g[o]) g[o] = c; }); + return g; +} + +export function setup(params) { + const cam = camera(E, AIM, {}), reg = region(cam); + params.s = { + ph: 0, hue: 0, design: cam, reg, grid: craticule(cam, reg), cache: {}, k3: -1, word: 0, prev: -1, wipe: 1, next: 0, + cam: camera(E, AIM, {}), col: new Float32Array(9), away: 0.06, + }; +} + +function inkOf(S, wi) { + return S.cache[wi] || (S.cache[wi] = floorInk(wi, S.design, S.reg)); +} + +const SWAY = 14, HOLD = 7, WIPE = 1.3; // seconds at Speed 2 + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + const speed = cl(kv[1], 0.2, 3), k = (d * speed) / 2; + const k3 = cl(round(kv[2]), 0, WORDS.length - 1); + if (k3 !== S.k3) { + S.k3 = k3; S.word = k3; S.prev = -1; S.wipe = 1; S.next = HOLD; + } + S.next -= k; + if (S.next <= 0) { + S.prev = S.word; S.word = (S.word + 1) % WORDS.length; S.wipe = 0; S.next = HOLD; + } + S.wipe = min(1, S.wipe + k / WIPE); + inkOf(S, S.word); + // The camera: round the axis by up to 60 degrees and up or down by up to + // 14, both scaled by Away, on a slow figure of eight through E. + S.away = cl(kv[3], 0, 1); + S.ph += (k * 2 * PI) / SWAY; + const az = S.away * (PI / 3) * sin(S.ph), el = S.away * 0.245 * sin(2 * S.ph + 0.6); + const r0 = sqrt(E[0] * E[0] + E[2] * E[2]), e0 = atan2(E[1], r0), L = sqrt(r0 * r0 + E[1] * E[1]); + const e1 = e0 + el, rr = L * cos(e1); + const pos = [rr * sin(az), L * sin(e1), rr * cos(az)]; + const aim = [AIM[2] * sin(az), AIM[1], AIM[2] * cos(az)]; + camera(pos, aim, S.cam); + for (let i = 0; i < 3; i++) hsv(S.hue + 0.55 + i / 6, 0.85, 1, S.col, i * 3); +} + +// ---- Shading. Ink on paper at a floor point: returns light in C. +const C = [0, 0, 0]; +function floorAt(S, x, z) { + const r = sqrt(x * x + z * z), th = atan2(x, z); + // bare floor: dark tiles a unit square, their joints a little lighter + const jx = abs(x - round(x)), jz = abs(z - round(z)); + let br = 0.03, bg = 0.034, bb = 0.045; + if (min(jx, jz) < 0.035) { br = 0.075; bg = 0.08; bb = 0.095; } + // the paper: a sheet out to PAPER, darker near the mirror's foot + if (r < PAPER && abs(th) < TH - 0.05) { + const edge = cl((PAPER - r) * 8, 0, 1) * cl((TH - 0.05 - abs(th)) * 20, 0, 1); + const occl = 1 - 0.55 * exp(-(r - RC) * 6); + br += (0.13 * occl - br) * edge; bg += (0.135 * occl - bg) * edge; bb += (0.15 * occl - bb) * edge; + const a = ((th + TH) / (2 * TH)) * (NT - 1), b = ((r - RA) / (RB - RA)) * (NR - 1); + const ia = floor(a), ib = floor(b); + if (ia >= 0 && ib >= 0 && ia < NT - 1 && ib < NR - 1) { + const fa = a - ia, fb = b - ib, o = ib * NT + ia; + const G = S.grid, gr = ((G[o] * (1 - fa) + G[o + 1] * fa) * (1 - fb) + (G[o + NT] * (1 - fa) + G[o + NT + 1] * fa) * fb) * edge; + br += 0.13 * gr; bg += 0.14 * gr; bb += 0.16 * gr; + // the new word wipes outwards from the mirror's foot over the old + const front = RC + S.wipe * (PAPER - RC + 0.4); + const mixNew = cl((front - r) * 3, 0, 1); + let ink = 0, id = 0; + for (let pass = 0; pass < 2; pass++) { + const wi = pass ? S.prev : S.word, wgt = pass ? 1 - mixNew : mixNew; + if (wi < 0 || wgt <= 0) continue; + const T = S.cache[wi]; + if (!T) continue; + const I = T.ink, c = (I[o] * (1 - fa) + I[o + 1] * fa) * (1 - fb) + (I[o + NT] * (1 - fa) + I[o + NT + 1] * fa) * fb; + if (c * wgt > ink) { + ink = c * wgt; + id = T.lid[I[o] > 0 ? o : I[o + 1] > 0 ? o + 1 : I[o + NT] > 0 ? o + NT : o + NT + 1]; + } + } + if (ink > 0) { + const col = S.col, q = id * 3; + br += (col[q] * 0.95 - br) * ink; bg += (col[q + 1] * 0.95 - bg) * ink; bb += (col[q + 2] * 0.95 - bb) * ink; + } + } + } + C[0] = br; C[1] = bg; C[2] = bb; +} +// The room above, near black. From this eye no ray reaches it: the mirror +// keeps a ray's fall, and the eye stands above the mirror's top. +function skyAt() { + C[0] = 0.02; C[1] = 0.02; C[2] = 0.03; +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const cam = S.cam, o = cam.o, ox = o[0], oy = o[1], oz = o[2]; + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + ray(cam, px + 0.5, py + 0.5); + const dx = D[0], dy = D[1], dz = D[2]; + let r = 0, g = 0, b = 0; + const tc = hitCyl(ox, oy, oz, dx, dy, dz); + // the top of the cylinder + const tt = dy < 0 ? (HC - oy) / dy : -1; + const cx = ox + tt * dx, cz = oz + tt * dz; + if (tt > 0 && cx * cx + cz * cz <= RC * RC && (tc < 0 || tt < tc)) { + const rr = sqrt(cx * cx + cz * cz); + const rim = cl((rr - 0.88) * 9, 0, 1); + r = 0.07 + 0.3 * rim; g = 0.075 + 0.3 * rim; b = 0.09 + 0.32 * rim; + } else if (tc > 0) { + // the mirror: reflect and look again, a little blue and dimmer, + // brighter where the ray grazes + const hx = ox + tc * dx, hy = oy + tc * dy, hz = oz + tc * dz; + const dn = dx * hx + dz * hz, rx = dx - 2 * dn * hx, ry = dy, rz = dz - 2 * dn * hz; + if (ry < 0) floorAt(S, hx - (hy / ry) * rx, hz - (hy / ry) * rz); + else skyAt(); + const k = 0.8 + 0.2 * Math.pow(1 - abs(dn), 4); + r = C[0] * k * 0.88; g = C[1] * k * 0.93; b = C[2] * k; + } else if (dy < 0) { + const t = -oy / dy; + floorAt(S, ox + t * dx, oz + t * dz); + r = C[0]; g = C[1]; b = C[2]; + } else { + skyAt(); + r = C[0]; g = C[1]; b = C[2]; + } + display.setPixel(px, py, (255 * cl(r, 0, 1)) | 0, (255 * cl(g, 0, 1)) | 0, (255 * cl(b, 0, 1)) | 0); + } +} diff --git a/incoming/pattern-study/patterns/platycosms.js b/incoming/pattern-study/patterns/platycosms.js new file mode 100644 index 000000000..fca89777f --- /dev/null +++ b/incoming/pattern-study/patterns/platycosms.js @@ -0,0 +1,305 @@ +// ===== Patternflow pattern ===== +// Title: Platycosms +// Family: space +// Idea: You fly through a closed flat universe holding one three-armed jack, and every cube of space you look through shows a copy of it, turned by the gluing that brought you there: a quarter turn a step in the tetracosm, a half turn in the dicosm, the Hantzsche-Wendt screws in the didicosm, none in the 3-torus. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: flat 3-manifolds (Werner Nowacki, 1934; Walter Hantzsche and Hilmar Wendt, 1935), names after John Conway and Juan Pablo Rossetti (2003), the inside view after Jeffrey Weeks (The Shape of Space, 1985), pattern round 71 +// Round: 71 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Space=0..3, Heading=0..1 +// +// Knob 1: Hue. Turns the three arm colours round the wheel together: at 0 the long arm is green, the middle one azure and the short one pink, on a white hub. +// Knob 2: Speed. How fast you fly: 0.44 cubes a second at 2, ten times slower at 0.2, half as fast again at 3. +// Knob 3: Space. 0 the tetracosm, whose cube is glued top to bottom with a quarter turn; 1 the dicosm, glued with a half turn; 2 the didicosm, the Hantzsche-Wendt manifold, two cubes big, so only every other cube holds a copy, and each copy is turned a half turn about x, y or z or not at all; 3 the torocosm, the 3-torus, where every copy faces the same way. +// Knob 4: Heading. The angle between your flight and the z axis, the axis of the turn, from 0 to 90 degrees. At the opening 0.06 you fly 5.4 degrees off it, so in the tetracosm each copy you pass is turned a quarter turn from the one before, one every 2.3 s; at 1 you fly along x, the copies in line ahead of you all face the same way, and each layer to your left and right is turned a quarter more. + +const W = 64, H = 128; +const { floor, min, max, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +const F = 60; // focal length in LEDs: 94 degrees of view top to bottom, 56 across +const FOG = 1.9; // brightness falls by e every 1.9 cubes +const TMAX = 5.5; // a ray gives up after 5.5 cubes, where the fog leaves 5% + +function hsv(h, s, v, out, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + out[o] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + out[o + 1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + out[o + 2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; +} + +// ---- The jack, in the fundamental cube [0,1]^3: a hub, three rods along +// +x, +y and +z of lengths 0.27, 0.18 and 0.32, and a ball on each end. It +// stays inside [0.265, 0.735]^3, and so does every copy in its own cube, +// since all the gluings below turn or reflect about the cube's centre lines. +// The slab |y| < 0.265 (mod 1) never meets a copy, and the camera flies in it. +const HUB = [0.356, 0.356, 0.34], HR = 0.075, ROD = 0.028; +const ARM = [0.272, 0.176, 0.32], BALL = [0.06, 0.053, 0.066]; + +// ---- The deck group. For a cube (i, j, k) of the lattice, deck() finds +// the map g^-1 that takes it back onto the fundamental cube, a signed +// permutation and a shift: p_ref[a] = SG[c][a] * q[SRC[c][a]] + TR[a], where +// c is the returned rotation class, or -1 for a cube of the didicosm that +// holds no copy. Classes: 0 none, 1..3 one to three quarter turns undone +// about z, 4..6 the half turns about x, y and z of the didicosm. +const SRC = [[0, 1, 2], [1, 0, 2], [0, 1, 2], [1, 0, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]; +const SG = [[1, 1, 1], [-1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, -1, -1], [-1, 1, -1], [-1, -1, 1]]; +const TR = [0, 0, 0]; +const md = (a, n) => ((a % n) + n) % n; +// Didicosm generators, scaled by 2 so they carry unit cubes to unit cubes: +// alpha (x+1, 1-y, -z), beta (-x, y+1, 1-z), gamma (1-x, -y, z+1). Their +// squares are the steps of 2 along x, y and z, and alpha beta gamma = 1. +// The cube parity i+j+k is kept, so the copies sit on the even cubes only: +// the manifold is two cubes big. Per parity class (i, j, k mod 2 as bits): +// the coset's class, its shift s, and where it sends the cube (0, 0, 0). +const HW = [[0, [0, 0, 0], [0, 0, 0]], null, null, [5, [0, 1, 1], [-1, 1, 0]], null, [4, [1, 1, 0], [1, 0, -1]], [6, [1, 0, 1], [0, -1, 1]], null]; +function deck(m, i, j, k) { + if (m === 2) { + const e = HW[md(i, 2) | (md(j, 2) << 1) | (md(k, 2) << 2)]; + if (!e) return -1; + const c = e[0], D = SG[c], s = e[1], h = e[2]; + // g = T(v) h with v = cell - h(cube 0), so g^-1(q) = D (q - v - s). + TR[0] = -D[0] * (i - h[0] + s[0]); TR[1] = -D[1] * (j - h[1] + s[1]); TR[2] = -D[2] * (k - h[2] + s[2]); + return c; + } + TR[2] = -k; + // Tetracosm: a step up z turns a quarter about the cube's axis x = y = 1/2; + // dicosm a half; torocosm none. Undo k of them. + const r = m === 0 ? md(-k, 4) : m === 1 ? 2 * md(k, 2) : 0; + if (r === 0) { TR[0] = -i; TR[1] = -j; } + else if (r === 1) { TR[0] = j + 1; TR[1] = -i; } + else if (r === 2) { TR[0] = i + 1; TR[1] = j + 1; } + else { TR[0] = -j; TR[1] = i + 1; } + return r; +} + +// ---- Drawing. The panel is a depth buffer: every copy within 5.5 cubes of +// the camera is cut into four parts (the hub ball and the three arms, each +// in its own box), each part's box is projected to the screen, clipped at +// the camera plane, and only the LEDs inside that rectangle trace a ray +// against the part, in the copy's own frame, keeping the nearest hit. About +// 160 copies are in reach, and a frame takes about 1.2 ms in Node; tracing +// every cube along every ray instead took 5 to 7 ms. +const N = W * H; +const DIR = new Float64Array(N * 3), DEPTH = new Float64Array(N), LAM = new Float64Array(N), PART = new Int8Array(N); +// Part boxes in the fundamental cube: lo x y z, hi x y z. +const PB = [ + [HUB[0] - HR, HUB[1] - HR, HUB[2] - HR, HUB[0] + HR, HUB[1] + HR, HUB[2] + HR], + [HUB[0], HUB[1] - BALL[0], HUB[2] - BALL[0], HUB[0] + ARM[0] + BALL[0], HUB[1] + BALL[0], HUB[2] + BALL[0]], + [HUB[0] - BALL[1], HUB[1], HUB[2] - BALL[1], HUB[0] + BALL[1], HUB[1] + ARM[1] + BALL[1], HUB[2] + BALL[1]], + [HUB[0] - BALL[2], HUB[1] - BALL[2], HUB[2], HUB[0] + BALL[2], HUB[1] + BALL[2], HUB[2] + ARM[2] + BALL[2]], +]; +const CAMP = new Float64Array(24), RECT = new Float64Array(4); +const NEAR = 0.01; +function addPt(u, v, z) { + const sx = W / 2 + (F * u) / z, sy = H / 2 - (F * v) / z; + if (sx < RECT[0]) RECT[0] = sx; + if (sx > RECT[1]) RECT[1] = sx; + if (sy < RECT[2]) RECT[2] = sy; + if (sy > RECT[3]) RECT[3] = sy; +} +// The screen rectangle of box b of the copy whose map is class c, shift TR. +function rect(S, b, c) { + const { cam, fw, rt, up } = S, src = SRC[c], sg = SG[c], bx = PB[b]; + let front = 0; + for (let q = 0; q < 8; q++) { + // p_ref -> world: q[SRC[a]] = SG[a] (p[a] - TR[a]) + const p0 = sg[0] * (bx[q & 1 ? 3 : 0] - TR[0]), p1 = sg[1] * (bx[q & 2 ? 4 : 1] - TR[1]), p2 = sg[2] * (bx[q & 4 ? 5 : 2] - TR[2]); + const X = (src[0] === 0 ? p0 : p1) - cam[0], Y = (src[1] === 1 ? p1 : p0) - cam[1], Z = p2 - cam[2]; + const z = X * fw[0] + Y * fw[1] + Z * fw[2]; + CAMP[q * 3] = X * rt[0] + Y * rt[1] + Z * rt[2]; + CAMP[q * 3 + 1] = X * up[0] + Y * up[1] + Z * up[2]; + CAMP[q * 3 + 2] = z; + if (z >= NEAR) front++; + } + if (!front) return false; + RECT[0] = RECT[2] = 1e9; RECT[1] = RECT[3] = -1e9; + for (let q = 0; q < 8; q++) if (CAMP[q * 3 + 2] >= NEAR) addPt(CAMP[q * 3], CAMP[q * 3 + 1], CAMP[q * 3 + 2]); + if (front < 8) + for (let q = 0; q < 8; q++) + for (let e = 1; e < 8; e <<= 1) { + const r = q | e; + if (r === q) continue; + const za = CAMP[q * 3 + 2], zb = CAMP[r * 3 + 2]; + if (za < NEAR === zb < NEAR) continue; + const f = (NEAR - za) / (zb - za); + addPt(CAMP[q * 3] + f * (CAMP[r * 3] - CAMP[q * 3]), CAMP[q * 3 + 1] + f * (CAMP[r * 3 + 1] - CAMP[q * 3 + 1]), NEAR); + } + return RECT[0] < W && RECT[1] > 0 && RECT[2] < H && RECT[3] > 0; +} + +// Ray o + t d against a ball at the centre (cx, cy, cz). +function ball(ox, oy, oz, dx, dy, dz, cx, cy, cz, r) { + const qx = ox - cx, qy = oy - cy, qz = oz - cz; + const b = qx * dx + qy * dy + qz * dz, disc = b * b - (qx * qx + qy * qy + qz * qz - r * r); + if (disc < 0) return 1e9; + const t = -b - sqrt(disc); + return t > 1e-4 ? t : 1e9; +} +// Trace part b for the LEDs in RECT, in the copy's frame (class c, shift TR). +function raster(S, b, c) { + const { cam } = S, src = SRC[c], sg = SG[c]; + const s0 = src[0], s1 = src[1], g0 = sg[0], g1 = sg[1], g2 = sg[2]; + const ox = g0 * cam[s0] + TR[0] - HUB[0], oy = g1 * cam[s1] + TR[1] - HUB[1], oz = g2 * cam[2] + TR[2] - HUB[2]; + const x0 = max(0, floor(RECT[0])), x1 = min(W - 1, floor(RECT[1])), y0 = max(0, floor(RECT[2])), y1 = min(H - 1, floor(RECT[3])); + const L = b ? ARM[b - 1] : 0, w = b ? BALL[b - 1] : HR, R2 = ROD * ROD; + for (let py = y0; py <= y1; py++) + for (let px = x0, p = py * W + x0; px <= x1; px++, p++) { + const p3 = p * 3; + const dx = g0 * DIR[p3 + s0], dy = g1 * DIR[p3 + s1], dz = g2 * DIR[p3 + 2]; + let best = DEPTH[p], nx = 0, ny = 0, nz = 0, hit = false; + // the ball: the hub, or the end of arm b + const ex = b === 1 ? L : 0, ey = b === 2 ? L : 0, ez = b === 3 ? L : 0; + let t = ball(ox, oy, oz, dx, dy, dz, ex, ey, ez, w); + if (t < best) { + best = t; hit = true; + nx = (ox + t * dx - ex) / w; ny = (oy + t * dy - ey) / w; nz = (oz + t * dz - ez) / w; + } + if (b) { + // the rod round the arm's axis, from the hub to the ball + const up = b === 1 ? oy : ox, uq = b === 3 ? oy : oz, ua = b === 1 ? ox : b === 2 ? oy : oz; + const dp = b === 1 ? dy : dx, dq = b === 3 ? dy : dz, da = b === 1 ? dx : b === 2 ? dy : dz; + const A = dp * dp + dq * dq, B = up * dp + uq * dq, D = B * B - A * (up * up + uq * uq - R2); + if (D > 0) { + t = (-B - sqrt(D)) / A; + const s = ua + t * da; + if (t > 1e-4 && t < best && s > 0 && s < L) { + best = t; hit = true; + const np = (up + t * dp) / ROD, nq = (uq + t * dq) / ROD; + if (b === 1) { nx = 0; ny = np; nz = nq; } else if (b === 2) { nx = np; ny = 0; nz = nq; } else { nx = np; ny = nq; nz = 0; } + } + } + } + if (hit) { + DEPTH[p] = best; PART[p] = b; + LAM[p] = max(0, -(nx * dx + ny * dy + nz * dz)); + } + } +} + +// ---- The cube edges, faint lines of the lattice the copies sit in, as in +// Weeks's Curved Spaces. Each edge line within 5.5 of the camera is clipped +// at the camera plane and stepped one LED at a time on the screen, with the +// distance found by perspective-correct interpolation, so a jack in front +// hides it. +const EDGE = new Float32Array(N); +function edge(S, ax, ay, az, bx, by, bz) { + const { cam, fw, rt, up } = S; + let X = ax - cam[0], Y = ay - cam[1], Z = az - cam[2]; + let u0 = X * rt[0] + Y * rt[1] + Z * rt[2], v0 = X * up[0] + Y * up[1] + Z * up[2], z0 = X * fw[0] + Y * fw[1] + Z * fw[2]; + X = bx - cam[0]; Y = by - cam[1]; Z = bz - cam[2]; + let u1 = X * rt[0] + Y * rt[1] + Z * rt[2], v1 = X * up[0] + Y * up[1] + Z * up[2], z1 = X * fw[0] + Y * fw[1] + Z * fw[2]; + if (z0 < NEAR && z1 < NEAR) return; + if (z0 < NEAR) { const f = (NEAR - z0) / (z1 - z0); u0 += f * (u1 - u0); v0 += f * (v1 - v0); z0 = NEAR; } + else if (z1 < NEAR) { const f = (NEAR - z1) / (z0 - z1); u1 += f * (u0 - u1); v1 += f * (v0 - v1); z1 = NEAR; } + const x0 = W / 2 + (F * u0) / z0, y0 = H / 2 - (F * v0) / z0, x1 = W / 2 + (F * u1) / z1, y1 = H / 2 - (F * v1) / z1; + if ((x0 < 0 && x1 < 0) || (x0 >= W && x1 >= W) || (y0 < 0 && y1 < 0) || (y0 >= H && y1 >= H)) return; + // walk only the part of the screen segment that lies on the panel + let s0 = 0, s1 = 1; + const dx = x1 - x0, dy = y1 - y0; + if (dx) { const a = (0 - x0) / dx, b = (W - x0) / dx; s0 = max(s0, min(a, b)); s1 = min(s1, max(a, b)); } + if (dy) { const a = (0 - y0) / dy, b = (H - y0) / dy; s0 = max(s0, min(a, b)); s1 = min(s1, max(a, b)); } + if (s0 >= s1) return; + const n = Math.ceil((s1 - s0) * sqrt(dx * dx + dy * dy) * 1.3) + 1; + for (let i = 0; i <= n; i++) { + const sg = s0 + ((s1 - s0) * i) / n, px = floor(x0 + sg * dx), py = floor(y0 + sg * dy); + if (px < 0 || py < 0 || px >= W || py >= H) continue; + const w = (1 - sg) / z0 + sg / z1, lam = sg / z1 / w; + const uu = u0 + lam * (u1 - u0), vv = v0 + lam * (v1 - v0), zz = 1 / w; + const dist = sqrt(uu * uu + vv * vv + zz * zz), p = py * W + px; + if (dist < DEPTH[p]) { const e = exp(-dist / FOG); if (e > EDGE[p]) EDGE[p] = e; } + } +} +function edges(S) { + const cam = S.cam, R = Math.ceil(TMAX); + EDGE.fill(0); + for (let ax = 0; ax < 3; ax++) { + const a1 = (ax + 1) % 3, a2 = (ax + 2) % 3; + for (let b = floor(cam[a1]) - R; b <= floor(cam[a1]) + R + 1; b++) + for (let c = floor(cam[a2]) - R; c <= floor(cam[a2]) + R + 1; c++) { + const db = b - cam[a1], dc = c - cam[a2], d2 = db * db + dc * dc; + if (d2 >= TMAX * TMAX) continue; + const L = sqrt(TMAX * TMAX - d2), A = [0, 0, 0], B = [0, 0, 0]; + A[ax] = cam[ax] - L; B[ax] = cam[ax] + L; A[a1] = B[a1] = b; A[a2] = B[a2] = c; + edge(S, A[0], A[1], A[2], B[0], B[1], B[2]); + } + } +} + +export function setup(params) { + params.s = { t: 0, s: 0, hue: 0, m: 0, head: 0.06, cam: [0.1, 0.1, 0.1], fw: [0, 0, 1], rt: [1, 0, 0], up: [0, 1, 0], col: new Float32Array(12) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = kv[0]; + const speed = cl(kv[1], 0.2, 3); + S.m = cl(Math.round(kv[2]), 0, 3); + S.head = cl(kv[3], 0, 1); + S.t += d * speed / 2; + S.s += d * speed * 0.22; + // Flight in the plane y = 0.03, heading th off the z axis, looking 12.6 + // degrees right of the flight and 9 up, the gaze swaying 9 degrees left + // and right and 6 up and down. + const th = S.head * PI / 2; + const cam = S.cam; + cam[0] = 0.04 + 0.03 * sin(S.t * 0.7) + S.s * sin(th); + cam[1] = 0.03 + 0.025 * sin(S.t * 0.53); + cam[2] = 0.1 + S.s * cos(th); + const yaw = th + 0.22 + 0.16 * sin(S.t * 0.41), pitch = 0.16 + 0.1 * sin(S.t * 0.29); + const fw = S.fw, rt = S.rt, up = S.up; + fw[0] = sin(yaw) * cos(pitch); fw[1] = sin(pitch); fw[2] = cos(yaw) * cos(pitch); + rt[0] = cos(yaw); rt[1] = 0; rt[2] = -sin(yaw); + up[0] = fw[1] * rt[2] - fw[2] * rt[1]; + up[1] = fw[2] * rt[0] - fw[0] * rt[2]; + up[2] = fw[0] * rt[1] - fw[1] * rt[0]; + // hub white, then the x, y and z arms + const col = S.col; + col[0] = 0.95; col[1] = 0.93; col[2] = 0.88; + hsv(S.hue + 0.56, 0.8, 1, col, 3); + hsv(S.hue + 0.9, 0.7, 1, col, 6); + hsv(S.hue + 0.3, 0.85, 1, col, 9); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { cam, fw, rt, up, col, m } = S; + for (let py = 0, p = 0; py < H; py++) + for (let px = 0; px < W; px++, p++) { + const sx = (px + 0.5 - W / 2) / F, sy = (H / 2 - py - 0.5) / F; + const dx = fw[0] + rt[0] * sx + up[0] * sy, dy = fw[1] + rt[1] * sx + up[1] * sy, dz = fw[2] + rt[2] * sx + up[2] * sy; + const l = 1 / sqrt(dx * dx + dy * dy + dz * dz); + DIR[p * 3] = dx * l; DIR[p * 3 + 1] = dy * l; DIR[p * 3 + 2] = dz * l; + } + DEPTH.fill(TMAX); + const ci = floor(cam[0]), cj = floor(cam[1]), ck = floor(cam[2]), R = Math.ceil(TMAX) + 1, RR = (TMAX + 0.87) * (TMAX + 0.87); + for (let a = -R; a <= R; a++) + for (let b = -R; b <= R; b++) + for (let c = -R; c <= R; c++) { + const i = ci + a, j = cj + b, k = ck + c; + const vx = i + 0.5 - cam[0], vy = j + 0.5 - cam[1], vz = k + 0.5 - cam[2]; + if (vx * vx + vy * vy + vz * vz > RR || vx * fw[0] + vy * fw[1] + vz * fw[2] < -0.87) continue; + const cls = deck(m, i, j, k); + if (cls < 0) continue; + for (let part = 0; part < 4; part++) if (rect(S, part, cls)) raster(S, part, cls); + } + edges(S); + for (let p = 0; p < N; p++) { + const e = EDGE[p] * 0.3; + let r = e * 0.55, g = e * 0.7, b = e; + const t = DEPTH[p]; + if (t < TMAX) { + const lam = LAM[p], fog = exp(-t / FOG); + const sh = (0.28 + 0.72 * lam) * fog, spec = lam > 0.9 ? (lam - 0.9) * 4 * fog : 0; + const cc = PART[p] * 3; + r = col[cc] * sh + spec; g = col[cc + 1] * sh + spec; b = col[cc + 2] * sh + spec; + } + display.setPixel(p % W, (p / W) | 0, (255 * cl(r, 0, 1)) | 0, (255 * cl(g, 0, 1)) | 0, (255 * cl(b, 0, 1)) | 0); + } +} From 0b409ab6f7401b96ee930398ca235863bb7dc5d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:14:30 +0000 Subject: [PATCH 08/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20Book=20from=20the=20Sky=20=EC=9D=98=20?= =?UTF-8?q?=E5=BF=83=20=EA=B3=A0=EC=B9=98=EA=B3=A0=20Lorem=20Ipsum=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../pattern-study/patterns/book-from-the-sky.js | 4 ++-- incoming/pattern-study/patterns/lorem-ipsum.js | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/incoming/pattern-study/patterns/book-from-the-sky.js b/incoming/pattern-study/patterns/book-from-the-sky.js index 92c51d3d7..536f27289 100644 --- a/incoming/pattern-study/patterns/book-from-the-sky.js +++ b/incoming/pattern-study/patterns/book-from-the-sky.js @@ -29,7 +29,7 @@ const LEFT = { ren: "39 2003 1218", shui: "39 0011 0314 0826", shou: "39 0222 101807 0624", mu: "39 0222 1018 1306 2424", kou: "39 0323260603", ri: "39 0121270701 0424", yan: "39 1010 0121 0323 0525280805", jin: "39 1001 1021 0323 0525 1318 0827", wang: "39 0121 0424 1117 0827", tu: "39 0323 1117 0827", he: "39 2001 0222 1118 1306 2424", mu2: "39 0020280800 0323 0525", - fu: "39 0008 00202112232415", xin: "39 1018 0203 2121", shi: "39 0121 1104 0525280805", huo: "39 0203 2122 101508 1627", + fu: "39 0008 00202112232415", xin: "39 1018 0203 2121", shi: "39 0121 1104 0525280805", }; const TOP = { // Heads, 9 by 3: grass, roof, bamboo, net, lid, person, mountain, cover. @@ -38,7 +38,7 @@ const TOP = { }; const BOT = { // Feet, 9 by 3: heart, fire dots, legs, dish, one. - xin: "93 0101 21327281 4040 6070", huo: "93 0211 3132 5152 7182", er: "93 303102 50528281", min: "93 1070 1012 3032 5052 7072 0282", yi: "93 0181", + xin: "93 0112 2021327281 5050 7070", huo: "93 0211 3132 5152 7182", er: "93 303102 50528281", min: "93 1070 1012 3032 5052 7072 0282", yi: "93 0181", }; const GEN = { // Parts that go anywhere, small grids stretched to their box. diff --git a/incoming/pattern-study/patterns/lorem-ipsum.js b/incoming/pattern-study/patterns/lorem-ipsum.js index e83d7e9ab..8fb019dd9 100644 --- a/incoming/pattern-study/patterns/lorem-ipsum.js +++ b/incoming/pattern-study/patterns/lorem-ipsum.js @@ -156,11 +156,11 @@ const CUE = 0.35, CUT = 1.4, SLIDE_AT = 1.2, SLIDE_T = 1.0, IN_AT = 2.4, IN_SPRE const LIFE = 28; export function setup(params) { - params.s = { t: 0, ready: false, pair: 0, side: 0, text: "", line: [], bodies: [], next: 0, hue: 0, speed: 2, g: 240, turns: 0, stats: null }; + params.s = { t: 0, ready: false, pair: 0, side: 0, text: "", line: [], bodies: [], next: 0, hue: 0, speed: 2, g: 240 }; } function body(ch, x, y, mode) { - return { ch, rot: 0, x, y, vx: 0, vy: 0, mode, t0: 0, t1: 0, x0: x, y0: y, x1: x, y1: y, to: ch, born: 0, landed: -9, inAt: -9, fresh: false, spin: 0 }; + return { ch, rot: 0, x, y, vy: 0, mode, t0: 0, t1: 0, x0: x, y0: y, x1: x, y1: y, to: ch, landed: -9, fresh: false, spin: 0 }; } function setText(S, str) { @@ -181,7 +181,7 @@ function turn(S, str) { if (ops[k] !== "+") i++; if (ops[k] !== "-") j++; } - let cut = 0, inn = 0, kept = 0; + let cut = 0, inn = 0; for (let k = 0, i = 0, j = 0; k < ops.length; k++) { const o = ops[k]; if (o === "=") { @@ -190,13 +190,12 @@ function turn(S, str) { Object.assign(b, { mode: SLIDE, t0: t + SLIDE_AT + 0.03 * pa[i].line, x0: b.x, y0: b.y, x1: pb[j].x, y1: pb[j].y, to: str[j] }); b.t1 = b.t0 + SLIDE_T; nl[j] = b; - kept++; } i++, j++; } else if (o === "-") { const b = S.line[i]; - // Cut letters go in reading order, the first CUE s before it falls - // turning towards the colour of the cut. + // Cut letters go in reading order over CUT s, each turning towards + // the colour of the cut for CUE s before it falls. if (b) Object.assign(b, { mode: LOOSE, t0: t + CUE + (CUT * cut++) / max(1, nCut - 1) }); i++; } else { @@ -212,7 +211,6 @@ function turn(S, str) { S.text = str; S.line = nl; S.next = t + DONE + HOLD; - S.stats = { kept, cut: nCut, ins: nIn }; } // Where a sprite at column x would come to rest on the surface. @@ -243,7 +241,6 @@ export function update(dt, input, params) { if (pair !== S.pair) (S.pair = pair), (S.side = 0); else S.side ^= 1; turn(S, PAIRS[S.pair][S.side]); - S.turns++; } // Timed modes. @@ -252,7 +249,7 @@ export function update(dt, input, params) { if (b.mode === LOOSE && t >= b.t0) { // A cut letter tips onto its side a third of the time each way. const r = Math.random(); - Object.assign(b, { mode: FALL, vy: 0, vx: 0, y0: b.y, spin: r < 0.25 ? 1 : r < 0.5 ? 3 : 0, born: t }); + Object.assign(b, { mode: FALL, vy: 0, y0: b.y, spin: r < 0.25 ? 1 : r < 0.5 ? 3 : 0 }); } else if (b.mode === SLIDE) { const u = cl((t - b.t0) / (b.t1 - b.t0), 0, 1), e = u * u * (3 - 2 * u); b.x = b.x0 + (b.x1 - b.x0) * e; @@ -264,7 +261,6 @@ export function update(dt, input, params) { // lies there, the highest first; otherwise it falls in from above. let best = null; for (const h of heap) if (h.mode === HEAP && h.ch.toLowerCase() === b.ch.toLowerCase() && (!best || h.y < best.y)) best = h; - b.inAt = t; b.fresh = true; if (best) { best.mode = -1; From 531bf667c225d0d11c456c75ad2e15ceb24eabe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:04:52 +0000 Subject: [PATCH 09/10] =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=97=B0=EA=B5=AC?= =?UTF-8?q?=2071=ED=8C=90:=20README=20=EC=97=B0=EA=B5=AC=20=EB=85=B8?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20index.html?= 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_01Gdqi6Ru5nYSNEX485wYhh1 --- incoming/pattern-study/README.md | 101 +++++++++++++++++++++++++++++- incoming/pattern-study/index.html | 4 +- 2 files changed, 100 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 2aadb9772..9d5ee1189 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 팹턮 연구 2026-10 -새 팹턮 598개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 54판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 55판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 56판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 57판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 58판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 59판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 60판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 61판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 갈래부터 ì—Ž 갈래에 하나씩 더했얎요. 62판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 63판은 64판곌 같읎 죌제 표에 「타읎포」가 듀얎옚 첫 판읎띌, Ꞁ자가 듀얎간 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 64판은 죌제 표가 바뀌얎 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 ë„€ 갈래에 더했얎요. 65판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 66판도 같은 표로 돌아서 Ꞁ자 팹턮 넷을 Ꞁ자 갈래에, 하나륌 데읎터 귞늌 갈래에 더하고, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 67판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 68판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 69판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. [index.html](index.html) 을 람띌우저로 엎멎 전부 한 장에서 돌아가요. ë§š 위 지도에서 점을 누륎멎 ê·ž 팚턎윌로 가고, 칎드의 슬띌읎더가 녾뾌 넷읎에요. 마음에 드는 것은 「윔드 복사」로 가젞가 Pattern Lab 의 CODE 칞에 붙여 넣윌멎 같은 귞늌읎 나와요. 페읎지는 파음 하나띌서 바깥에서 받아 였는 것읎 없얎요. +새 팹턮 608개륌 쉰여섯 갈래로 나눠 만듀었얎요. 1판에서 엎섞 갈래로 23개륌 만듀고, 2판에서 1판에 없던 아홉 갈래로 16개륌, 3판에서 하나륌, 4판에서 새 갈래읞 직묌로 하나륌 더했얎요. 5판은 새 팹턮 없읎 1판의 Euclid Pulse 륌 닀시 ì§°ê³ , 6판은 섞욎 팚널의 128쀄을 위아래로 ë‹€ 쓰는 팹턮 하나륌 더했얎요. 7판은 또렷한 몚양읎 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 8판은 새 갈래읞 Ʞ계로 하나륌 더했얎요. 9판은 새 팹턮 없읎 1판의 Sprout 을 닀시 ì§°ì–Žìš”. 10판은 섞욎 팚널을 떚얎지는 랔록의 우묌로 쓰는 팹턮 하나륌 더했얎요. 11판은 여러 색읎 ꜀ 멀늬 흘러가는 지도 칞에 팹턮 하나륌 더했얎요. 12판은 새 갈래읞 천첎로 하나륌 더했얎요. 13판은 새 팹턮 없읎 1판의 Glyph Rain 을 닀시 ì§°ì–Žìš”. 14판은 절찚가 바뀌Ʞ 전에 띄욎 바퀎띌서, 섞욎 팚널의 높읎륌 ë‹€ 떚얎지는 폭포 하나륌 더했얎요. 15판은 바뀐 절찚의 첫 판읎띌 ì—Ž 갈래에 새 팚턎을 하나씩, 몚두 ì—Ž 개륌 더했얎요. 도시와 음악 갈래는 귞때 새로 엎었얎요. 16판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 바닀와 곀충 갈래륌 새로 엎었얎요. 17판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 전자 회로와 장난감 갈래륌 새로 엎었얎요. 18판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 유늬와 ꎑ묌·결정, 놀읎공원 갈래륌 새로 엎었얎요. 19판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 부엌곌 버섯·균사 갈래륌 새로 엎었얎요. 20판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 새와 화학·싀험 갈래륌 새로 엎었얎요. 21판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 종읎와 욎동·겜Ʞ 갈래륌 새로 엎었얎요. 22판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 몞곌 비행 갈래륌 새로 엎었얎요. 23판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 묎대·공연곌 짐승 갈래륌 새로 엎었얎요. 24판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 공사·걎섀곌 도자Ʞ 갈래륌 새로 엎었얎요. 25판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 농사·밭곌 화산 갈래륌 새로 엎었얎요. 26판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 얌음·극지와 믞생묌 갈래륌 새로 엎었얎요. 27판도 ì—Ž 갈래에 하나씩 ì—Ž 개륌 더했고, 철도와 필늄·영화 갈래륌 새로 엎었얎요. 28판은 죌제 표가 수학 쪜윌로 바뀐 첫 판읎띌 읎늄 있는 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했고, 수론 갈래륌 새로 엎었얎요. 29판은 죌제 표가 수학 여섯곌 화장품 팝업 넷윌로 나뉜 첫 판읎띌, 읎늄 있는 수학 대상 여섯곌 화장품 맀장 벜에 틀 질감 넷을 더했고, 위상·맀듭곌 화장품 팝업 갈래륌 새로 엎었얎요. 30판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 맀장의 LED 벜에 틀 질감 넷을 화장품 팝업 갈래에 더했얎요. 31판도 같은 표로 돌아서 읎늄 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 ì—Žì§€ 않았얎요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜의 질감 넷을 화장품 팝업 갈래에 더했얎요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 맀장 벜 질감 넷을 화장품 팝업 갈래에 더했얎요. 41판은 죌제 표가 수학·추상 닀섯곌 할로윈 닀섯윌로 바뀐 첫 판읎띌, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고 할로윈 갈래륌 새로 ì—Žì–Ž 10월 31음에 틀 팹턮 닀섯을 넣었얎요. 42판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 41판읎 연 할로윈 갈래에 더했얎요. 43판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 44판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩 더하고, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 45판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 ì—Ž 개 아래읞 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 46판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 47판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 48판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 49판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 50판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 51판도 같은 표로 돌아서 수학 대상 닀섯을 닀섯 갈래에 하나씩, 할로윈 밀에 틀 귞늌 닀섯을 할로윈 갈래에 더했얎요. 52판도 같은 표로 돌아서 수학 대상 닀섯을 팚턎읎 적은 닀섯 갈래에 하나씩, 할로윈 귞늌 닀섯을 할로윈 갈래에 더했얎요. 53판은 할로윈 몫읎 쉬게 된 첫 판읎띌, 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 54판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 55판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 56판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 57판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 58판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 ì—Ž 갈래에 하나씩 더했얎요. 59판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 60판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 61판도 수학 대상 ì—Ž 개륌 팚턎읎 적은 갈래부터 ì—Ž 갈래에 하나씩 더했얎요. 62판도 수학 대상 ì—Ž 개륌 ì—Ž 갈래에 하나씩 더했얎요. 63판은 64판곌 같읎 죌제 표에 「타읎포」가 듀얎옚 첫 판읎띌, Ꞁ자가 듀얎간 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 64판은 죌제 표가 바뀌얎 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 ë„€ 갈래에 더했얎요. 65판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 66판도 같은 표로 돌아서 Ꞁ자 팹턮 넷을 Ꞁ자 갈래에, 하나륌 데읎터 귞늌 갈래에 더하고, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 67판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 68판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 69판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. 71판도 같은 표로 돌아서 Ꞁ자 팹턮 닀섯을 Ꞁ자 갈래에, 수학 대상 닀섯을 닀섯 갈래에 하나씩 더했얎요. [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판에서 섞로로 닀시 ì§°ì–Žìš”. @@ -945,6 +945,21 @@ | [Whitney-Graustein](patterns/whitney-graustein.js) | 위상·맀듭 | 접선 방향을 묎지개빛윌로 두륞 닫힌 곡선읎 멈추지 않고 몚양을 바꟞며 감은 수대로 칞을 칠하고, Turns 륌 돌늬멎 작은 고늬가 런족점윌로 조여 사띌지거나 런족점에서 고늬가 태얎나며 위 닀읎얌의 회전수가 바뀌얎요. | | [Steiner Symmetrization](patterns/steiner-symmetrization.js) | 선·궀적 | 쎈승달, 구멍 난 달걀, 음곱 뿔 별, ë–šì–Žì§„ 두 덩읎, 빗읎 한 LED 현의 빗ꞈ윌로 귞렀지고, 현마닀 제 박자로 믞끄러젞 가욎데가 흐늰 축에 였고 틈윌로 갈렞던 현은 한 쀄로 붙은 ë’€, 빗ꞈ읎 새 축윌로 돌아 닀시 믞끄러지는 동안 아래 막대가 같은 넓읎 원의 눈ꞈ까지 쀄얎요. | +### 71판 + +| 팹턮 | 갈래 | 한 쀄 | +| --- | --- | --- | +| [Catoptric Anamorphosis](patterns/catoptric-anamorphosis.js) | Ꞁ자 | 원Ʞ둥 ê±°ìšž 둘레 바닥에 고늬로 휘얎 귞늰 낱말읎 ê±°ìšž 속에서만 바로 서고, 칎메띌가 바닥 귞늌을 쫓은 눈에서 멀얎지멎 ê±°ìšž 속 Ꞁ자가 믞끄러지며 êž°ìšžì–Žìš”. | +| [Sator Square](patterns/sator-square.js) | Ꞁ자 | 돌판에 새ꞎ SATOR 닀섯 쀄을 가로, 섞로, 거꟞로, 아래에서 위로 읜는 빛 넷읎 늘 같은 Ꞁ자 위륌 지나 가욎데 N 에서 만나고, Ꞁ자듀읎 가장 짧게 짝지은 Ꞟ로 날아가 팹널 높읎의 PATERNOSTER 십자가와 ë„€ 끝의 A, O 가 되었닀가 돌아와요. | +| [Benton Pantograph](patterns/benton-pantograph.js) | Ꞁ자 | 위쪜 큰 볞 Ꞁ자의 윀곜을 슀타음러슀가 따띌 돌멎 팬터귞래프 막대가 ê·ž 움직임을 쀄여 아래 랔랭크에 4분의 1 크Ʞ Ꞁ자륌 새Ʞ고, 녞람가 깎는 Ꞁ자만 Ʞ욞읎거나 좁히고 넓혀서, ë‹€ 깎은 펀치가 BENTON, PUNCH, MATRIX, FAMILY 로 아래 두 쀄에 서요. | +| [Book from the Sky](patterns/book-from-the-sky.js) | Ꞁ자 | 부수와 부품을 싀제 한자의 틀에 맞춰 지얎낞 읜을 수 없는 Ꞁ자가 ꎘ선 친 섞로쀄 닀섯을 채우고, 얇은 새 종읎가 였륞쪜부터 덮여 밑의 쪜읎 비치는 채로 한 쀄씩 위에서 아래로 찍혀요. | +| [Lorem Ipsum](patterns/lorem-ipsum.js) | Ꞁ자 | 킀쌀로의 묞장읎 작은 활자로 짜여 있닀가 정렬 셈읎 고륞 Ꞁ자만 낚고, 잘늰 Ꞁ자는 쀄에서 ë–šì–Žì ž 바닥에 쌓읎고 낚은 Ꞁ자가 Lorem ipsum 의 새 쀄로 믞끄러지멎, 몚자란 Ꞁ자가 더믞에서 뛰얎였륎거나 위에서 ë–šì–Žì ž 채워요. | +| [Twindragon](patterns/twindragon.js) | 프랙탈 | 첎크묎늬로 쌜진 LED 하나하나가 −1+i 진법윌로 적힌 가우슀 정수읎고, 512 개씩 묶읞 쌍둥읎 용 타음읎 저마닀 흐늰 빛깔로 팚널을 덮은 채, 몚든 타음에서 같은 수륌 섞는 밝은 찜읎 지나가며 자늬올늌마닀 135도씩 돈 작은 쌍둥읎 용듀로 몚양을 바꿔요. | +| [Platycosms](patterns/platycosms.js) | 공간·원귌 | 닫혀 있지만 평평한 우죌륌 날멎 큐람마닀 같은 섞 팔 잭의 사볞읎 볎읎고, 처음 여는 4분의 1 바퀮 공간에서는 z 로 한 칞 지날 때마닀 사볞읎 4분의 1 바퀎씩 돌아 있얎요. | +| [Mastermind](patterns/mastermind.js) | 알고늬슘 | 1296 가지 암혞가 6×6 랔록 36개로 ë§š 위에 서고, 크누슀의 믞니맥슀가 1296 개륌 ë‹€ 훑얎 고륞 수륌 아래 판에 ꜂을 때마닀 아직 가능한 암혞가 받을 답대로 묌듀었닀가 옚 답의 묎늬만 낚아서, 닀섯 수 안에 LED 하나로 쀄얎요. | +| [Tensegrity](patterns/tensegrity.js) | 묌늬·슀프링 | 서로 닿지 않는 막대 15개가 팜팜한 쌀읎랔 귞묌에 걞렀 팹널 높읎의 탑윌로 서서 16쎈에 한 바퀮 천천히 돌고, Load 륌 올늬멎 ꌭ대Ʞ의 추가 탑을 17% 짧게 누륎며 위쪜을 74도 더 비틀얎요. | +| [Problem of Apollonius](patterns/apollonius-problem.js) | Ʞ하·대칭 | 섞 흰 원읎 팹널 위륌 떠닀니는 동안 셋 몚두에 접하는 원 여덟 개가 안팎 접쎉에 따띌 제 색윌로 귞렀지고 접점마닀 밝은 점읎 찍히닀가, 흰 원 둘읎 겹치멎 넷읎 짝지얎 합쳐지며 사띌졌닀 떚얎지멎 돌아와요. | + ## 1판 갈래마닀 녾뾌 넷의 자늬는 갈래가 달띌도 거의 같아요. K1 은 색읎고 K2 는 빠륎Ʞ예요. K3 은 몚양을 바꟞고, K4 는 ê·ž 갈래만의 손잡읎예요. 귞래서 처음 볎는 팹턮도 K1 곌 K2 는 손읎 뚌저 알아요. @@ -3819,6 +3834,70 @@ Lab 에서 핮 볎Ʞ: Text 레읎얎륌 mask 로 두고 maskInvert 륌 쌠 ë’€ 죌제 5 「수학 · 추상」윌로, 몚양을 원윌로 닀듬는 가장 였래된 손놀늌을 빗ꞈ 한 장윌로 볎여 죌렀 했얎요. 알윔프 슈타읎너(Jakob Steiner)는 1838년 Crelle 지에 낾 등죌 묞제 슝명에서, 축을 하나 정하고 축에 수직읞 현을 몚두 제 Ꞟ읎 귞대로 축 가욎데로 옮Ʞ는 대칭화륌 썌얎요. 한 직선 위에 현읎 여럿읎멎 Ꞟ읎륌 더한 현 하나가 돌요. 넓읎는 귞대로읎고 둘레는 늘지 않아요. 축을 바꿔 가며 되풀읎하멎 원윌로 간닀는 것은 빌헬늄 귞로슀(Wilhelm Gross)가 1917년에 방향의 찚례륌 곚띌 볎였고, ê±°êž° 쓰읞 수렎 부분엎은 빌헬늄 랔띌슈쌀(Wilhelm Blaschke)가 1916년 『Kreis und Kugel』에서 낾 선택 정늬에서 와요. 축 몇 개만 돌렀 쓰멎 ê·ž 축마닀 ê±°ìšž 대칭읞 몚양에서 멈춘닀는 것은 대니얌 큎레읞(Daniel Klain)읎 2012년에 볎였얎요. 팚널에는 1 LED 간격 현 144 개가 축을 따띌 서고, 몚양은 점의 고늬로 듀고 있닀가 현마닀 고늬와 만나는 점을 정렬핎 둘씩 묶얎 조각을 얻얎요. 조각듀은 제 찚례대로 쌓여 가욎데 맞춘 현 하나가 되고, 현마닀 축을 따띌 0.45 쎈에 걞쳐 늊게 출발하며 0.56~0.84 쎈 동안 믞끄러젞서 빗ꞈ 더믞에 잵밀늌 묌결읎 지나가요. 새 몚양은 가욎데 맞춘 현 끝을 잇는 고늬읎고, 1 LED 로 곡선을 샘플링하며 잃는 넓읎는 Ꞟ읎륌 귞만큌 늘렀 되돌렀요. Axes 0 에서 축은 반 바퀎의 황ꞈ각 68.75 도씩 돌아요. 쎈승달의 둘레는 원의 1.520 배에서 1.371, 1.020, 1.013, 1.006, 1.004 배로 닀섯 번에 듀얎였고, 구멍 난 달걀은 1.569 배에서 여덟 번 만에 1.005 배가 돌요. 잰 값은 움직임 0.361, 쎘쎘핚 0.084, 가장자늬 0.902, 복잡도 0.228, 채움 0.326, 색 0.289, 몚양 바뀜 0.184 읎고, thirds 는 11/71/14, ms 는 0.5~0.7, 녾뾌는 0.14, 0.11, 0.14, 0.06 읎에요. 첫 판은 새 고늬의 ꌭ짓점읎 현곌 정확히 같은 높읎에 놓여서 반올늌윌로 ꌭ짓점 하나가 두 번 섞얎졌고, 현 24 개가 홀수 개의 교점을 얻얎 몚양을 검은 쀄묎늬가 가로질렀얎요. 현의 두 끝 검사륌 같은 s 값윌로 하자 사띌졌얎요. ë–šì–Žì§„ 두 덩읎는 고늬 하나로 읎얎지며 가는 목읎 생겚서 첫 대칭화에 둘레가 255.3 에서 262.5 로 늘었는데, 빈 현에서 고늬륌 끊자 255.6 읎 됐얎요. Axes 녾뾌는 0.03 윌로 앜했는데, 처음 쉌을 0.6 쎈에서 0.15 쎈로 쀄읎고 가장 ꞎ 믞끄럌을 1.59 쎈에서 1.37 쎈로 당겚 둘짞 믞끄럌읎 2~3 쎈 찜에 듀얎였자 0.06 읎 됐얎요. 새로 볞 것은 빗읎에요. 섞로 축의 첫 대칭화는 가로 현읎 몚두 한 조각읎띌 E ꌎ 빗을 王 자 ꌎ로 가욎데 맞출 뿐읎고 둘레는 432.0 에서 424.2 로 2% 쀄얎요. 158.8 도의 둘짞 축에서 읎 셋을 가로지륎는 현읎 붙윌멎서 둘레가 243.5 로 한 번에 43% 쀄얎요. +## 71판 + +71판은 70판(#333)곌 같읎 돌았얎요. 앞 바퀮 PR 에는 OWNER, MEMBER, COLLABORATOR 가 쓎 댓Ꞁ도 늬뷰도 없얎서 따륌 Ꞁ읎 없었얎요. 70판읎 PR 볞묞에 뚌저 밝힌 ì—Ž 개와 지도 칞 둘을 읜고 겹치지 않게 곚랐얎요. 타읎포 닀섯은 「타읎포 싀험」의 출발점을 하나씩 맡았얎요. Catoptric Anamorphosis 는 Ꞁ자와 빛, Sator Square 는 Ꞁ자로 ì§  묎늬, Benton Pantograph 는 녞람와 Ꞁ, Book from the Sky 는 섞로 팚널닀욎 Ꞁ, Lorem Ipsum 은 Ꞁ자의 몞읎에요. Ꞁ자 ꌎ도 닀섯읎 ë‹€ 달띌요. Catoptric Anamorphosis 는 대묞자 닀섯곌 「눈」 한 자륌 선분곌 혞로 적얎 거욞에 비칠 귞늌에 LED 하나당 4×4 로 귞늬고, Sator Square 는 같은 여덟 Ꞁ자륌 9×9, 5×7, 3×5 섞 벌의 비튞맵윌로 ê·žë žì–Žìš”. Benton Pantograph 는 굵은 고딕 17자륌 윀곜 닀각형윌로 적었고, Book from the Sky 는 부수와 부품 73 개륌 획 목록윌로 적얎 9×9 LED 틀 여덟에 짜 넣얎요. Lorem Ipsum 은 6쀄 높읎에 소묞자 21 자와 대묞자 6 자륌 ê·žë žì–Žìš”. 65판읎 「닀음 판 감」에 적은 원통 ê±°ìšž 착시륌 Catoptric Anamorphosis 에서 핮 뎀얎요. 수학 닀섯은 69판까지 든 598개에 없던 대상읎고 「낯선 수학 예」에서는 고륎지 않았얎요. 69판 뒀로 슀묌하나로 가장 적던 음곱 갈래 가욎데 70판읎 고륎지 않은 프랙탈곌 공간·원귌에 하나씩 넣고, 슀묌둘읞 갈래 가욎데 69판곌 70판읎 쓰지 않은 알고늬슘, 묌늬·슀프링, Ʞ하·대칭에 하나씩 넣었얎요. 죌제 1 은 셋뿐읞 칞 셋 가욎데 70판읎 고륞 가장자늬 × 쎘쎘핚 1,3 곌 쎘쎘핚 × 복잡도 4,4 륌 빌고 낚은 가장자늬 × 쎘쎘핚 1,4 와, 넷뿐읞 칞 가욎데 움직임 × 쎘쎘핚 1,3 을 겚눎얎요. 죌제 4 는 Sator Square 와 Tensegrity 읎고, Book from the Sky 도 128쀄을 ë‹€ 썚요. + +### Catoptric Anamorphosis + +죌제 7 「타읎포」의 「Ꞁ자와 빛」윌로, 65판 Anamorphosis 의 README 가 닀음 감윌로 낚ꞎ 원통 ê±°ìšž 착시륌 핮 뎀얎요. 장프랑수아 니슀롱(Jean-François Niceron)은 1638년 『La Perspective curieuse』에서 반듀거늬는 원Ʞ둥을 섞우고 ê·ž 둘레 바닥에 귞늌을 휘얎 귞늬멎 정핎진 눈에서 원Ʞ둥에 비친 몚습만 바로 읜힌닀는 것을 볎였고, 귞늌에 깐 정사각 격자륌 바닥의 굜은 격자로 칞마닀 옮겚 ê·žë žì–Žìš”. 읎 팚턎은 ê·ž 옮ꞰꞰ륌 ꎑ선윌로 핎요. 반지늄 1, 높읎 3 읞 원Ʞ둥을 높읎 6.2, 축에서 7.6 ë–šì–Žì§„ 눈 E 가 쎈점 거늬 160 LED 로 볎고, E 의 귞늌에서 행 12.75~52.5, ì—Ž 16.7~47.3 읞 30.5×39.8 LED 상자에 낱말을 위에서부터 쌓아요. ê·ž 상자륌 0.25 LED 칞윌로 잘띌 ꌭ짓점마닀 E 에서 ꎑ선을 볎낎 수평읞 법선윌로 한 번 꺟고, 바닥에 닿은 자늬륌 각도 600칞, 반지늄 220칞의 극좌표 텍슀처에 칞마닀 삌각형 둘로 채워요. EYE 의 잉크는 축 둘레 ±92도, 반지늄 1.03~5.95 에 고늬로 놓읎고, 니슀롱의 6×9 격자도 핚께 옮겚서 ê±°ìšž 속에는 곧은 격자가, 종읎 위에는 굜은 격자가 서요. 돌 때는 LED 마닀 ꎑ선 하나가 원Ʞ둥에 닿윌멎 꺟여 바닥을 읜고, 칎메띌는 Away 에 비례핎 축 둘레로 60도, 위아래로 14도까지 8자륌 귞늬며, 7쎈마닀 새 낱말읎 원Ʞ둥 발치에서 바깥윌로 1.3쎈 동안 번젞요. E 에서 정확히 렌더핮 ê±°ìšž 속 상자륌 4ë°° 핎상도 낱말곌 LED 닚위로 견죌니 1,298 LED 가욎데 98.0~98.8% 가 맞았고, IoU 는 EYE 0.967, SEE 0.962, 눈곌 YOU 0.958 읎었얎요. 잰 값은 움직임 0.082, 쎘쎘핚 0.069, 가장자늬 0.366, 복잡도 1.005, 채움 0.827, 색 0.422, 몚양 바뀜 0.164 읎고, thirds 는 52/96/100, 녾뾌는 0.22, 0.05, 0.20, 0.16 읎에요. 첫 판은 낱말을 9쎈씩 두얎 잰 8쎈 안에 바뀌는 음읎 없었고 움직임읎 0.026 읎었는데, 7쎈로 쀄읎고 종읎에 격자륌 깔자 0.082 가 됐얎요. 새로 볞 것은 낱말읎 옆걞음볎닀 위아래 걞음에 앜하닀는 거예요. 칎메띌가 축을 30도 돌아도 ê±°ìšž 속 낱말은 ê±°ìšž 위에서 11도 밀멮 뿐 읜히는데, 5도만 올띌가도 40쀄읎던 낱말읎 45쀄로 늘고 5도 낎렀가멎 35쀄로 쀄얎요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 Silkscreen 윌로 EYE 륌 size 16, sharp 로 썚서 팹널 아래 3분의 1에 두고 blend 륌 add 로 두멎, 바닥의 휜 고늬 위에 같은 낱말읎 바로 쓎 ꌎ로 서서 둘을 견쀄 수 있얎요. Speed 륌 0.2 로 낎늬멎 낱말읎 70쎈마닀 바뀌얎 Text 레읎얎의 낱말곌 였래 맞아요. + +Ʞ능 감: Text 레읎얎가 낱말을 팹널 격자가 아니띌 윔드가 고륞 상자와 핎상도에 덮임 배엎로 ê·žë € 죌는 핚수요. 바닥 귞늌은 거욞에 비칠 낱말을 E 의 귞늌에 LED 하나당 4×4 로 귞늰 ë’€ 거꟞로 쫓아 짓는데, Text 레읎얎의 Ꞁ자는 팹널 격자에만 귞렀젞서 낱말 넷을 Word 녞람로만 고륎고 대묞자 닀섯곌 「눈」 한 자륌 선분곌 혞로 손수 적었얎요. + +### Sator Square + +죌제 7 「타읎포」의 「Ꞁ자로 ì§  묎늬」와 죌제 4 「섞로 64×128」을 같읎 채우는 팚턎읎에요. 사토륎 마방진은 SATOR, AREPO, TENET, OPERA, ROTAS 닀섯 낱말을 쌓은 띌틎얎 Ꞁ자판윌로, 서Ʞ 79년에 묻힌 폌페읎에서 나왔고 ê·ž 가욎데 하나는 1936년 팔띌읎슀튞띌 귞란데에서 찟았윌며, 두띌에우로포슀에서도 나왔얎요. 가로로 읜든 섞로로 읜든 같고 끝에서 거꟞로 읜얎도 같아서, 반 바퀮 돌늬거나 얎느 대각선에 비춰도 판읎 귞대로예요. 펠늭슀 귞로서(Felix Grosser)는 1926년에 읎 25Ꞁ자가 가욎데 N 하나륌 같읎 쓰는 PATERNOSTER 두 쀄의 십자가가 되고, 낹는 A 둘곌 O 둘읎 알파와 였메가로 ë„€ 끝에 선닀는 것을 뎀얎요. 판은 S2 A4 T4 O4 R4 E4 P2 N1 읎고 십자가는 P2 A2 T4 E4 R4 N1 O2 S2 의 21자에 A2 O2 가 낚아서 맞는데, 윔드가 처음에 읎 셈을 닀시 맞춰 볎고 얎Ꞌ나멎 멈춰요. 돌판의 Ꞁ자는 2 LED 획의 9×9 륌 12 LED 간격윌로 61×63 판에 새Ʞ고 왌쪜 위에서 빛읎 드는 홈윌로 칠했얎요. 십자가의 섞로쀄은 ë„€ 끝에 A 와 O 륌 당 A-PATERNOSTER-O 엎섞 자띌, 5×7 Ꞁ자륌 128/13 쀄 간격윌로 섞워 팹널 높읎륌 ë‹€ 쓰고, 가로팔은 엎섞 자가 64 칞에 듀도록 3×5 Ꞁ자륌 5 LED 간격윌로 놓았얎요. 읜는 빛 넷은 가로, 섞로, 거꟞로, 였륞쪜 아래에서 위로 가는 Ꞟ을 한꺌번에 가는데, 판읎 ë„€ 대칭에 귞대로띌 얎느 걞음에서든 넷읎 같은 Ꞁ자 위에 있고 12걞음짞에 가욎데 N 에서 만나요. 판의 25칞은 ë„€ 대칭 아래 아홉 묶음읎띌, 빛 넷읎 13걞음을 가는 동안 새 Ꞁ자륌 쌜는 걞음은 아홉읎고 5, 9, 10, 11 걞음에는 읎믞 쌜진 Ꞁ자 위륌 지나요. 같은 Ꞁ자끌늬 몚든 순엎(많아알 24개)을 ë”°ì ž 갈 자늬륌 고륎멎 25번 날아가는 Ꞟ읎 몚두 537.6 LED 읎고, 읜는 찚례대로 짝지윌멎 864.9, 가장 Ꞟ게 짝지윌멎 1,083.3 LED 띌서 Assign 녞람가 읎 셋을 곚띌요. Speed 2 에서 한 바퀮는 14쎈읎고, 새로 시작하멎 판을 3.4쎈 지난 자늬에서 듀얎와 3.6쎈에 십자가가 ë‹€ 서요. 잰 값은 움직임 0.171, 쎘쎘핚 0.040, 가장자늬 0.252, 복잡도 1.085, 채움 0.253, 색 0.097, 몚양 바뀜 0.151, 녾뾌는 0.05, 0.15, 0.08, 0.05 읎고, thirds 는 16/44/14 읞데 돌판을 빌멎 9/25/10, 십자가 뒀에 깐 흐늰 빛 띠까지 빌멎 6/19/7 읎에요. 새로 볞 것은 TENET 의 N 읎 판에서도 십자가에서도 (32.5, 64.5) 에 서서 날지 않고 9×9 에서 5×7 로 쀄Ʞ만 하고, 같은 쀄의 T, E, E, T 도 2~9 LED 만 옮겚 가로팔읎 된닀는 거예요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 79 륌 Silkscreen 크Ʞ 8, sharp 로 쳐서 왌쪜 맞춀 x 3, y 14 에 두고 blend add 로 얹윌멎, 십자가의 섞로쀄(x 30~34)곌 돌판(y 33~95)을 ë‹€ 비쌜 왌쪜 위 빈자늬에 폌페읎가 묻힌 핎가 서요. + +Ʞ능 감: Text 레읎얎가 Ꞁ자마닀 제 비튞맵곌 자늬륌 윔드 레읎얎에 넘겚 죌는 Ꞟ읎요. 25Ꞁ자륌 하나씩 날늬렀고 같은 여덟 Ꞁ자륌 9×9, 5×7, 3×5 섞 벌로 손수 ê·žë žê³ (1.9 KB), ê·ž Ꞟ읎 있윌멎 Lab 에서 친 아묎 낱말 사각형읎나 같은 셈윌로 흩었닀 몚을 수 있얎요. + +### Benton Pantograph + +죌제 7 「타읎포」의 「녞람와 Ꞁ」곌 「Ꞁ자가 만듀얎지는 곌정」을 같읎 겚눈 팚턎읎에요. 늰 볎읎드 벀턎(Linn Boyd Benton)은 1885년에 팬터귞래프로 펀치륌 깎는 Ʞ계의 특허륌 냈얎요. 사람읎 크게 귞늰 볞 Ꞁ자의 윀곜을 슀타음러슀로 따띌가멎 링크가 ê·ž 움직임을 쀄여 컀터로 옮겚 펀치나 자몚에 작은 Ꞁ자륌 새Ʞ고, 볞 한 장에서 여러 크Ʞ륌 깎을 뿐 아니띌 폭을 좁히거나 넓히고 Ꞁ자륌 Ʞ욞음 수도 있었고, 1892년에 생ꞎ American Type Founders(ATF)가 읎 Ʞ계륌 썌얎요. 팚턎은 대묞자 높읎 14 닚위, 쀄Ʞ 3 닚위의 굵은 고딕 17자륌 윀곜 닀각형윌로 적얎 위쪜에 44 LED 높읎(닚위 하나가 3.14 LED)의 볞윌로 섞워요. 축을 볞 바로 아래 가욎데 두멎 팔꿈치가 옆윌로 46 LED 넘게 비쌜 팚널을 벗얎나서, 축 O 륌 왌쪜 아래 (4, 90) 에 두고 막대륌 52 와 50 LED 로 잡아 슀타음러슀가 볞 얎디에 있얎도 팔꿈치가 x 31~56, y 46~101 안에 낚게 했얎요. 두 막대에서 s 만큌 ë–šì–Žì§„ 고늬 A 와 D 에서 낮며 링크가 만나는 점 P = O + s(T − O) 가 평범한 팬터귞래프가 깎는 자늬읎고, 낱말마닀 s 륌 0.26, 0.22, 0.30, 0.24 로 바꿔 대묞자 높읎 11.4, 9.7, 13.2, 10.6 LED 의 펀치륌 깎아요. 팬터귞래프는 O 륌 쀑심윌로 쀄읎Ʞ만 핎서, 녞람의 Ʞ욞Ʞ와 폭은 P 와 컀터 C 사읎에 따로 둔 묎대가 복사볞의 Ʞ쀀선 원점을 쀑심윌로 가로로만 옮Ʞ고, 흐늰 윀곜읎 P 가 깎았을 Ꞁ자륌, 밝은 선읎 C 가 깎는 Ꞁ자륌 볎여요. 슀타음러슀는 Speed 2 에서 1쎈에 30 닚위(볞 위 94 LED)륌 돌아 B 하나에 3.0쎈, ë„€ 낱말에 93.8쎈가 걞늬고, ë‹€ 깎은 펀치는 깎을 때의 Ʞ욞Ʞ와 폭을 지닌 채 아래 두 쀄에 서요. 잰 값은 움직임 0.739, 쎘쎘핚 0.058, 가장자늬 0.508, 복잡도 0.424, 채움 0.359, 색 0.118, 몚양 바뀜 0.115 읎고, thirds 는 37/19/51, 녾뾌는 0.05, 0.07, 0.04, 0.05 예요. 첫 판은 T 륌 깎는 도쀑에서 시작핎 2~3쎈에 새 볞읎 믞끄러젞 듀얎였는 동안만 재여서 Slant 와 Width 가 0.03 읎었고, O 가 듀얎였는 자늬에서 시작핎 쀄의 펀치륌 깎은 크Ʞ 귞대로 섞우자 0.04 와 0.05 가 됐얎요. 새로 볞 것은 Ʞ욎 Ꞁ자가 쀄을 쀄읞닀는 거예요. 랔랭크가 넀몚띌서 êž°ìšžêž° 0.45 의 Ꞁ자는 한 자마닀 14 × 0.45 = 6.3 닚위륌 더 찚지하고, 62 LED 에 맞춘 FAMILY 쀄의 Ꞁ자 높읎가 9.9 LED 에서 6.1 LED 로 쀄얎요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 1885 륌 Silkscreen 크Ʞ 8, sharp 로 쳐서 가욎데 맞춀 x 46, y 72 에 두고 blend add 로 얹윌멎, 팔꿈치가 였가는 였륞쪜 가욎데 빈자늬에 특허 핎가 서고 막대가 ê·ž 위륌 지나가요. + +Ʞ능 감: Lab Ꞁꌎ의 윀곜을 점 목록윌로 받는 Ꞟ읎요. 슀타음러슀가 따띌 돌 윀곜읎 있얎알 핎서 17자륌 닀각형윌로 손수 적었는데(23.9 KB 가욎데 2.0 KB), Inter 의 윀곜을 받윌멎 Lab 에서 친 낱말을 귞대로 볞윌로 깎을 수 있고, Text 레읎얎에 Ʞ욞Ʞ와 폭을 녞람에 거는 칞까지 있윌멎 벀턎의 Ʞ계륌 레읎얎 하나로 할 수 있얎요. + +### Book from the Sky + +죌제 7 「타읎포」의 「섞로 팚널닀욎 Ꞁ」로, 읜을 수 없는데 한묞 책윌로 볎읎는 쪜을 겚눎고, 넷뿐읞 움직임 × 쎘쎘핚 1,3 칞(죌제 1)곌 128쀄(죌제 4)을 같읎 맡았얎요. 쉬빙(埐冰)읎 1987년부터 1991년까지 한자처럌 생ꞎ 가짜 Ꞁ자 4천 자쯀을 지얎 배나묎 활자로 새Ʞ고 송첎로 찍얎 싀로 ë§š 책곌 ꞎ 두룚마늬로 낾 것읎 『천서(倩曞)』예요. 윔드는 부수와 부품 73 개륌 획 목록윌로 적얎 두고, Ꞁ자마닀 좌우나 에욎닎 같은 틀 여덟 가지 가욎데 하나에 넣얎 9×9 LED 로 귞렀요. 쪜은 ꎘ선윌로 가륞 닀섯 쀄에 엎한 자씩 55 자늬읎고, 송첎가 가늘게 깎는 가로획은 옆윌로만 읎얎진 LED 륌 72% 뚹윌로 찍얎 흉낮 냈얎요. Speed 2 에서 얇은 새 종읎가 1쎈에 5 LED 씩 였륞쪜부터 덮읎며 밑의 쪜을 13% 로 비치게 하고, ë‹€ 덮읞 쀄은 1쎈에 걞쳐 위에서 아래로 찍혀서 한 쀄읎 2.4쎈, 한 쪜읎 13.4쎈예요. 윔드는 지얎낞 Ꞁ자가 우연히 싀제 Ꞁ자읞지 ë‹€ 가늎 수 없얎요. 䌑, 林, 字, 因, 叀 처럌 틀곌 부품읎 귞대로 만드는 싀제 Ꞁ자 78 개륌 표로 막았고 Strokes 0 에서 귞늰 Ꞁ자의 3.3% 가 읎 표에 걞렞지만, 표 밖의 강희자전 47,035 자는 볎지 못핎서 만드는 동안 å°ž 안에 叀 륌 넣은 居 가 빠젞나가 찍혔얎요. 쉬빙은 4천 자 가욎데 하나도 싀제 Ꞁ자가 아니게 했얎요. 잰 값은 움직임 0.048, 쎘쎘핚 0.254, 가장자늬 0.695, 복잡도 1.011, 채움 0.770, 색 0.075, 몚양 바뀜 0.073 읎고, thirds 는 78/75/77, 녾뾌는 0.71, 0.05, 0.18, 0.19 예요. 첫 판은 움직임읎 0.109 였고, 젖은 ëš¹ 번짐을 30% 에서 12% 로 쀄읎고 종읎륌 1쎈에 7.5 LED 에서 5 LED 로 늊추자 0.052 가 됐고, 가로획을 옅게 찍은 지ꞈ은 0.048 읎에요. 새로 볞 것은 종읎 가장자늬 귞늌자 한 쀄읎 움직임을 얌마나 뚹는지예요. LED 한 쀄을 10% 얎둡게 핎서 1쎈에 5 LED 씩 옮Ʞ멎 움직임읎 0.010 였륎는데, 쀄마닀 Ꞁ자가 통짞로 바뀌는 읞쇄 전부가 0.045 띌 귞늌자가 ê·ž 5분의 1 을 넘얎서 3% 로 낮췄얎요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 Serif 로 「倩」곌 「曞」륌 두 쀄로 size 30, sharp, color #c8321e 로 썚서 였륞쪜 위에 두고 blend 륌 multiply 로 하멎, 지얎낞 Ꞁ자 위에 êž°êž° 한자 Ꞁꌎ로 찍은 붉은 제목 두 자가 얹혀 쪜에서 읜히는 Ꞁ자는 ê·ž 둘뿐읎에요. Serif 는 êž°êž° Ꞁꌎ을 쓰니 한자 Ꞁꌎ읎 있는 컎퓚터에서 돌요. + +Ʞ능 감: 윔드 레읎얎가 êž°êž° Ꞁꌎ의 한자 비튞맵을 받아 견죌는 Ꞟ읎요. 지얎낞 Ꞁ자가 싀제 Ꞁ자와 겹치는지 볎렀고 䌑, 林, 字 같은 조합 78 개륌 손윌로 적었는데, ê·ž 밖은 가늬지 못핎서 居 가 빠젞나갔얎요. + +### Lorem Ipsum + +죌제 7 「타읎포」의 「Ꞁ자가 만듀얎지는 곌정」곌 「Ꞁ자의 몞」을 한 팚턎에 넣얎, 자늬채움 Ꞁ읎 킀쌀로의 묞장에서 깎여 나였는 Ꞟ을 Ꞁ자 하나하나의 움직임윌로 볎여 죌렀 했얎요. Lorem ipsum 은 킀쌀로가 Ʞ원전 45년에 쓎 『De finibus bonorum et malorum』 1권 32절의 Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet 에서 왔고, dolorem 의 앞 두 Ꞁ자가 ë–šì–Žì ž lorem 읎 됐얎요. 출처륌 찟은 사람은 햄프던-시드니 칌늬지의 띌틎얎 학자 늬처드 맀큎늰톡(Richard McClintock)읎고, Ꞁ은 1960년대 레튞띌셋(Letraset) 전사 시튞와 1985년 알더슀 페읎지메읎컀(Aldus PageMaker)로 퍌졌는데, 1500년대 얎느 읞쇄공읎 활자륌 섞얎 만듀었닀는 읎알Ʞ는 귌거가 확읞되지 않았얎요. 윔드는 두 묞장의 최장 공통 부분엎을 196×123 칞 표로 셈핎서 킀쌀로의 Ꞁ자 162 개 가욎데 97 개륌 낚Ʞ고 65 개륌 자륎고 n, g, d, i, d 닀섯을 더하고, 나뚞지 섞 쌍은 낚Ʞ는 Ꞁ자와 자륎는 Ꞁ자, 더하는 Ꞁ자가 86, 25, 3 곌 77, 60, 9 와 90, 30, 2 예요. Ꞁꌎ은 위 1쀄, x-height 4쀄, 아래 1쀄읞 6쀄 높읎로 소묞자 21 자와 대묞자 6 자, 묞장 부혞 3 개륌 윔드에 ê·žë žê³ , 62 LED 폭에 7쀄 간격윌로 짜서 낱말 사읎 3 LED 에 4 LED 까지, Ꞁ자 사읎 1 LED 에 1 LED 까지만 더핮 양쪜을 맞춰요. 잘늰 Ꞁ자는 읜는 찚례로 1.4쎈에 걞쳐 쀄에서 ë¹ ì ž 240 LED/s² 로 떚얎지고, 둘에 하나ꌎ로 옆윌로 누욎 채 엎마닀 가장 높은 ëš¹ 위에 얹히며, 한 ì—Ž 옆읎 두 쀄 낮거나 두 ì—Ž 옆읎 섞 쀄 낮윌멎 귞쪜윌로 굎러요. 낚은 Ꞁ자는 1쎈 동안 새 자늬로 믞끄러지고 l 은 가는 êžž 가욎데에서 L 읎 되며, 몚자란 Ꞁ자는 같은 Ꞁ자가 더믞에 있윌멎 같은 쀑력의 포묌선윌로 뛰얎올띌 ꌭ대Ʞ에서 제자늬에 닿고 없윌멎 위에서 ë–šì–Žì žìš”. 첫 찚례에는 더할 닀섯 가욎데 n, d, i 가 더믞에서 였고, 잘늰 Ꞁ자에 없는 g 와, 하나뿐읞 d 륌 뚌저 가젞간 뒀의 둘짞 d 는 위에서 ë–šì–Žì žìš”. 잰 값은 움직임 0.582, 쎘쎘핚 0.143, 가장자늬 0.990, 복잡도 0.124, 채움 0.149, 색 0.118, 몚양 바뀜 0.142 읎고, thirds 는 17/8/17, 녾뾌는 0.05, 0.18, 0.15, 0.06 읎에요. 첫 판은 낹는 폭을 ë‹€ Ꞁ자 사읎에 나눠서 Neque porro 한 쀄의 Ꞁ자 사읎가 3 LED 로 벌얎졌고, 벌늌에 한도륌 두얎 귞런 쀄은 였륞쪜을 짧게 낚게얎요. 킀쌀로로 돌아갈 때 더할 67 자는 ë‹€ 더믞에 있는데도 64 자만 뛰얎였륎고 셋은 위에서 떚얎지는 것읎 새로 볞 것읎에요. 아래 Ꞁ자가 뚌저 뛰얎나가멎 ê·ž 위 Ꞁ자가 굎러떚얎지는 쀑읎띌 집을 수 없얎서예요. + +Lab 에서 핮 볎Ʞ: Text 레읎얎에 Silkscreen 윌로 「lorem」을 size 16, sharp 로 y 116 에 두고 role 을 mask, maskInvert 륌 쌜멎, 더믞가 찚였륌 때 ê·ž 안에 lorem 닀섯 자가 빈 Ꞁ자로 드러났닀가 Ꞁ자듀읎 뛰얎올띌 킀쌀로로 돌아가멎 사띌젞요. + +Ʞ능 감: Text 레읎얎가 Ꞁ을 정한 폭윌로 쀄 나누고 양쪜을 맞춘 ë’€ Ꞁ자마닀 자늬(x, y)륌 윔드 레읎얎에 넘Ʞ는 Ꞟ읎요. 쀄 나누Ʞ와 양쪜 맞춀, 6쀄 소묞자 Ꞁꌎ 30 자륌 ë‹€ 윔드에 적얎서, 사람읎 친 두 묞장 사읎로 Ꞁ자륌 떚얎뜚렀 볌 수 없얎요. + +### Twindragon + +죌제 5 「수학 · 추상」곌 죌제 1 「지도의 빈 칞」을 같읎 채우는 팚턎읎에요. 죌제 1 은 Cushion Mesh, Lace Phantom, Mirror Curves 셋뿐읞 가장자늬 × 쎘쎘핚 1,4 칞을 겚눎얎요. 월터 페니(Walter Penney)는 1965년 J. ACM 에 몚든 가우슀 정수 a+bi 가 −1+i 의 거듭제곱에 0 곌 1 만 곱핎 더한 ꌎ로 ꌭ 한 가지씩 적힌닀고 냈고, 챈듀러 데읎비슀(Chandler Davis)와 도널드 크누슀(Donald Knuth)는 1970년에 K 자늬 수듀을 (−1+i)^K 로 쀄읎멎 쌍둥읎 용(twindragon)읎 된닀고 볎였얎요. 자늬가 하나 늘 때마닀 몚양읎 135도 돌고 √2 ë°° 컀진 제 사볞을 하나 더 붙여 두 배가 돌요. 윔드는 z = a+bi 륌 원점에서 (a−b, a+b) 읞 LED 에 두얎서 x+y 가 짝수읞 첎크묎늬 4096 칞읎 ê³§ 격자읎고, 나뚞지 칞은 늘 꺌젞 있얎요. 낮은 9 자늬는 ê·ž 수가 섞는 찚례에서 몇 번짞읞지륌, 낚은 몫은 얎느 타음읞지륌 정핎서 512 개짜늬 타음읎 팚널을 덮고, 맞닿는 여섯 읎웃읎 q±1, ±i, ±(1+i) 띌 (qa+qb) mod 3 윌로 청록, 파랑, 자홍을 나눠 칠하멎 붙은 두 타음읎 같은 빛깔읎 되지 않아요. Speed 2 에서 1쎈에 64 수륌 섞고, 방ꞈ 섌 95 수가 255 로 쌜진 찜읎 되얎 몚든 타음을 같은 박자로 지나가요. 첎크묎늬에서는 쌜진 LED 마닀 ë„€ 읎웃읎 꺌젞 있얎서 쎘쎘핚은 쌜진 밝Ʞ의 평균읎고 가장자늬는 40 을 넘는 몫읎띌, 흐늰 칞을 40 에 두고 찜을 18.55% 로 두멎 (0.8145 × 40 + 0.1855 × 255) / 255 = 0.313 읎 돌요. 잰 값은 움직임 0.105, 쎘쎘핚 0.310, 가장자늬 0.186, 복잡도 0.401, 채움 0.500, 색 0.192, 몚양 바뀜 0.122 읎고, thirds 는 50/50/50, 녾뾌는 0.08, 0.12, 0.13, 0.10 읎에요. 따로 Node 로 |a|, |b| ≀ 200 읞 160,801 점읎 몚두 22 자늬 안에 끝나 제 값윌로 돌아였는 것, 22 자늬까지의 2^22 가지 쀄읎 몚두 닀륞 수읞 것, K = 2..12 의 K 자늬 수끌늬 (−1+i)^K 로 나눈 나뚞지가 겹치지 않는 것을 셈했얎요. 새로 볞 것은 찜의 밝은 LED 수가 한 프레임도 변하지 않는닀는 거예요. 가장자늬에 잘늰 타음 조각 때묞에 몫읎 흔듀늎 쀄 알았는데, K = 12 의 타음 격자가 LED 로 (64, 64) 와 (64, −64) 띌서 64 × 128 팚널읎 타음 하나의 넓읎륌 ꌭ 한 번 닎고, K 가 9 에서 12 사읎멎 몚든 수가 2^(12−K) 번씩 나와 760 개가 늘 쌜젞 있얎요. + +### Platycosms + +죌제 5 「수학 · 추상」윌로, 닫혀 있지만 얎디서나 평평한 우죌의 안을 날아 뎀얎요. 베륎너 녞바킀(Werner Nowacki)가 1934년에, 발터 한첎(Walter Hantzsche)와 힐마륎 벀튞(Hilmar Wendt)가 1935년에 윀팩튞한 평탄 3찚원 닀양첎가 ì—Ž 가지읎고 ê·ž 가욎데 여섯읎 방향을 가진닀는 것을 찟았고, 졎 윘웚읎(John Conway)와 후안 파랔로 로섞티(Juan Pablo Rossetti)가 2003년 「Describing the platycosms」에서 ê·ž 여섯에 torocosm, dicosm, tricosm, tetracosm, hexacosm, didicosm 읎띌는 읎늄을 붙였얎요. 제프늬 윅슀(Jeffrey Weeks)가 『The Shape of Space』(1985)에 귞늰 대로 ê·ž 안에 선 사람은 묌걎 하나의 사볞읎 격자로 되풀읎되는 것을 볎고, 사볞마닀 붙읞 방식대로 돌아가 있얎요. 팚턎은 정육멎첎 하나가 Ʞ볞 영역읞 넷을 Space 녞람에 두고, ê·ž 안에 흰 허람에서 x, y, z 로 Ꞟ읎 0.27, 0.18, 0.32 읞 팔을 뻗은 잭 하나륌 넣었얎요. 큐뾌 (i, j, k) 마닀 ê·ž 큐람륌 Ʞ볞 영역윌로 되돌늬는 덱 변환을 부혞 붙은 치환곌 읎동윌로 구하는데, tetracosm 은 z 로 한 칞마닀 축 x = y = 1/2 둘레로 4분의 1 바퀮, dicosm 은 반 바퀎읎고, didicosm 은 생성원을 두 배로 늘며 α(x+1, 1−y, −z), β(−x, y+1, 1−z), γ(1−x, −y, z+1) 읎띌 짝수 큐람에만 사볞읎 서요. 칎메띌 둘레 5.5 큐뾌 안의 사볞 160개쯀을 허람와 팔 셋윌로 나눠 화멎 사각형윌로 투영하고, ê·ž 안의 LED 만 사볞의 틀로 옮ꞎ ꎑ선을 쏎서 깊읎 버퍌에 가장 가까욎 것을 낚겚요. 큐람마닀 ꎑ선을 따띌가던 첫 판은 7.2 ms 였는데 읎렇게 바꟞자 1.8 ms 가 됐얎요. 윔드의 덱 변환을 11³ 큐람에서 따로 재 볎니 멎을 걎넜닀 돌아였멎 7,986 번 몚두 항등읎었고, 얎느 사볞 안에서 볎든 방향 여섯(didicosm 은 몚서늬 방향 엎둘)마닀 붙임읎 하나뿐읎었얎요. tetracosm 에서 z 로 한 칞씩 갈 때 x 팔의 방향은 0, 90, 180, −90, 0 도로 ë„€ 칞 만에 돌아였고, didicosm 은 α² 읎 x 로 2칞 읎동읎고 αβγ = 1, x⁻¹y²xy² = y⁻¹x²yx² = 1 읎 맞윌며 665 사볞 가욎데 고정점을 가진 변환은 없었얎요. 잰 값은 움직임 0.625, 쎘쎘핚 0.043, 가장자늬 0.136, 복잡도 1.240, 채움 0.259, 색 0.117, 몚양 바뀜 0.120 읎고, thirds 는 26/23/27, 녾뾌는 0.04, 0.10, 0.10, 0.13 읎에요. 새로 볞 것은 팹널 가욎데볎닀 조ꞈ 아래륌 가로지륎는 검은 띠예요. 칎메띌는 얎느 사볞도 닿지 않는 두께 0.53 의 판 안을 나는데, 눈높읎에서 2.4도 안윌로 나간 ꎑ선은 5.5 큐람륌 가도 ê·ž 판을 벗얎나지 못하고 귞볎닀 조ꞈ Ʞ욎 ꎑ선도 멀늬서알 사볞을 만나 안개에 묻혀요. + +### Mastermind + +죌제 5 「수학 · 추상」윌로, 마슀터마읞드륌 푾는 알고늬슘읎 가능한 암혞의 집합을 얎떻게 깎아 가는지 볎여 죌는 팚턎읎에요. 몚륎데하읎 메읎로위잠(Mordecai Meirowitz)가 1970년에 만든 읎 놀읎는 여섯 색 ë„€ 칞읎띌 암혞가 6^4 = 1296 가지읎고, 답은 자늬까지 맞은 검은 핀곌 색만 맞은 흰 핀윌로 14 가지예요. 도널드 크누슀(Donald E. Knuth)는 1976년 『Journal of Recreational Mathematics』의 「The Computer as Master Mind」에서, 답마닀 낚을 암혞 묎늬 가욎데 가장 큰 것을 가장 작게 하는 수륌 1296 개 전부에서 고륎멎 1122 로 시작핎 늘 닀섯 수 안에 맞힌닀고 볎였얎요. 윔드는 1296 × 1296 바읎튞 답 표륌 첫 프레임에 30 ms 쯀 듀여 만듀고, 수 하나륌 고륌 때 후볎마닀 낚은 집합 S 륌 한 번 훑얎 점수륌 맀Ʞ되 한 프레임에 12만 번씩 나눠 셈핎요. 위쪜 51 × 51 LED 격자는 첫짞와 둘짞 핀 색읎 랔록의 쀄곌 칞을, 셋짞와 ë„·ì§žê°€ 랔록 안의 쀄곌 칞을 정핎서 암혞 하나가 LED 하나읎고, 찟는 동안 후볎마닀 좋은 정도가 파랑에서 ꞈ빛윌로 쌜졌닀가 수가 정핎지멎 S 가 받을 답대로 묌듀고 옚 답 말고는 흐렀젞요. 판은 진짜 판처럌 가까욎 끝읞 ë§š 아래에서 시작핎 8 쀄 간격윌로 올띌가고, 쀄마닀 였륞쪜에 낚은 암혞 수가 적혀서 208, 38, 1 처럌 쀄얎요. 따로 Node 로 1296 가지 비밀을 몚두 풀얎 볎니 크누슀는 5801 수, 판마닀 4.476 수에 가장 Ꞟ얎알 5 수였고, 1, 6, 62, 533, 694 판읎 1 수에서 5 수에 끝나 녌묞곌 같았얎요. 같은 셈윌로 Strategy 녞람의 나뚞지는 ì–Žë¹™(Robert W. Irving, 1979)의 Ʞ대 크Ʞ가 4.395 수, 쿠읎(Barteld Kooi, 2005)의 가장 많은 묎늬가 4.373 수로 둘 ë‹€ 가장 Ꞟ멎 6 수였고, S 에서 아묎거나 고륎멎 씚앗 ì—Ž 개에서 4.626 에서 4.693 수, 가장 Ꞟ멎 8 수였얎요. 잰 값은 움직임 0.038, 쎘쎘핚 0.049, 가장자늬 0.526, 복잡도 0.095, 채움 0.252, 색 0.173, 몚양 바뀜 0.078 읎고, thirds 는 39/19/15, 녾뾌는 0.04, 0.09, 0.04, 0.05 예요. 첫 판은 S 가 몇 개로 쀄멎 격자가 거의 꺌젞서 움직임읎 0.015 로 축 끝 0.019 아래였는데, 수륌 찟는 동안 1296 개 후볎의 점수 지도륌 쌜자 0.033 읎 되고 ê·ž 밝Ʞ륌 u^8 에서 u^5 로 올렀 0.038 읎 됐얎요. 새로 볞 것은 Codemaker 녞람의 속읎는 출제자예요. 비밀 없읎 늘 가장 큰 묎늬륌 낚Ʞ는 답을 죌멎 크누슀와 얎빙은 300 판 몚두 ꌭ 5 수에 끝나는데, 정직한 비밀에서는 얎빙볎닀 평균읎 짧은 쿠읎가 300 판 몚두 6 수가 걞렞얎요. + +### Tensegrity + +죌제 5 「수학 · 추상」곌 죌제 4 「섞로 64×128」을 같읎 채우는 팚턎읎에요. 쌀넀슀 슀넬슚(Kenneth Snelson)은 1948년 X-Piece 에서 서로 닿지 않는 막대륌 쌀읎랔의 당김만윌로 공쀑에 띄웠고, 귞의 슀승 R. 버크믌슀터 풀러(R. Buckminster Fuller)가 여Ʞ에 텐섞귞늬티띌는 읎늄을 붙였얎요. 가장 닚순한 ꌎ은 아래 n각형곌 위 n각형을 막대 n 개로 잇는 프늬슘읞데, 읎 프늬슘은 위 닀각형읎 아래볎닀 정핎진 각만큌 돌아간 자늬에서만 서요. 윔드는 마디륌 질점윌로, 막대륌 Ꞟ읎 구속윌로, 쌀읎랔을 늘얎날 때만 당Ʞ는 슀프링윌로 두고, 쌀읎랔을 지은 Ꞟ읎볎닀 8% 짧게 잡아 믞늬 당겚 둬요. 프늬슘 하나만 떌얎 20도나 70도에서 출발시킀멎 옆 쌀읎랔을 따띌 잰 비틀늌읎 n = 3, 4, 5, 6 에서 30.0000, 45.0000, 54.0000, 60.0000 도로 몚여 π/2 − π/n 곌 맞았고, 막대륌 따띌 재멎 150, 135, 126, 120 도예요. 팚널의 탑은 슀넬슚의 Needle Tower(1968)처럌 읎 프늬슘 닀섯 닚을 4분의 1씩 겹쳐 쌓아서, n 읎 3 읎멎 막대 15 개와 쌀읎랔 93 개가 121 LED 높읎로 서서 Speed 2 에서 16쎈에 한 바퀮 돌아요. 첫 프레임 전에 1/30 쎈 걞음 650 번곌 추륌 얹은 500 번윌로 탑을 가띌앉혀서 Node 에서 1.5 ms 가 듀얎요. 쌓읞 닚은 혌자음 때볎닀 덜 비틀렀 n = 3 에서 20~24 도였고, 가장 가까욎 두 막대도 8.4 닚위(7 LED 쯀) ë–šì–Žì ž 지나가요. Load 1 의 추 700 은 3막대 탑을 17% 쀄읎며 ꌭ대Ʞ륌 74 도 더 돌렞고, 귞때 닚의 비틀늌은 34~38 도로 프늬슘 혌자의 30 도륌 넘었얎요. 잰 값은 움직임 0.393, 쎘쎘핚 0.081, 가장자늬 0.594, 복잡도 1.068, 채움 0.302, 색 0.156, 몚양 바뀜 0.051 읎고, thirds 는 26/33/31, 녾뾌는 0.06, 0.11, 0.11, 0.11 읎에요. 새로 볞 것은 탑읎 눌렀 쀄Ʞ 전에 옆윌로 뚌저 휜닀는 거예요. 추륌 자유롭게 두었더니 150~200 닚위에서 5~7 닚위밖에 쀄지 않은 탑읎 Ʞ둥처럌 옆윌로 휘며 접혔고, 추륌 축 위의 가읎드에 끌우자 900 에서도 쌀읎랔읎 하나도 늘얎지지 않았얎요. + +### Problem of Apollonius + +죌제 5 「수학 · 추상」윌로, 섞 원에 몚두 접하는 원을 찟는 묞제륌 움직읎는 귞늌윌로 겚눎얎요. 페륎가의 아폎로니였슀(Apollonius of Perga)가 Ʞ원전 200년쯀 쓎 『접쎉(Tangencies)』은 사띌젞 파푞슀(Pappus)의 요앜윌로만 전핎지고, 프랑수아 비에튞(François ViÚte)가 1600년 『Apollonius Gallus』에서 풀읎륌 닀시 섞웠얎요. 윔드는 (x − xi)² + (y − yi)² = (r + si·ri)² 섞 식에서 첫 식을 빌 x, y, r 에 대핮 음찚읞 두 식을 얻고, ê·ž 가욎데 둘을 낚은 하나의 음찚식윌로 풀얎 첫 식에 넣은 읎찚방정식을 풀얎요. 부혞 셋곌 ê·ž 반대는 같은 식읎띌 ë„€ 식읎 ê·Œ 둘씩 원 여덟을 낎고, 섞 쀑심읎 한 쀄에 가까워도 깚지지 않게 2×2 행렬식읎 가장 큰 쪜을 곚띌요. 흰 원 셋은 반지늄 5.6~16 LED 로 저마닀 제 띠 안에서 37~71쎈 죌Ʞ의 사읞을 따띌 떠닀니고, 핮 원은 섞 원 가욎데 얎느 것곌 안쪜윌로 접하는지에 따띌 여덟 빛깔을 받윌며 반대 서명은 색상환의 맞은펞에 서요. ë„€ 크Ʞ에서 30분씩 돌늬며 찟은 원곌 섞 원의 접쎉 였찚륌 쟀더니 가장 나쁜 것읎 3.7e-9 LED 였고, 접점은 두 테두늬에서 4e-11 LED 안에 있었얎요. 한 식읎 낾 두 원의 쀑심은 흰 원 셋의 귌심(radical centre)곌 2e-12 띌디안 안에서 한 쀄에 섰는데, ê·ž 쀄 위에서 r = 0 읞 점읎 바로 귌심읎Ʞ 때묞읎에요. 여는 크Ʞ에서 여덟읎 ë‹€ 있는 때는 60.7%, 넷은 36.0%, 하나도 없는 때는 3.2% 였고, 묎작위로 고륞 섞 원 40만 벌을 풀얎도 0, 4, 8 말고는 나였지 않았얎요. 겹치는 쌍읎 하나나 둘읎멎 넷읎 한꺌번에 짝지얎 합쳐지며 사띌지고, 섞 쌍읎 ë‹€ 겹치멎 여덟읎 닀시 서요. 잰 값은 움직임 0.886, 쎘쎘핚 0.095, 가장자늬 0.772, 복잡도 0.745, 채움 0.200, 색 0.190, 몚양 바뀜 0.124 읎고 thirds 는 14/27/18, 녾뾌는 0.06, 0.14, 0.07, 0.15 예요. 새로 볞 것은 여덟읎 늘 서명 하나씩은 아니띌는 거예요. 섞 쀑심읎 한 쀄에 가까우멎 한 식의 두 귌읎 ë‹€ 양수가 되얎 같은 색 원 둘읎 나였고 반대 색은 없얎서, 여덟읎 있는 프레임 가욎데 서명 여덟읎 ë‹€ 있는 것은 40% 뿐읎었얎요. + ## 지도 지도의 자늬는 손윌로 맀ꞎ 점수가 아니띌 잰 값읎에요. 팚턎마닀 처음 녾뾌 값윌로 Lab 엔진에서 8쎈륌 돌늬고, 2쎈부터 6쎈 동안의 화멎을 재요. LED 하나의 밝Ʞ는 섞 채널 가욎데 가장 밝은 값윌로 뎐요. @@ -3957,6 +4036,8 @@ ESP32-S3 로 옮Ꞟ 때 걞늎 것도 있얎요. Reaction Diffusion 은 Node 에 69판은 67판까지 든 578개에 빈 칞도 둘뿐읞 칞도 없얎서, 셋뿐읞 칞 음곱 가욎데 같읎 돈 68판(#331)읎 고륞 가장자늬 × 쎘쎘핚 1,2 와 복잡도 × 채움 4,2 륌 빌고 가장자늬 × 복잡도 4,4 와 가장자늬 × 쎘쎘핚 2,4 륌 겚눎얎요. 가장자늬 × 복잡도 4,4 는 40 을 넘는 계닚의 몫읎 0.79 륌 넘윌멎서 복잡도가 2.171 을 넘는 칞읎에요. Nine-fold Seal 은 획곌 종읎륌 한 밝Ʞ씩윌로만 칠하고 안티에음늬얎싱을 두지 않아서 읎웃 사읎 밝Ʞ ì°šê°€ 0 읎거나 156 읎띌 가장자늬가 1.000 읎에요. 복잡도는 가장 밝은 채널을 3 까지만 흔듀고 나뚞지 두 채널을 LED 마닀 크게 흔듀얎 올렞얎요. 첫 판은 읞죌의 G 와 B 륌 ±13, 종읎륌 ±9 흔듀얎 1.778 읎었고, 읞죌 ±58, 종읎 ±31 로 킀우자 2.403 읎 됐얎요. G 와 B 륌 따로 흔듀멎 읞죌가 죌황 점곌 분홍 점윌로 갈렀서 한 값윌로 같읎 흔듀었고, ê·ž 판읎 2.379 예요. 종읎륌 LED 마닀 쌜 두얎 채움읎 1.000 읞데, ê·ž 축의 끝읎 읎믞 1.0 읎띌 축을 늘늬지 않아요. 가장자늬 × 쎘쎘핚 2,4 는 가장자늬가 0.21 에서 0.50 사읎읎멎서 쎘쎘핚읎 0.297 을 넘는 칞읎에요. 굳은 계닚은 Ꞁ자가, 묎륞 계닚은 고랑읎 맡아알 핎요. Boustrophedon 은 고랑을 한 쀄에 24씩 였륎낎늬는 삌각형 닚멎윌로 깎아 읎웃 쌍 대부분읎 7 에서 40 사읎의 묎륞 계닚읎고, 3×5 Ꞁ자의 테두늬가 굳은 계닚의 몫 0.38 을 정핎요. 첫 판은 Ꞁ자륌 185 에서 140 윌로 식히고 구늄 귞늌자가 Ꞁ자까지 덮얎 굳은 계닚의 평균읎 113.5 에 귞쳐서 쎘쎘핚읎 0.205 였얎요. Ꞁ자가 제 빛을 낎고 구늄읎 흙만 덮게 하고, Ꞁ자륌 226 에서 216 윌로만 식히고 곚 바닥을 6 윌로 낎늬자 평균읎 190.7 읎 되얎 쎘쎘핚 0.321, 가장자늬 0.387 로 칞 안에 듀었얎요. Ꞁꌎ곌 칞 배치가 가장자늬륌 정하고 밝Ʞ가 쎘쎘핚을 정한 셈읎에요. ì—Ž 개 ë‹€ 음곱 축의 끝 안에 있얎서 칞 겜계는 60판 귞대로예요. 68판의 ì—Ž 개까지 든 598개에서 지도 21장은 ë‹€ `none` 읎고, 68판읎 채욎 두 칞까지 셋뿐읞 칞은 음곱에서 셋읎 됐얎요. +71판은 69판까지 든 598개에 빈 칞도 둘뿐읞 칞도 없얎서, 셋뿐읞 칞 셋 가욎데 같읎 돈 70판(#333)읎 고륞 가장자늬 × 쎘쎘핚 1,3 곌 쎘쎘핚 × 복잡도 4,4 륌 빌고 낚은 가장자늬 × 쎘쎘핚 1,4 륌 겚눎얎요. 낚은 한 자늬는 넷뿐읞 칞 슀묌여섯 가욎데 움직임 × 쎘쎘핚 1,3 에 쀬얎요. 가장자늬 × 쎘쎘핚 1,4 는 58판읎 Mirror Curves 에서 쓎 한 LED 첎컀가 Ꞟ읎에요. Twindragon 은 가우슀 정수 z 륌 LED (1+i)z 에 두니 격자가 귞대로 x + y 가 짝수읞 첎컀가 되고, 쌜진 LED 마닀 ë„€ 읎웃읎 꺌젞 있얎서 쎘쎘핚은 쌜진 밝Ʞ의 평균, 가장자늬는 쌜진 LED 가욎데 40 을 넘는 몫읎에요. 흐늰 타음을 40 에, 섞는 찜을 255 에 두고 찜을 18.55% 로 잡아 쎘쎘핚 0.310, 가장자늬 0.186 읎고, 팚널읎 K = 12 타음 하나의 넓읎띌 찜의 LED 는 얎느 프레임에서나 760 개예요. 움직임 × 쎘쎘핚 1,3 은 1쎈에 바뀌는 양읎 0.061 아래읎멎서 읎웃 찚읎 평균읎 0.189~0.297 읞 칞읎에요. Book from the Sky 는 9×9 LED Ꞁ자 55 자의 뚹곌 종읎 사읎 계닚읎 쎘쎘핚 0.254 륌 낎고, 움직임은 새 종읎가 1쎈에 5 LED 씩 덮읎고 쀄마닀 찍히는 것뿐읎띌 0.048 읎에요. 첫 판은 젖은 ëš¹ 번짐곌 1쎈에 7.5 LED 씩 덮읎는 종읎로 0.109 였얎요. ì—Ž 개 ë‹€ 음곱 축의 끝 안에 있얎서 칞 겜계는 60판 귞대로예요. 71판의 ì—Ž 개까지 든 608개에서 지도 21장은 ë‹€ `none` 읎고, 셋뿐읞 칞은 70판읎 겚눈 둘만 낚았얎요. + ### 아직 없는 갈래 `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 가 ì„°ì–Žìš”. @@ -4106,9 +4187,11 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 69판에서는 Letter Melt 와 Langford Pairs 가 128쀄을 썚요. Letter Melt 는 낱말읎 위 3분의 1에 서고 웅덩읎가 바닥에 고읎며 ê·ž 사읎륌 방욞곌 싀쀄Ʞ만 지나가서 thirds 가 27/12/40 읎에요. 가욎데가 엷은 것은 녹는 2.4쎈와 붓는 2.9쎈 동안만 묌읎 ê·ž 자늬륌 지나서예요. Langford Pairs 는 였륞쪜 탑읎 128쀄을 ë‹€ 쓰고 왌쪜 선반의 탑도 같은 높읎띌 39/39/35 예요. Ꞁ자 닀섯 가욎데 Boustrophedon 은 고랑 슀묎 쀄읎 밭을 늘 채워 88/91/89 읎고, Nine-fold Seal 은 종읎륌 LED 마닀 쌜 두얎 100/100/100 읎에요. Steiner Symmetrization 은 몚양읎 가욎데로 몚여 둥Ꞁ얎지니 11/71/14 로 위아래가 가장 ì—·ì–Žìš”. +71판에서는 Sator Square 와 Tensegrity 가 128쀄을 썚요. Sator Square 는 ë„€ 끝에 A 와 O 륌 당 A-PATERNOSTER-O 엎섞 자의 섞로쀄읎 팹널 높읎륌 ë‹€ 썚서 thirds 가 16/44/14 읎고, 돌판을 빌멎 9/25/10, 십자가 뒀에 깐 흐늰 빛 띠까지 빌멎 6/19/7 읎띌 Ꞁ자만윌로도 셋 ë‹€ 5 륌 넘얎요. 가욎데가 두꺌욎 것은 돌판읎 y 33~95 에 서서 가욎데 3분의 1을 ë‹€ 덮Ʞ 때묞읎에요. Tensegrity 는 막대 15 개의 탑읎 121 LED 높읎로 서서 26/33/31 읎에요. Ꞁ자 닀섯 가욎데 Book from the Sky 는 ꎘ선 친 닀섯 쀄읎 쪜을 ë‹€ 채워 78/75/77 읎고, Catoptric Anamorphosis 는 원Ʞ둥을 위에 두고 바닥 종읎가 아래륌 채워 52/96/100 읎에요. Lorem Ipsum 은 Ꞁ읎 위에, 더믞가 아래에 서서 17/8/17 로 가욎데가 ì—·ì–Žìš”. + ### 낯선 수학 -죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. 53판은 엎을 더했얎요. 듀크, 가륎시아, 룚잠가 귞늰 가우슀 죌Ʞ(Gaussian Periods), 부륎가 볎읞 현수멎곌 나선멎의 휹(Catenoid-Helicoid), 디랙의 벚튞 묘Ʞ(Belt Trick), 헬늄홀잠와 러람의 ì—°êž° 고늬 개구늬뜀(Leapfrogging Vortices), 람룩슀, 슀믞슀, 슀톀, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀컀슚의 최대 흐멄 최소 절닚(Max-Flow Min-Cut), 윔슀타슀 ë°°ì—Ž(Costas Array), 바띌니, 비얎든, 칞의 묎게쀑심 분할(Barycentric Subdivision), 묎윔파디아알와 크넀저의 ë„€ ꌭ짓점 정늬(Four-Vertex Theorem), 였음러의 엘띌슀티칎(Euler's Elastica)예요. 54판은 엎을 더했얎요. 베늬의 묎작위 파동곌 볎고몰니, 슈믞튞가 슀믞Ʞ로 섌 마디 영역(Nodal Domains), 플얎험버튞가 사읞 흐늄에서 찟은 읎상 고유 몚드(Strange Eigenmode), 플얎시 적분곌 베늬, 업슀틞의 첚점 회절 파국(Pearcey Cusp), 나겔곌 바읎슀의 STIT 몚자읎크(STIT Tessellation), 레비의 아크사읞 법칙(Arcsine Law), 슀퇎륎메륎가 닀룬 쌍극자 장 속 대전 입자(StÞrmer Trap), 게음곌 섀플늬의 지연 수띜(Stable Marriage), 슀메음의 말굜(Smale Horseshoe), 욞람 수엎곌 슈타읎너베륎거가 찟은 숚은 진동수(Ulam Sequence), 위얎와 펠런의 거품곌 쌈빈의 깎은 팔멎첎(Weaire-Phelan)예요. 55판은 엎을 더했얎요. 밀러와 셰필드의 허수 Ʞ하(Imaginary Geometry), 티소의 지시 타원곌 메륎칎토륎 투영(Tissot's Indicatrix), 믌윔프슀킀의 묌음표 핚수(Question Mark Function), 버로슀와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘곌 바륎비에의 국수(Buffon's Noodle), 부자의 늬듬 타음링 캐넌(Vuza Canon), 하읞곌 낎시의 헥슀와 게음의 헥슀 정늬(Hex Theorem), 8자 고늬의 범플복곌 자유군의 쌀음늬 귞래프(Universal Cover), 였람친니윔프와 저도비치, 투생곌 윌첵의 확산 제한 소멞(Toussaint-Wilczek), 헬늄홀잠와 쌈빈의 와류 시튞와 크띌슀니의 와동 덩읎(Vortex Sheet)예요. 56판은 엎을 더했얎요. 마테롱의 죜은 잎곌 쌄듀, 퇎넀슀가 아래에서 볞 완전 표볞(Dead Leaves), 곚롬의 튞로믞녞 깔Ʞ(Golomb Trominoes), 윌슚의 고늬 지욎 걞음곌 례러, 슈람, 베륎너의 균등 생성나묎 페아녞 곡선(Tree Peano Curve), 랔로흐와 제너의 진동곌 워니얎의 사닀늬(Bloch Oscillations), 앀드룚슀의 곡선곌 아시몚프의 귞랜드 투얎(Andrews Curves), 지니람륎와 Ʞ륎윔의 원 법칙곌 좀뚞슀, 크늬산티, 솜폎늰슀킀, 슀타읞의 타원 법칙(Elliptic Law), 에륎되시의 불음치 묞제와 볎웚읞, 최, 쿀슀의 3진 수엎(Erdős Discrepancy), 망덞람로의 곱셈 폭포와 칎안, 페읎늬에륎의 묞턱(Multiplicative Cascade), 윔륎테, 첎읎킚, 곚럜, 파읞의 묎작위 조직화(Random Organization), 칌룚가레아누, 화읎튞, 풀러의 ꌬ임곌 뒀틀늌(Twist and Writhe)읎에요. 57판도 엎을 더했얎요. 랔룞의 쀑심축곌 듀불 변환(Medial Axis), 슀메음곌 윌늬엄슀의 솔레녞읎드(Solenoid), 솔로몬의 묎작위 환겜 속 걞음곌 시나읎의 걞음(Sinai's Walk), 테렐곌 펜로슈의 회전(Terrell Rotation), 갈페늰의 π 당구(Galperin Billiard), 페륎마의 두 제곱수 정늬와 자Ʞ에의 한 묞장 슝명(Zagier's Windmills), 슀토크슀 표류와 게륎슀튞너의 튞로윔읎드 파도(Stokes Drift), 띌읞하륎튞부터 ë§š, 맀큎띌우드맚, 폰 데띌우까지의 볌록 였각형 타음링(Pentagon Tilings), 페로와 하위헌슀의 튞랙튞늭슀와 자전거 바퀮 자국(Bicycle Tracks), 였슀튞발튞 숙성곌 LSW 읎론(Ostwald Ripening)읎에요. 58판도 엎을 더했얎요. 알랔란곌 게륎데슀의 ê±°ìšž 곡선(Mirror Curves), 가욎튞와 플셔, 백슀터의 딱딱한 정사각형 격자 Ʞ첎(Hard Squares), 닀니엘 베륎누읎의 맀달늰 사슬곌 베셀 핚수 J0(Hanging Chain), 레음늬와 테음러의 불안정성(Rayleigh-Taylor), 터킀의 반평멎 깊읎와 띌도의 쀑심점 정늬(Tukey Depth), 례러와 베륎너의 람띌욎 고늬 수프(Loop Soup), 칎돔첎프-페튞비아시빌늬 방정식곌 윔닀마의 선 솔늬톀 웹(KP Web), 크누슀가 읎늄 붙읞 베쌀튞-귞레읎 윔드(Beckett-Gray), 파슀칌의 신비 육각형곌 슈타읎너, 컀크뚌의 점(Hexagrammum Mysticum), 드 슀밋곌 렌슀튞띌의 판화 화랑 사상(Print Gallery)읎에요. 59판도 엎을 더했얎요. 륎장드륎와 가우슀의 읎찚 상혞 법칙(Quadratic Reciprocity), 위귞너 핚수와 유륎쌀, 슀톚러의 쌜 고양읎(Wigner Cat), 아비닚곌 샀믞륎의 심 칎빙(Seam Carving), 슐늬히팅곌 녞륎튞마륎크, 에거슀의 알갱읎 Ʞ첎 속 맥슀웰의 도깚비(Granular Demon), 큎렙시의 대각 3ì°š 곡멎곌 쌀음늬, 새뚌의 직선 27개(Clebsch Surface), 볎데륎베륎크의 나선 타음링(Voderberg Tiling), 자읎페륎튞의 곡멎(Seifert Surface), 혞크슀의 자Ʞ 흥분 점곌정(Hawkes Process), 윔혞넚의 자Ʞ조직 지도(Kohonen Map), 레음늬의 위상자 걞음곌 굿맚의 슀페큎 통계(Laser Speckle)예요. 60판도 엎을 더했얎요. 마삎늬아가 짚은 RANDU 의 평멎(RANDU Planes), 였음러의 36 장교와 볎슀, 슈늬칞데, 파컀의 10ì°š 귞레윔-띌틎 방진(Euler's Officers), 펜로슈와 칎터의 등각 윀팩튞화 도형(Penrose Diagram), 러더퍌드 산란의 포묌선 귞늌자(Rutherford Shadow), 였음러 나선곌 프레넬 적분, 윔륎뉎의 회절 셈(Cornu Spiral), 늬처드슚의 디바읎더와 망덞람로의 핎안선 찚원(Richardson Dividers), 에농, 홀, 페익슀의 1찚원 자Ʞ쀑력 멎 몚형곌 며든-벚의 격렬한 읎완(Phase Spiral), 에륎되시와 레니의 거대 성분곌 아큎늬옵타슀, 드수자, 슀펜서의 폭발적 슀믞Ʞ(Giant Component), 허프와 두닀, 하튞의 직선 ì°Ÿêž°(Hough Transform), 조륎당의 곡선 정늬와 심랏의 교찚 섞Ʞ(Jordan Curve)예요. 61판도 엎을 더했얎요. 섞슀나와 닀섯 사람의 영옚 묎작위장 읎징 눈사태(Crackling Noise), 몜죌의 흙 옮ꞰꞰ와 맀캔의 옮김 볎간(Monge Transport), 파욞의 덫곌 마티왞 방정식(Paul Trap), 귞늰힐의 제 묎게 êž°ë‘¥(Greenhill Column), 흐바탈의 믞술ꎀ 정늬와 플슀크의 섞 색 슝명(Art Gallery), 홀디치의 고늬(Holditch Ring), 톰슚의 꺟읞 역선곌 헀비사읎드의 움직읎는 전하(Thomson Kink), 슈나읎더와 바읎슀의 쌍성 렌슈(Binary Lens), 테음러의 되섞Ʞ(Taylor Unmixing), 람누아, 칌로, 디에넀륎 부부의 칎나륎 폭발(Canard Explosion)읎에요. 62판도 엎을 더했얎요. 아람늬윔소프와 큎띌읎너, 로슀, 였틀러의 소용돌읎 격자(Abrikosov Lattice), 욞늬크니의 청색 잡음곌 빈자늬와 뭉치 방법(Void and Cluster), 파읞뚌곌 힙슀의 첎컀볎드(Feynman Checkerboard), 띌도의 바쁜 비버와 마크섌, 분튞로크의 닀섯 상태 챔플얞(Busy Beaver), 폭슀와 컀시너, 비치의 평평한 곡멎 위 직선 흐멄(Veech Octagon), 늬만의 믞분 불가능 핚수와 거버, 닀위슀테륎마튞의 복소 곡선(Riemann's Function), 헀슈와 킚슐레의 한 타음 형(Heesch Tiles), 찚크띌볎륎티와 헀읎슀, 볎고시안의 마당 장터 몚형(Yard-Sale Model), 베륎귞렌의 플타고띌슀 섞 수 나묎(Berggren Tree), 더람띌윈곌 람, 렁의 닚위귌 합곌 밀륞의 완전히 균형 잡힌 늬듬(Balanced Rhythms)읎에요. 63판은 닀섯을 더했얎요. 몚팻의 몚서늬 소용돌읎와 닀넀닀의 사진(Moffatt Eddies), 맥슀웰의 묌고Ʞ 눈 렌슈(Maxwell's Fish-Eye), 읎지쌀비치의 슀파읎크 신겜망(Izhikevich Network), 애듀러와 하소퍌, 우슬늬의 듀뜞 집합 였음러 지표(Excursion Sets), 심슚곌 랔띌읎슀의 ì—­ì„€(Simpson's Paradox)읎에요. 64판은 닀섯을 더했얎요. 암만곌 베읞컀륎의 팔각 타음링을 더 람띌윈의 닀쀑 격자로 지은 것(Ammann-Beenker), 겔판튞, 칎프띌녞프, 저레빈슀킀의 아메바와 포륎슀베늬, 파사레, 치흐의 구멍(Polynomial Amoeba), 킀랔곌 죌렉의 결핚 얌Ʞ와 플시뚌, 데 킀아띌, 칌띌륎윔, 몚늬지의 지귞재귞 전읎(Kibble-Zurek Ions), 알윔비의 적분곌 힐의 영속도 곡선(Hill's Regions), 슀티랐슚곌 칎플찚의 거꟞로 선 진자와 아치슚, 멀늰의 읞도 밧쀄 묘Ʞ(Indian Rope Trick)예요. 65판은 닀섯을 더했얎요. 폰 녞읎만곌 멀늰슀의 거품 법칙(Foam Coarsening), 베드퍌드와 맥멀런의 자Ʞ 아핀 칎펫(McMullen Carpet), 큎띌읞의 돛곌 띌귞랑죌의 죌Ʞ 연분수(Klein's Sails), 알윔비의 잡지선곌 샬의 공쎈점 곡멎(Jacobi Geodesics), 하닀마륎와 늬람친슀킀의 묌방욞곌 윔륎테슀의 정규화 슀토크슬렛(Sedimenting Cloud)읎에요. 67판은 닀섯을 더했얎요. 띌돈의 변환곌 띌마찬드란, 띜슈믞나띌알난의 랹프 필터(Radon Transform), 아륎틎의 람레읎드 군곌 폭슀, 녞읎워슀의 배치 공간(Artin Braids), 월시의 핚수계와 싀베슀터의 행렬, 페음늬의 ì°šë¡€(Walsh Functions), 아레프의 깜박읎는 소용돌읎(Blinking Vortex), 뒀팜의 사읎큎띌읎드(Dupin Cyclide)예요. 68판은 닀섯을 더했얎요. 듀로넀의 빈 왞접원곌 로슚의 뒀집Ʞ(Delaunay Flips), 몚넀로-뒀멘의 플볎나치 닚얎 프랙탈(Fibonacci Word), 로슈의 검출 Ʞ쀀(Rose Criterion), 요섞푞슀 묞제와 바셰의 셈 놀읎(Josephus Problem), 레비치비타의 평행 읎동곌 푞윔의 진자(Parallel Transport)예요. 69판은 닀섯을 더했얎요. 랭퍌드의 짝곌 데읎비슀의 졎재 조걎, 슀윜렘의 수엎(Langford Pairs), 아하로녞프와 뎄의 횚곌와 베늬 등의 파멎 전위(Aharonov-Bohm), 페니의 놀읎와 윘웚읎의 선행수(Penney's Game), 휘튞니와 귞띌우슀타읞의 회전수 정늬(Whitney-Graustein), 슈타읎너의 대칭화와 귞로슀의 수렎(Steiner Symmetrization)읎에요. +죌제 5 로 쓎 수학곌 닀음에 핮 볌 수학읎에요. 28판읎 처음 ì—Ž 개륌 썌얎요. 슈람-뢰람너 진화(Loewner Trace), 포드 원(Ford Circles), 턾 난 ê³µ 정늬(Hairy Ball), 아슈텍 닀읎아몬드와 북극권(Aztec Diamond), 부니몚비치 겜Ʞ장(Stadium Billiard), 띌우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 닀멎첎와 E8(Gosset Polytope), 윘웚읎의 볎고 말하Ʞ 수엎(Look and Say), 위토프 게임의 귞런디 값(Wythoff Nim), 쌍곡 평멎의 띠 몚형(Hyperbolic Band)읎에요. 29판은 여섯을 더했얎요. 큎띌읞의 j 불변량(J-Invariant), 혾프 올뭉치(Hopf Fibration), 닀읎슚 람띌욎 욎동(Dyson Gas), 가쌀알 바늘 묞제의 페론 나묎(Perron Tree), 임계선 위의 늬만 제타 핚수(Zeta Loops), Ꞟ버튞-섀넌-늬슈 늬플곌 베읎얎-디아윔니슀의 음곱 번(Seven Shuffles)읎에요. 30판도 여섯을 더했얎요. 섞 간격 정늬(Three Gaps), 혞프슀태터 나비와 TKNN 정수(Hofstadter Butterfly), 녞읎만의 왞부 당구(Outer Billiards), 볌드의 탄도 퇎적곌 KPZ 앞멎(Ballistic Deposition), 아륎놀튞의 믞앀더(Meander), 람로드벀튞와 핎뚞슬늬의 슀믞Ʞ(Percolation)예요. 31판도 여섯을 더했얎요. 마슀킷 조각의 큎띌읞 êµ° 극한 집합(Indra's Pearls), 늬틀우드 닀항식의 ê·Œ(Littlewood Roots), 고든의 가우슀 핎자(Gaussian Moat), 아륎놀튞의 고양읎 사상(Cat Map), 자부슀킀와 크룚슀칌의 KdV 솔늬톀(KdV Solitons), 볎읎 곡멎(Boy's Surface)읎에요. ROUND.md 의 「낯선 수학 예」 여덟 가욎데 쓎 것은 슈람-뢰람너 진화와 아슈텍 닀읎아몬드, 몚듈러 형식, 혾프 올뭉치, 섞 간격 정늬, 왞부 당구 여섯읎었고, 31판읎 늬틀우드 닀항식의 귌곌 큎띌읞 군의 극한 집합을 썚서 여덟을 ë‹€ 썌얎요. 32판도 여섯을 더했얎요. 타벗 횚곌와 양자 칎펫(Talbot Carpet), Ꞟ람레슀 추잡의 표(Gilbreath Table), 원 사상의 아륎놀튞 혀(Arnold Tongues), 윘웚읎와 띌딘의 핀휠 타음링(Pinwheel Tiling), 윌킚슚곌 윌렘섌의 칚입 슀믞Ʞ(Invasion Percolation), 욞람의 묞제와 핎뚞슬늬 곌정(Hammersley Lines)읎에요. 33판도 여섯을 더했얎요. 에읞절, 홀로읎드, 로믹, 비띌귞의 묎작위 정렬 넀튞워크(Sorting Network), 베륎튞랑의 ì—­ì„€(Bertrand Paradox), 펜로슈의 비출 수 없는 ë°©(Penrose Room), 당듀랭 구(Dandelin Spheres), 테읎튞의 볎로메였 고늬(Borromean Rings), 곚드바흐 혜성(Goldbach Comet)읎에요. 34판도 여섯을 더했얎요. 톰슚 묞제와 였음러가 낚Ʞ는 닀섯 읎웃 엎둘(Thomson Problem), 쌈빈의 ë°° 묌결(Kelvin Wake), 요넀알마가 낾 와닀의 혞수(Lakes of Wada), 샀읎데거의 강 귞묌(Scheidegger Rivers), 몚저의 소파 묞제와 핎뚞슬늬의 소파(Moving Sofa), 크누슀의 춀추는 링크로 푌 펜토믞녞 덮Ʞ(Pentomino Cover)예요. 35판도 여섯을 더했얎요. 뢞로의 폭읎 음정한 곡선(Constant Width), 렌니의 죌찚 묞제(Rényi Parking), 칎늬의 비죌Ʞ 왕 타음(Kari-Culik Tiles), 윜띌윔슀킀 수엎(Kolakoski), 쿠띌몚토와 바톡토흐의 킀메띌 상태(Chimera State), 죌윥슀킀 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했얎요. 귞레읎슚의 정늬가 말하는 곡선 닚축 흐멄(Curve Shortening), 헀우드의 음곱 색 토러슀 지도(Heawood Torus), 슈바륎잠 랜턮(Schwarz Lantern), 윌람러햄곌 깁슀의 현상(Gibbs Phenomenon), 타원 곡선의 현곌 접선(Elliptic Chords), 지뚌의 파국 Ʞ계(Catastrophe Machine)예요. 37판도 여섯을 더했얎요. 포천의 쓞Ʞ 선 알고늬슘(Fortune's Sweep), 앙투안의 목걞읎(Antoine's Necklace), 칎륎만 소용돌읎 Ꞟ의 안정성(Kármán Street), 당당한 원판의 헥사틱 상(Hexatic Phase), 졎슚-멜 쪜맀와 아람띌믞 법칙(Johnson-Mehl), 쿠띌몚토-시바신슀킀 방정식(Kuramoto-Sivashinsky)읎에요. 38판도 여섯을 더했얎요. 앀더슚 국소화(Anderson Localization), 묎얎가 ì°Ÿê³  Chenciner 와 Montgomery 가 슝명한 섞 몞 묞제의 8자 핮(Figure-Eight Orbit), 예섌곌 빈튞너, 에륎되시의 베륎누읎 합성곱(Bernoulli Convolutions), 람띌우얎륎의 고정점 정늬륌 슝명하는 슈페륎너 볎조정늬(Sperner's Lemma), 에겐베륎거와 폎늬아의 항아늬(Pólya Urn), 뇌륎고륎의 묎한 수엎(Infinity Series)읎에요. 39판도 여섯을 더했얎요. 고든, 웹, 월퍌튞의 등슀펙튞럌 북(Isospectral Drums), 폰 망곚튞의 ꌎ로 쓎 늬만의 명시 공식(Explicit Formula), 요한 베륎누읎의 최닚 강하선(Brachistochrone), 슈타읎너의 포늬슘(Steiner Chain), 몚런 집닚윌로 귞늰 킹뚌의 합류(Kingman Coalescent), 볎륎수크-욞람 정늬(Borsuk-Ulam)예요. 40판도 여섯을 더했얎요. 앚더슀의 람띌욎 연속 랜덀 나묎(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 핚수(Hydrogen Orbital), 저도비치 귌사의 팬쌀읎크(Zeldovich Pancakes), 망덞람로가 낎닀볎고 례러, 슈람, 베륎너가 슝명한 람띌욎 욎동의 바깥 겜계(Brownian Frontier), 복소 ꞎ슈부륎크-란닀우 방정식의 결핚 난류(Defect Turbulence), 룚퍌튞 왕자의 정육멎첎(Prince Rupert's Cube)예요. 41판은 닀섯을 더했얎요. 홀곌 왜걎의 현수선 Ꞟ곌 정닀각형 바퀮(Square Wheels), 퐁슬레의 포늬슘(Poncelet Porism), 슐레플늬의 4찚원 정닀포첎륌 자륞 닚멎(Polytope Slices), 큎늌쌁곌 티뚌, 마귞누손의 사읎튞슀왑(Siteswap), Dudeney 의 잡화상 퍌슐곌 월늬슀-볎여읎-게륎비엔 정늬(Hinged Dissection)예요. 42판은 닀섯을 더했얎요. 바륞슀도륎프 규칙윌로 도는 Ʞ사 여행(Knight's Tour), 페륎믞와 파슀타, 욞람, 칭구의 되돌아옎(FPUT Recurrence), 포슬늬에-늜킚 반전Ʞ(Peaucellier Linkage), 몰늬의 삌등분선 정늬(Morley Triangle), 랄곌 마드띌슀, 소칌의 플벗 알고늬슘윌로 돌며 자Ʞ 회플 걞음(Pivot Walk)읎에요. 43판은 닀섯을 더했얎요. 월늬슀 워컀의 칌레읎도사읎큎(Kaleidocycle), 쌍곡형에서 록소드롬형윌로 감Ʞ는 뫌비우슀 변환의 흐멄(Möbius Loxodrome), 터튞의 용수철 정늬(Tutte Embedding), 디펀튞와 크래비잠의 고늬 정늬륌 품은 히토메자시 사시윔(Hitomezashi), 룜게 현상곌 첎비쇌프 점(Runge Phenomenon)읎에요. 44판은 닀섯을 더했얎요. 마크 칎잠의 고늬 몚형(Kac Ring), 슀톀곌 튜킀의 햄 샌드위치 정늬(Ham Sandwich), 람란튞의 불가늬아 솔늬테얎(Bulgarian Solitaire), 엘슀쌄슀와 프늬시의 탄도 소멞(Ballistic Annihilation), 페레귞늰의 솔늬톀(Peregrine Soliton)읎에요. 45판은 닀섯을 더했얎요. 로걎-셰프와 베륎시크-쌀로프의 극한 곡선윌로 닀가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도람룚신 겜계 조걎을 걎 임계 읎징 몚형의 SLE(3) 겜계(Ising Interface), 프늬예제프와 동료듀의 였음러 걷개로 쌓는 로터 띌우터 응집곌 레빈-페레슀의 슀맀시 합(Rotor-Router), 폎늬아의 곌수원 묞제(Pólya's Orchard), 넬슚의 평멎 색칠 묞제와 아읎슈벚의 음곱 색(Hadwiger-Nelson)읎에요. 46판은 닀섯을 더했얎요. 람늎룚앙의 구역(Brillouin Zones), 몚슀 읎론곌 레람의 귞래프(Reeb Graph), 로렌잠 Ʞ첎와 시나읎의 당구(Lorentz Gas), 늬얞 Ꞁ래슀의 점묎늬(Glass Patterns), 벀치와 수테띌, 불플아니의 확률 공명(Stochastic Resonance)읎에요. 47판은 닀섯을 더했얎요. 였킀프와 홍, 슀튞로가잠의 슀워멀레읎터(Swarmalators), 빌슀의 왞로욎 달늬Ʞ 선수 추잡(Lonely Runner), 샀잠의 올로읎드와 ê·ž 전개도(Oloid), 쟚베의 원 채우Ʞ 정늬와 윜늰슀-슀티랐슚의 반복(Koebe Packing), 윘웚읎의 병사와 황ꞈ비 묎게(Conway's Soldiers)예요. 48판은 닀섯을 더했얎요. 였하띌의 맀듭 뫌비우슀 에너지(Möbius Energy), 첎비쇌프의 펞향곌 룚빈슀타읞-사륎낙의 밀도(Chebyshev's Bias), 섀든곌 레킀엔, 마슀든의 읎쀑 소용돌읎 능선(Double Gyre), 아자늬와 프로슀튞의 깜빡읎는 래칫(Flashing Ratchet), 망덞람로의 프랙탈 슀믞Ʞ(Fractal Percolation)예요. 49판은 닀섯을 더했얎요. 뒀러의 전개 묞제와 셰퍌드의 추잡(DÃŒrer's Unfolding), 베늬와 곚드버귞가 재규격화한 섞타 합의 ꌬ부랑 곡선(Curlicue), 였음러 방정식곌 푞앵소의 폎혞드로 볞 테니슀 띌쌓 정늬(Tennis Racket), 쌐프의 사슬곌 ë„€ 색 정늬(Kempe Chains), 퇎플늬잠의 낎접 정사각형 묞제와 귞늰-로람의 직사각형(Square Peg)읎에요. 50판은 닀섯을 더했얎요. 크레읎치넌곌 배철러의 2찚원 난류가 거꟞로 가는 캐슀쌀읎드와 맥윌늬엄슀의 소용돌읎(Inverse Cascade), 사포발곌 로소, 구예의 확산 앞쀄(Diffusion Front), 필늬와 마륎쌀티, 쌀읎잠와 타읎왞륎의 욎동성 상분늬(Motility Clusters), 박곌 슀넀펜의 진화 몚형(Bak-Sneppen), 늬섞의 영원한 가속(Risset Rhythm)읎에요. 51판은 닀섯을 더했얎요. 슀믞슀와 마읎얎슀, 캐플런, 굿뚌-슀튞띌우슀의 몚자 타음곌 H/T/P/F 메타타음 치환(Hat Monotile), 반도와 동료듀의 최적 속도 몚형곌 슀Ʞ알마의 원형 튾랙 싀험(Phantom Jam), 사토와 테읎튞의 법칙곌 타원곡선의 a_p(Sato-Tate), 칌띌비와 알우의 공간에서 핞슚읎 잘띌 귞늰 5ì°š 페륎마 곡선(Calabi-Yau Quintic), 에덞슀람룚너와 레처, 조몚로디얞의 지속 혞몰로지와 바윔드(Persistence Barcode)예요. 52판은 닀섯을 더했얎요. 슈람곌 셰필드가 등고선을 SLE(4) 로 볎읞 2찚원 가우슀 자유장(Gaussian Free Field), 당튀와 마슈묎닀륎, 베링거의 ꎑ탄성 힘 사슬(Force Chains), 앀슀컎의 사쀑죌와 마테읎칎, 플잠몚늬슀의 같은 통계 닎ꞈ질(Anscombe's Quartet), 드 람띌윈 수엎곌 BEST 정늬(de Bruijn Sequence), 띌읞슀의 베타 평멎 띠 제튞(Zonal Jets)예요. 53판은 엎을 더했얎요. 듀크, 가륎시아, 룚잠가 귞늰 가우슀 죌Ʞ(Gaussian Periods), 부륎가 볎읞 현수멎곌 나선멎의 휹(Catenoid-Helicoid), 디랙의 벚튞 묘Ʞ(Belt Trick), 헬늄홀잠와 러람의 ì—°êž° 고늬 개구늬뜀(Leapfrogging Vortices), 람룩슀, 슀믞슀, 슀톀, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀컀슚의 최대 흐멄 최소 절닚(Max-Flow Min-Cut), 윔슀타슀 ë°°ì—Ž(Costas Array), 바띌니, 비얎든, 칞의 묎게쀑심 분할(Barycentric Subdivision), 묎윔파디아알와 크넀저의 ë„€ ꌭ짓점 정늬(Four-Vertex Theorem), 였음러의 엘띌슀티칎(Euler's Elastica)예요. 54판은 엎을 더했얎요. 베늬의 묎작위 파동곌 볎고몰니, 슈믞튞가 슀믞Ʞ로 섌 마디 영역(Nodal Domains), 플얎험버튞가 사읞 흐늄에서 찟은 읎상 고유 몚드(Strange Eigenmode), 플얎시 적분곌 베늬, 업슀틞의 첚점 회절 파국(Pearcey Cusp), 나겔곌 바읎슀의 STIT 몚자읎크(STIT Tessellation), 레비의 아크사읞 법칙(Arcsine Law), 슀퇎륎메륎가 닀룬 쌍극자 장 속 대전 입자(StÞrmer Trap), 게음곌 섀플늬의 지연 수띜(Stable Marriage), 슀메음의 말굜(Smale Horseshoe), 욞람 수엎곌 슈타읎너베륎거가 찟은 숚은 진동수(Ulam Sequence), 위얎와 펠런의 거품곌 쌈빈의 깎은 팔멎첎(Weaire-Phelan)예요. 55판은 엎을 더했얎요. 밀러와 셰필드의 허수 Ʞ하(Imaginary Geometry), 티소의 지시 타원곌 메륎칎토륎 투영(Tissot's Indicatrix), 믌윔프슀킀의 묌음표 핚수(Question Mark Function), 버로슀와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘곌 바륎비에의 국수(Buffon's Noodle), 부자의 늬듬 타음링 캐넌(Vuza Canon), 하읞곌 낎시의 헥슀와 게음의 헥슀 정늬(Hex Theorem), 8자 고늬의 범플복곌 자유군의 쌀음늬 귞래프(Universal Cover), 였람친니윔프와 저도비치, 투생곌 윌첵의 확산 제한 소멞(Toussaint-Wilczek), 헬늄홀잠와 쌈빈의 와류 시튞와 크띌슀니의 와동 덩읎(Vortex Sheet)예요. 56판은 엎을 더했얎요. 마테롱의 죜은 잎곌 쌄듀, 퇎넀슀가 아래에서 볞 완전 표볞(Dead Leaves), 곚롬의 튞로믞녞 깔Ʞ(Golomb Trominoes), 윌슚의 고늬 지욎 걞음곌 례러, 슈람, 베륎너의 균등 생성나묎 페아녞 곡선(Tree Peano Curve), 랔로흐와 제너의 진동곌 워니얎의 사닀늬(Bloch Oscillations), 앀드룚슀의 곡선곌 아시몚프의 귞랜드 투얎(Andrews Curves), 지니람륎와 Ʞ륎윔의 원 법칙곌 좀뚞슀, 크늬산티, 솜폎늰슀킀, 슀타읞의 타원 법칙(Elliptic Law), 에륎되시의 불음치 묞제와 볎웚읞, 최, 쿀슀의 3진 수엎(Erdős Discrepancy), 망덞람로의 곱셈 폭포와 칎안, 페읎늬에륎의 묞턱(Multiplicative Cascade), 윔륎테, 첎읎킚, 곚럜, 파읞의 묎작위 조직화(Random Organization), 칌룚가레아누, 화읎튞, 풀러의 ꌬ임곌 뒀틀늌(Twist and Writhe)읎에요. 57판도 엎을 더했얎요. 랔룞의 쀑심축곌 듀불 변환(Medial Axis), 슀메음곌 윌늬엄슀의 솔레녞읎드(Solenoid), 솔로몬의 묎작위 환겜 속 걞음곌 시나읎의 걞음(Sinai's Walk), 테렐곌 펜로슈의 회전(Terrell Rotation), 갈페늰의 π 당구(Galperin Billiard), 페륎마의 두 제곱수 정늬와 자Ʞ에의 한 묞장 슝명(Zagier's Windmills), 슀토크슀 표류와 게륎슀튞너의 튞로윔읎드 파도(Stokes Drift), 띌읞하륎튞부터 ë§š, 맀큎띌우드맚, 폰 데띌우까지의 볌록 였각형 타음링(Pentagon Tilings), 페로와 하위헌슀의 튞랙튞늭슀와 자전거 바퀮 자국(Bicycle Tracks), 였슀튞발튞 숙성곌 LSW 읎론(Ostwald Ripening)읎에요. 58판도 엎을 더했얎요. 알랔란곌 게륎데슀의 ê±°ìšž 곡선(Mirror Curves), 가욎튞와 플셔, 백슀터의 딱딱한 정사각형 격자 Ʞ첎(Hard Squares), 닀니엘 베륎누읎의 맀달늰 사슬곌 베셀 핚수 J0(Hanging Chain), 레음늬와 테음러의 불안정성(Rayleigh-Taylor), 터킀의 반평멎 깊읎와 띌도의 쀑심점 정늬(Tukey Depth), 례러와 베륎너의 람띌욎 고늬 수프(Loop Soup), 칎돔첎프-페튞비아시빌늬 방정식곌 윔닀마의 선 솔늬톀 웹(KP Web), 크누슀가 읎늄 붙읞 베쌀튞-귞레읎 윔드(Beckett-Gray), 파슀칌의 신비 육각형곌 슈타읎너, 컀크뚌의 점(Hexagrammum Mysticum), 드 슀밋곌 렌슀튞띌의 판화 화랑 사상(Print Gallery)읎에요. 59판도 엎을 더했얎요. 륎장드륎와 가우슀의 읎찚 상혞 법칙(Quadratic Reciprocity), 위귞너 핚수와 유륎쌀, 슀톚러의 쌜 고양읎(Wigner Cat), 아비닚곌 샀믞륎의 심 칎빙(Seam Carving), 슐늬히팅곌 녞륎튞마륎크, 에거슀의 알갱읎 Ʞ첎 속 맥슀웰의 도깚비(Granular Demon), 큎렙시의 대각 3ì°š 곡멎곌 쌀음늬, 새뚌의 직선 27개(Clebsch Surface), 볎데륎베륎크의 나선 타음링(Voderberg Tiling), 자읎페륎튞의 곡멎(Seifert Surface), 혞크슀의 자Ʞ 흥분 점곌정(Hawkes Process), 윔혞넚의 자Ʞ조직 지도(Kohonen Map), 레음늬의 위상자 걞음곌 굿맚의 슀페큎 통계(Laser Speckle)예요. 60판도 엎을 더했얎요. 마삎늬아가 짚은 RANDU 의 평멎(RANDU Planes), 였음러의 36 장교와 볎슀, 슈늬칞데, 파컀의 10ì°š 귞레윔-띌틎 방진(Euler's Officers), 펜로슈와 칎터의 등각 윀팩튞화 도형(Penrose Diagram), 러더퍌드 산란의 포묌선 귞늌자(Rutherford Shadow), 였음러 나선곌 프레넬 적분, 윔륎뉎의 회절 셈(Cornu Spiral), 늬처드슚의 디바읎더와 망덞람로의 핎안선 찚원(Richardson Dividers), 에농, 홀, 페익슀의 1찚원 자Ʞ쀑력 멎 몚형곌 며든-벚의 격렬한 읎완(Phase Spiral), 에륎되시와 레니의 거대 성분곌 아큎늬옵타슀, 드수자, 슀펜서의 폭발적 슀믞Ʞ(Giant Component), 허프와 두닀, 하튞의 직선 ì°Ÿêž°(Hough Transform), 조륎당의 곡선 정늬와 심랏의 교찚 섞Ʞ(Jordan Curve)예요. 61판도 엎을 더했얎요. 섞슀나와 닀섯 사람의 영옚 묎작위장 읎징 눈사태(Crackling Noise), 몜죌의 흙 옮ꞰꞰ와 맀캔의 옮김 볎간(Monge Transport), 파욞의 덫곌 마티왞 방정식(Paul Trap), 귞늰힐의 제 묎게 êž°ë‘¥(Greenhill Column), 흐바탈의 믞술ꎀ 정늬와 플슀크의 섞 색 슝명(Art Gallery), 홀디치의 고늬(Holditch Ring), 톰슚의 꺟읞 역선곌 헀비사읎드의 움직읎는 전하(Thomson Kink), 슈나읎더와 바읎슀의 쌍성 렌슈(Binary Lens), 테음러의 되섞Ʞ(Taylor Unmixing), 람누아, 칌로, 디에넀륎 부부의 칎나륎 폭발(Canard Explosion)읎에요. 62판도 엎을 더했얎요. 아람늬윔소프와 큎띌읎너, 로슀, 였틀러의 소용돌읎 격자(Abrikosov Lattice), 욞늬크니의 청색 잡음곌 빈자늬와 뭉치 방법(Void and Cluster), 파읞뚌곌 힙슀의 첎컀볎드(Feynman Checkerboard), 띌도의 바쁜 비버와 마크섌, 분튞로크의 닀섯 상태 챔플얞(Busy Beaver), 폭슀와 컀시너, 비치의 평평한 곡멎 위 직선 흐멄(Veech Octagon), 늬만의 믞분 불가능 핚수와 거버, 닀위슀테륎마튞의 복소 곡선(Riemann's Function), 헀슈와 킚슐레의 한 타음 형(Heesch Tiles), 찚크띌볎륎티와 헀읎슀, 볎고시안의 마당 장터 몚형(Yard-Sale Model), 베륎귞렌의 플타고띌슀 섞 수 나묎(Berggren Tree), 더람띌윈곌 람, 렁의 닚위귌 합곌 밀륞의 완전히 균형 잡힌 늬듬(Balanced Rhythms)읎에요. 63판은 닀섯을 더했얎요. 몚팻의 몚서늬 소용돌읎와 닀넀닀의 사진(Moffatt Eddies), 맥슀웰의 묌고Ʞ 눈 렌슈(Maxwell's Fish-Eye), 읎지쌀비치의 슀파읎크 신겜망(Izhikevich Network), 애듀러와 하소퍌, 우슬늬의 듀뜞 집합 였음러 지표(Excursion Sets), 심슚곌 랔띌읎슀의 ì—­ì„€(Simpson's Paradox)읎에요. 64판은 닀섯을 더했얎요. 암만곌 베읞컀륎의 팔각 타음링을 더 람띌윈의 닀쀑 격자로 지은 것(Ammann-Beenker), 겔판튞, 칎프띌녞프, 저레빈슀킀의 아메바와 포륎슀베늬, 파사레, 치흐의 구멍(Polynomial Amoeba), 킀랔곌 죌렉의 결핚 얌Ʞ와 플시뚌, 데 킀아띌, 칌띌륎윔, 몚늬지의 지귞재귞 전읎(Kibble-Zurek Ions), 알윔비의 적분곌 힐의 영속도 곡선(Hill's Regions), 슀티랐슚곌 칎플찚의 거꟞로 선 진자와 아치슚, 멀늰의 읞도 밧쀄 묘Ʞ(Indian Rope Trick)예요. 65판은 닀섯을 더했얎요. 폰 녞읎만곌 멀늰슀의 거품 법칙(Foam Coarsening), 베드퍌드와 맥멀런의 자Ʞ 아핀 칎펫(McMullen Carpet), 큎띌읞의 돛곌 띌귞랑죌의 죌Ʞ 연분수(Klein's Sails), 알윔비의 잡지선곌 샬의 공쎈점 곡멎(Jacobi Geodesics), 하닀마륎와 늬람친슀킀의 묌방욞곌 윔륎테슀의 정규화 슀토크슬렛(Sedimenting Cloud)읎에요. 67판은 닀섯을 더했얎요. 띌돈의 변환곌 띌마찬드란, 띜슈믞나띌알난의 랹프 필터(Radon Transform), 아륎틎의 람레읎드 군곌 폭슀, 녞읎워슀의 배치 공간(Artin Braids), 월시의 핚수계와 싀베슀터의 행렬, 페음늬의 ì°šë¡€(Walsh Functions), 아레프의 깜박읎는 소용돌읎(Blinking Vortex), 뒀팜의 사읎큎띌읎드(Dupin Cyclide)예요. 68판은 닀섯을 더했얎요. 듀로넀의 빈 왞접원곌 로슚의 뒀집Ʞ(Delaunay Flips), 몚넀로-뒀멘의 플볎나치 닚얎 프랙탈(Fibonacci Word), 로슈의 검출 Ʞ쀀(Rose Criterion), 요섞푞슀 묞제와 바셰의 셈 놀읎(Josephus Problem), 레비치비타의 평행 읎동곌 푞윔의 진자(Parallel Transport)예요. 69판은 닀섯을 더했얎요. 랭퍌드의 짝곌 데읎비슀의 졎재 조걎, 슀윜렘의 수엎(Langford Pairs), 아하로녞프와 뎄의 횚곌와 베늬 등의 파멎 전위(Aharonov-Bohm), 페니의 놀읎와 윘웚읎의 선행수(Penney's Game), 휘튞니와 귞띌우슀타읞의 회전수 정늬(Whitney-Graustein), 슈타읎너의 대칭화와 귞로슀의 수렎(Steiner Symmetrization)읎에요. 71판은 닀섯을 더했얎요. 페니의 −1+i 진법곌 데읎비슀, 크누슀의 쌍둥읎 용(Twindragon), 녞바킀, 한첎, 벀튞의 평탄 3찚원 닀양첎와 윘웚읎, 로섞티의 읎늄(Platycosms), 크누슀의 믞니맥슀 마슀터마읞드(Mastermind), 슀넬슚의 텐섞귞늬티 프늬슘(Tensegrity), 아폎로니였슀의 접쎉 묞제와 비에튞의 풀읎(Problem of Apollonius)예요. 목록 밖에서 닀음에 핮 볌 것읎에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷읎에요. Ꞟ람레슀 표는 소수 말고 닀륞 수엎로도 섞욞 수 있얎요. 였듀늬윔(1993)에 따륎멎 2 닀음에 간격읎 작고 고륎게 흩얎진 홀수가 였는 수엎에서는 1 의 엎읎 서고, 간격읎 크멎 깚젞요. 간격의 크Ʞ륌 녞람로 걞멎 1 의 엎읎 묎너지는 자늬가 볎여요. 핎뚞슬늬 곌정의 점 대신 격자 칞마닀 지수 분포 묎게륌 두멎 마지막 통곌 슀믞Ʞ(로슀튞, 1981)가 되고, 극한 몚양읎 쌍곡선에서 (√x+√y)² 의 겜계로 바뀌얎요. @@ -4196,6 +4279,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 69판에서 볞 것읎에요. 셈읎 정확한 대상은 읎번에도 팹턮 안에서 뚌저 맞춰 뎀얎요. Langford Pairs 는 윔드와 같은 순서로 따로 섞얎 뒀집은 것을 하나로 친 3, 4, 7, 8, 11, 12 의 ë°°ì—Ž 수가 1, 1, 26, 150, 17,792, 108,144 였고, 슀윜렘 수엎은 4, 5, 8, 9, 12 에서 3, 5, 252, 1,328, 227,968 읎었얎요. Penney's Game 은 윘웚읎의 선행수로 셈한 승산읎 따로 돌며 20만 판곌 0.0013 안에서 맞았고, 팚턎을 Speed 3 에서 한 시간 돌며 HHT 대 THH 5,475 판에서 B 가 0.7512 륌 가젞갔얎요. Aharonov-Bohm 은 분수 찚수 베셀 핚수륌 밀러의 역방향 점화식곌 녞읎만의 합윌로 구핎서, 자속 0 에서 평멎파와 1e-14 안윌로, 자속 1 에서 1.4e-8 안윌로 같아요. 정늬가 수륌 못 박는 대상은 팚널에서 지쌜뎀얎요. Steiner Symmetrization 의 쎈승달은 황ꞈ각 찚례의 축 닀섯 번에 둘레가 원의 1.520 배에서 1.004 배로 듀얎왔얎요. 황ꞈ각 찚례가 늘 원윌로 간닀는 정늬는 ì°Ÿì§€ 못했고, 닀섯 몚양읎 ë‹€ 1% 안윌로 듀얎였는 것만 뎀얎요. Whitney-Graustein 은 회전수가 1 에서 0 윌로 낎렀갈 때가 0 에서 1 로 였륌 때볎닀 곡선을 훚씬 섞게 비틀얎서, 흔듀늌을 ë‹€ 꺌도 한 구간읎 닀륞 구간볎닀 16ë°° 빚늬 달렀요. 팚널의 폭읎 정늬륌 ë‹€ 닎지 못한 곳도 있었얎요. Aharonov-Bohm 의 반 양자에서 ë§š 아랫쀄 양 끝의 밀늌 찚읎는 0.5 가 아니띌 0.445 바퀎예요. 양 끝읎 귞늌자 선에서 30 LED 읞데, ꎀ ë’€ 85 LED 에서의 프레넬 폭 √(λx) 가 24 LED 띌 회절읎 아직 낚아서예요. +71판에서 볞 것읎에요. 셈읎 정확한 대상은 읎번에도 팹턮 안에서 뚌저 맞춰 뎀얎요. Twindragon 은 |a|, |b| ≀ 200 읞 가우슀 정수 160,801 개가 몚두 22 자늬 안에 끝나 제 값윌로 돌아왔고, 22 자늬까지의 2^22 가지 쀄읎 ë‹€ 닀륞 수였얎요. Mastermind 는 1296 가지 비밀을 ë‹€ 풀얎 크누슀의 4.476 수와 1, 6, 62, 533, 694 판의 나뉚읎 녌묞곌 같았고, 얎빙곌 쿠읎는 평균읎 더 짧은 대신 가장 Ꞟ멎 6 수였얎요. Problem of Apollonius 는 ë„€ 크Ʞ에서 30분씩 돌며 접쎉 였찚가 3.7e-9 LED 륌 넘지 않았고, 묎작위 섞 원 40만 벌의 핎의 수가 0, 4, 8 뿐읎었얎요. Platycosms 는 덱 변환을 11³ 큐람에서 재얎 사볞마닀 여섯 방향에 붙임읎 하나뿐읎었고, didicosm 의 αβγ = 1 읎 맞았얎요. Tensegrity 는 프늬슘 하나륌 떌얎 돌늬자 옆 쌀읎랔을 따띌 잰 비틀늌읎 n = 3, 4, 5, 6 에서 π/2 − π/n 에 소수 ë„·ì§ž 자늬까지 몚였얎요. 정늬 밖의 것은 팚널에서 뎀얎요. 쌓은 텐섞귞늬티의 닚은 혌자음 때볎닀 덜 비틀렀 n = 3 에서 20~24 도였고, 추륌 얹윌멎 34~38 도로 프늬슘 혌자의 30 도륌 넘었얎요. 아폎로니였슀의 원 여덟은 늘 서명 여덟읎 아니얎서, 섞 쀑심읎 한 쀄에 가까우멎 한 식읎 같은 서명 둘을 낎고 반대 서명은 없얎요. 여덟읎 선 프레임 가욎데 서명읎 ë‹€ 닀륞 것은 40% 였얎요. 마슀터마읞드에서 비밀 없읎 가장 큰 묎늬륌 낚Ʞ는 속읎는 출제자륌 만나멎 크누슀와 얎빙은 300 판 몚두 5 수, 쿠읎는 몚두 6 수였얎요. + 목록 밖에서 31판 뒀에 핮 볌 것읎에요. 페륎믞-파슀타-욞람-칭구의 되돌아옎(1955)은 KdV Solitons 와 같은 뿌늬띌 같은 판에 두지 않아요. 목록 밖에서 33판 뒀에 핮 볌 것읎에요. 토칎륎슀킀의 ë°©(1995)은 거욞로 된 닀각형 방에서 한 점의 빛읎 닀륞 한 점에만 닿지 못하는 방읎띌 Penrose Room 의 ꎑ선 추적을 귞대로 썚요. 프늬드뚌곌 슀윔띌(1987)는 둥귌 원 셋윌로는 볎로메였 고늬륌 만듀 수 없닀는 것을 볎였는데, 원을 밀얎 넣을 때 얎디서 부딪히는지가 귞늌읎 돌요. 정렬 넀튞워크의 시간 t 의 순엎을 점윌로 찍윌멎 대각선에서 원판을 지나 반대 대각선윌로 도는 점 구늄읎 돌요. ê·ž 원판읎 2007년 녌묞읎 낎닀볞 아륎킀메데슀 법칙읎에요. 간격읎 2k 읞 소수 쌍의 수는 Goldbach Comet 곌 같은 FFT 로 섞고, k 마닀 하디-늬틀우드 읞수로 갈늬는 쀄Ʞ 귞늌읎 돌요. @@ -4270,6 +4355,8 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 목록 밖에서 69판 뒀에 핮 볌 것읎에요. 토녞묎띌 아킀띌(Akira Tonomura) 등읎 1986년에 쎈전도첎로 감싌 고늬 자석윌로 찍은 전자 홀로귞랚처럌, 고늬 안곌 밖을 지난 간섭 쀄묎늬가 반 칞 얎Ꞌ나는 귞늌을 Aharonov-Bohm 의 ꞉수로 ê·žë € 볌 만핎요. 자속ꎀ 둘을 반대 자속윌로 나란히 두멎 두 귞늌자 선 사읎의 띠만 밀며 채 낎렀와요. 페니의 놀읎륌 ë„€ 번 쀄로 넓히멎 (x2 의 반대) x1 x2 x3 규칙읎 엎여섯 가욎데 여섯에서 최선읎 아니에요. HHTH 에 규칙읎 고륞 THHT 는 7:5 로 읎Ʞ지만 HHHT 는 2:1 로 읎겚서, ê·ž 얎Ꞌ낚을 녾뾌 하나로 넘겚 볎는 판읎 돌요. 휘튞니가 같은 녌묞에 적은 공식, ê³§ 교찚점마닀 맀ꞎ 부혞의 합에 ±1 을 더하멎 회전수가 된닀는 것을 Whitney-Graustein 의 곡선 위에 교찚점 표시로 볎여 쀄 만하고, 랔띌디믞륎 아륎놀튞(Vladimir Arnold)가 1994년에 낾 평멎 곡선의 불변량 J+, J−, St 는 곡선읎 제 몞에 접하거나 삌쀑점을 지날 때만 바뀌얎서 ê·ž 순간을 셀 수 있얎요. 한 직선에 대핮 거욞쌍 두 점 가욎데 한쪜만 옮Ʞ는 ê·¹ 대칭화(polarization)는 Steiner Symmetrization 의 빗ꞈ 틀에서 현읎 믞끄러지는 대신 조각읎 축 걎너펞윌로 뒀집혀 걎너가요. 같은 색 랔록을 셋씩 쌓아 읎웃한 둘 사읎마닀 k 랔록읎 끌게 하는 랭퍌드 묞제의 확장은 Langford Pairs 의 탑에 귞대로 서고, 도널드 크누슀(Donald Knuth)의 춀추는 연결(2000)로 선택지가 가장 적은 칞부터 고륎는 탐색을 옆에 두멎 두 탐색 나묎의 크Ʞ륌 나란히 잎 수 있얎요. +목록 밖에서 71판 뒀에 핮 볌 것읎에요. 윌늬엄 Ꞟ버튞(William J. Gilbert)가 1982년 『Mathematical Intelligencer』에 귞늰 −n+i 진법의 타음은 Twindragon 의 첎컀와 섞는 찜을 귞대로 쓰고 n 만 녞람에 걞멎, 쌍둥읎 용읎 닀륞 ꌎ의 타음로 넘얎가는 것읎 볎여요. 바잚띌프 흐바탈(VaÅ¡ek Chvátal)읎 1983년 『Combinatorica』에서 닀룬 n 칞 n 색 마슀터마읞드는 Mastermind 의 격자에 칞 수만 바꿔 올렀서 답 묎늬가 갈늬는 ꌎ을 견쀄 수 있얎요. 막대 여섯의 텐섞귞늬티 읎십멎첎는 로버튞 윔넬늬(Robert Connelly)와 월터 화읎틀늬(Walter Whiteley)의 프늬슀튞레슀 안정성(1996)윌로 얎느 방향읎 가장 묎륞지 셈할 수 있얎서, Tensegrity 의 시뮬레읎션에 ê·ž 방향윌로 누륎는 녞람륌 달멎 돌요. 데칎륎튞의 원 정늬(1643)는 서로 접한 섞 원에 접하는 두 원의 곡률을 한 식윌로 죌얎서, Problem of Apollonius 의 흰 원 셋을 서로 닿게 몰멎 ê·ž 두 원을 식곌 나란히 섞욞 수 있얎요. 육각Ʞ둥을 3분의 1 바퀎와 6분의 1 바퀎로 붙읎는 tricosm 곌 hexacosm, 사볞읎 거욞상윌로 서는 비가향 amphicosm 은 Platycosms 의 사볞 투영에 Ʞ볞 영역만 바꿔 넣윌멎 돌요. + ### 타읎포 죌제 7 로 핮 볞 Ꞁ자 싀험곌 닀음에 핮 볌 것읎에요. 63판곌 64판읎 같읎 돌며 닀섯씩 처음 만듀었얎요. 63판의 닀섯은 한Ꞁ 자몚가 날아와 몚아쓰Ʞ로 잠Ʞ는 조늜(Jamo Assembly), 놀턎곌 하뚌의 Ʞ혞 몚자읎크(Type Mosaic), 혞프슀태터의 튞늜렛 귞늌자(Trip-let), 칎프늬칎의 계산 종읎(Kaprekar's Routine), 캐럎의 낱말 사닀늬(Word Ladder)예요. @@ -4288,7 +4375,7 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. Ʞ능 감을 몚윌멎 넷읎 Ꞁ자 ꌎ을 윔드 밖에서 받는 Ꞟ을 바랐얎요. Long Division 곌 Mouse's Tale 은 Lab 의 픜셀 Ꞁꌎ을 크Ʞ별 비튞맵윌로, Anamorphosis 는 아래 레읎얎의 비튞맵을 텍슀처로, Hunminjeongeum 은 획 순서와 방향을 닎은 한Ꞁ 획 Ꞁꌎ로 받고 ì‹¶ì–Ž 했얎요. Inversions 는 Text 레읎얎에 녞람로 각도륌 거는 돌늬Ʞ 횚곌와, 돌며 Ꞁ읎 닀륞 Ꞁ곌 LED 까지 같은지 견죌는 도구륌 바랐얎요. -닀음에 핮 볌 것읎에요. 0 1 6 8 9 만 쓰는 수는 돌렀도 숫자로 읜혀서 1961 은 돌렀도 1961 읎고, 귞닀음 귞런 핮는 6009 예요. Inversions 의 획 Ꞁꌎ곌 LED 견죌Ʞ륌 귞대로 썚요. 1/89 의 소수는 플볎나치 수륌 한 자늬씩 밀얎 더한 합읎고, 1/998001 은 000 부터 997 까지 늘얎놓닀 998 만 걎너뛰얎서 Long Division 의 판에 나란히 섞욞 만핎요. 원통 ê±°ìšž 착시 원귌(장프랑수아 니슀롱, 『La Perspective curieuse』, 1638)은 바닥에 휘얎 귞늰 낱말읎 가욎데 섞욎 원통 거욞에 비쳐알 바로 읜히는 귞늌읎띌, Anamorphosis 의 ꎑ선을 원통에서 한 번 꺟윌멎 돌요. 핎례볞 䟋矩의 연서, ê³§ ㅇ 을 입술소늬 아래 읎얎 썚서 순겜음 ã…ž 을 짓는 법은 Hunminjeongeum 의 획 표로 귞렀요. Ʞ욀 아폎늬넀륎의 「Il pleut」(『Calligrammes』, 1918)는 Ꞁ쀄 닀섯읎 빗쀄Ʞ처럌 êž°ìšžì–Ž 낎늬는 귞늌시띌 Mouse's Tale 의 셈을 귞대로 썚요. +닀음에 핮 볌 것읎에요. 0 1 6 8 9 만 쓰는 수는 돌렀도 숫자로 읜혀서 1961 은 돌렀도 1961 읎고, 귞닀음 귞런 핮는 6009 예요. Inversions 의 획 Ꞁꌎ곌 LED 견죌Ʞ륌 귞대로 썚요. 1/89 의 소수는 플볎나치 수륌 한 자늬씩 밀얎 더한 합읎고, 1/998001 은 000 부터 997 까지 늘얎놓닀 998 만 걎너뛰얎서 Long Division 의 판에 나란히 섞욞 만핎요. 핎례볞 䟋矩의 연서, ê³§ ㅇ 을 입술소늬 아래 읎얎 썚서 순겜음 ã…ž 을 짓는 법은 Hunminjeongeum 의 획 표로 귞렀요. Ʞ욀 아폎늬넀륎의 「Il pleut」(『Calligrammes』, 1918)는 Ꞁ쀄 닀섯읎 빗쀄Ʞ처럌 êž°ìšžì–Ž 낎늬는 귞늌시띌 Mouse's Tale 의 셈을 귞대로 썚요. 66판의 닀섯은 혞프슀태터의 gridfont 륌 녞람로 닀시 짓는 격자 Ꞁꌎ(Gridfonts), Ꞁ에서 ë–šì–Žì§„ Ꞁ자륌 섞얎 띌읎녞타읎프 자판의 찚례와 견죌는 막대(Etaoin Shrdlu), 낱말곌 ê·ž 음화륌 막대 격자로 번갈아 드러낎는 읞쇄(Barrier Grid), 윀곜 Ꞁ자가 RGB LED 격자에 래슀터되는 êžž(Subpixel), 유니윔드 한Ꞁ 11,172 음절을 섞는 죌행 Ʞ록계(Syllable Odometer)예요. @@ -4322,6 +4409,14 @@ Flock, Harmonograph, Big Type 은 처음 화멎읎 빚강읎에요. 69판 뒀에 핮 볌 것읎에요. 7섞귞뚌튞륌 돌렀 한Ꞁ 자몚로 읜는 놀읎가 있얎요. 2 는 귞대로 ㄹ, 돌며 7 은 ㄎ, 0 은 ㅁ, 1 은 ㅣ 로 읜혀서 숫자 몇 개로 「륎」나 「니」 같은 음절을 ì¹  수 있얎요. Nine-fold Seal 의 ì ‘êž° 규칙을 백묞(癜文)윌로 뒀집얎 Ꞁ자륌 종읎색윌로 낚Ʞ고 바탕을 읞죌로 찍거나, 칞의 마디륌 한 가닥 Ꞟ로 몚두 읎얎 한 음절읎 한붓귞늬Ʞ 믞로가 되게 핮 볌 만핎요. 롱고롱고처럌 아래 왌쪜 쀄에서 시작핎 위로 올띌가며 판을 돌렀 읜는 쓰Ʞ나, 한 닚은 낎렀가고 닀음 닚은 올띌였는 섞로 부슀튞로페돈도 Boustrophedon 의 밭에서 돌요. 같은 64×128 바닥에서 크녞테 지도 대신 싱크혌(Sinkhorn) 반복윌로 푌 최적 수송 지도륌 믞늬 구워 두멎, Focus 륌 지나칠 때 두 지도가 낚Ʞ는 자국을 나란히 섞욞 수 있얎요. 큎띌베 등의 녌묞에서 Letter Melt 가 뺀 점탄성 용수철을 더하멎 녹은 획읎 꿀처럌 싀을 끌닀가 끊얎질 거예요. +71판의 닀섯은 원Ʞ둥 거욞에 비쳐알 바로 서는 바닥 Ꞁ(Catoptric Anamorphosis), 폌페읎 낱말 넀몚의 25자가 날아 PATERNOSTER 십자가가 되는 판(Sator Square), 큰 볞 Ꞁ자륌 따띌 돌멎 작고 êž°ìšžê³  홀쭉한 펀치륌 깎는 링크(Benton Pantograph), 읜을 수 없는 지은 Ꞁ자륌 섞로쀄로 찍는 쉬빙의 책(Book from the Sky), 킀쌀로의 묞장에서 Ꞁ자가 ë–šì–Žì ž 자늬채움 Ꞁ읎 되는 정렬(Lorem Ipsum)읎에요. + +71판에서 볞 것읎에요. 읎번에도 닀섯읎 ë‹€ Ꞁ자 ꌎ을 윔드에 적었고, 옮겚 쓎 Ꞁꌎ은 없얎요. 비친 Ꞁ자는 옆볎닀 위아래에 앜했얎요. Catoptric Anamorphosis 의 칎메띌가 축을 30도 돌아도 ê±°ìšž 속 낱말은 11도 밀멮 뿐 읜히는데, 눈읎 5도 였륎낎늬멎 40쀄 낱말읎 45쀄곌 35쀄로 늘고 쀄얎요. 대칭읞 Ꞁ자판은 날지 않는 Ꞁ자륌 낚게얎요. Sator Square 에서 TENET 의 N 은 판곌 십자가에서 같은 자늬띌 크Ʞ만 쀄고, 판의 25칞읎 ë„€ 대칭 아래 아홉 묶음읎띌 ë„€ 방향윌로 읜는 빛읎 늘 같은 Ꞁ자 위에 있얎요. Ʞ계로 깎는 Ꞁ자는 랔랭크가 Ꞁ자 크Ʞ륌 정했얎요. Benton Pantograph 에서 êž°ìšžêž° 0.45 의 Ꞁ자는 한 자마닀 대묞자 높읎의 0.45 배륌 더 찚지핎서, 62 LED 에 맞춘 FAMILY 쀄의 Ꞁ자가 9.9 LED 에서 6.1 LED 로 쀄얎요. 지얎낞 Ꞁ자는 싀제 Ꞁ자륌 플하Ʞ 얎렀웠얎요. Book from the Sky 는 틀곌 부품읎 귞대로 만드는 싀제 Ꞁ자 78 개륌 표로 막았는데도 å°ž 안에 叀 륌 넣은 居 가 빠젞나가 찍혔얎요. ë–šì–Žì§„ Ꞁ자는 닀시 쓰였얎요. Lorem Ipsum 읎 킀쌀로로 돌아갈 때 더할 67 자는 ë‹€ 더믞에 있는데, 아래 Ꞁ자가 뚌저 뛰얎나가멎 ê·ž 위 Ꞁ자가 굎러떚얎지는 쀑읎띌 64 자만 뛰얎였륎고 셋은 위에서 ë–šì–Žì žìš”. + +Ʞ능 감은 읎번에도 Ꞁ자륌 윔드 밖에서 받는 Ꞟ로 몚였고, 받윌렀는 것읎 Ꞁ자의 자늬와 ꌎ로 갈렞얎요. Sator Square 는 Text 레읎얎가 Ꞁ자마닀 제 비튞맵곌 자늬륌 넘겚 죌Ʞ륌, Lorem Ipsum 은 정한 폭윌로 쀄을 나누고 양쪜을 맞춘 Ꞁ자의 자늬륌 넘겚 죌Ʞ륌 바랐얎요. Benton Pantograph 는 Lab Ꞁꌎ의 윀곜을 점 목록윌로 받Ʞ륌 바랐얎요. Catoptric Anamorphosis 는 윔드가 고륞 상자와 핎상도에 귞늰 덮임 배엎을 바랐는데, 거욞에 비칠 낱말을 LED 하나당 4×4 로 귞렀알 핎서예요. Book from the Sky 는 êž°êž° Ꞁꌎ의 한자 비튞맵을 받아 지얎낞 Ꞁ자가 싀제 Ꞁ자와 겹치는지 견죌Ʞ륌 바랐얎요. + +71판 뒀에 핮 볌 것읎에요. 장룚읎 볌슈자륎(Jean-Louis Vaulezard)가 1630년에 닀룬 원뿔 ê±°ìšž 착시는 원뿔 ꌭ짓점 위에서 낎렀닀볌 때만 둥귌 바닥 귞늌읎 낱말로 몚여서, Catoptric Anamorphosis 의 ꎑ선을 원뿔에서 꺟고 칎메띌륌 위에 두멎 돌요. 퀞투슀 섞레누슀 사몚니쿠슀의 『Liber Medicinalis』(3섞Ʞ 묎렵)가 부적윌로 적게 한 ABRACADABRA 삌각형은 쀄마닀 끝 Ꞁ자륌 하나씩 떌얎 엎한 쀄로 쀄얎서, Sator Square 의 Ꞁꌎ 섞 벌로 128쀄을 위에서 아래로 쓞 수 있얎요. 알람레히튞 뒀러의 『잡정 지칚』(1525)은 자와 컎퍌슀로 로마 대묞자륌 짓는 법읎띌, Benton Pantograph 의 윀곜 닀각형 대신 원혞와 직선읎 찚례로 귞얎지며 Ꞁ자가 서는 판읎 돌요. 쉬빙읎 1990년대 쀑반부터 쓎 영묞 방첎 서법(Square Word Calligraphy)은 영얎 낱말의 알파벳을 한자처럌 한 넀몚에 몚아 읜히게 썚서, Book from the Sky 의 틀 여덟에 부품 대신 띌틎 Ꞁ자륌 넣윌멎 같은 쪜에서 읜히는 가짜 한자가 나와요. 랔띌디믞륎 레벀시테읞(Vladimir Levenshtein)의 펞집 거늬(1965)처럌 바꟞Ʞ륌 허띜하멎 Lorem Ipsum 에서 잘늬고 더핎지던 Ꞁ자 쌍읎 제자늬에서 닀륞 Ꞁ자로 바뀌얎 더믞로 가는 Ꞁ자가 쀄얎요. + ### 화장품 팝업 죌제 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 마닀 한 번만 바뀌는 셈윌로 귞늎 만핎요. diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index bc9ea741e..d766a2747 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

팹턮 연구

- - + + - + +