From f0a47863f6cac35d830fa908207a45d145b8859f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 12:31:20 +0000 Subject: [PATCH 01/19] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=2067?= =?UTF-8?q?=ED=8C=90=20=ED=83=80=EC=9D=B4=ED=8F=AC=20=EB=8B=A4=EC=84=AF?= =?UTF-8?q?=EA=B3=BC=20=EC=88=98=ED=95=99=20=C2=B7=20=EC=B6=94=EC=83=81=20?= =?UTF-8?q?=EB=8B=A4=EC=84=AF=EC=9D=B4=20=EC=84=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo From a8de89d75e6c1651b30d72b2e9c4f16b980af00b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 12:59:08 +0000 Subject: [PATCH 02/19] incoming: Shannon's Approximations draft, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../patterns/shannon-approximations.js | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 incoming/pattern-study/patterns/shannon-approximations.js diff --git a/incoming/pattern-study/patterns/shannon-approximations.js b/incoming/pattern-study/patterns/shannon-approximations.js new file mode 100644 index 000000000..7026994cc --- /dev/null +++ b/incoming/pattern-study/patterns/shannon-approximations.js @@ -0,0 +1,287 @@ +// ===== Patternflow pattern ===== +// Title: Shannon's Approximations +// Family: type +// Idea: A page of 21 lines of 16 small capitals is typed over and over by a cursor that writes Claude Shannon's approximations to English, from 27 equiprobable symbols up through letter, digram and trigram statistics to word frequencies and word pairs, all counted from the opening of Pride and Prejudice, and every run of letters that is a real word of that text lights up, so the page turns from dim noise into lit words as the order rises. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the series of approximations to English in A Mathematical Theory of Communication, section 3 (Claude Shannon, 1948), counted from the opening of Pride and Prejudice (Jane Austen, 1813), 3x5 capitals drawn for this pattern, pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Order=0..5, Blackout=0..1 +// +// Knob 1: Hue. Turns the page round the wheel: at 0 the words are sea green, word pairs from the book warm gold and the noise between them slate. +// Knob 2: Speed. Letters a second, eight times the knob: 16 at 2, so the cursor goes round the 336 cells of the page in 21 s; 1.6 at 0.2, 24 at 3. Fresh letters burn white and cool over a second. +// Knob 3: Order. Shannon's six steps, in whole steps: 0 the 27 symbols equiprobable, 1 letter frequencies, 2 digrams, 3 trigrams, 4 words by their frequency, 5 each word drawn after the word before it. A change rewrites the whole page from the top in 0.6 s. +// Knob 4: Blackout. How the runs that are not words show: dim letters at 0, and turned up they sink under dark ink bars until at 1 only the real words stand on a blacked-out page. + +const W = 64, H = 128; +const { floor, min, max, exp, round } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The opening of Pride and Prejudice, chapter 1 up to Lizzy, cut down to +// Shannon's 27 symbols: capitals and the space. 2,812 symbols, 577 words, +// 236 different words. +const TEXT = + "IT IS A TRUTH UNIVERSALLY ACKNOWLEDGED THAT A SINGLE MAN IN POSSESSION OF A " + + "GOOD FORTUNE MUST BE IN WANT OF A WIFE HOWEVER LITTLE KNOWN THE FEELINGS OR " + + "VIEWS OF SUCH A MAN MAY BE ON HIS FIRST ENTERING A NEIGHBOURHOOD THIS TRUTH " + + "IS SO WELL FIXED IN THE MINDS OF THE SURROUNDING FAMILIES THAT HE IS " + + "CONSIDERED THE RIGHTFUL PROPERTY OF SOME ONE OR OTHER OF THEIR DAUGHTERS MY " + + "DEAR MR BENNET SAID HIS LADY TO HIM ONE DAY HAVE YOU HEARD THAT NETHERFIELD " + + "PARK IS LET AT LAST MR BENNET REPLIED THAT HE HAD NOT BUT IT IS RETURNED SHE " + + "FOR MRS LONG HAS JUST BEEN HERE AND SHE TOLD ME ALL ABOUT IT MR BENNET MADE " + + "NO ANSWER DO YOU NOT WANT TO KNOW WHO HAS TAKEN IT CRIED HIS WIFE " + + "IMPATIENTLY YOU WANT TO TELL ME AND I HAVE NO OBJECTION TO HEARING IT THIS " + + "WAS INVITATION ENOUGH WHY MY DEAR YOU MUST KNOW MRS LONG SAYS THAT " + + "NETHERFIELD IS TAKEN BY A YOUNG MAN OF LARGE FORTUNE FROM THE NORTH OF " + + "ENGLAND THAT HE CAME DOWN ON MONDAY IN A CHAISE AND FOUR TO SEE THE PLACE " + + "AND WAS SO MUCH DELIGHTED WITH IT THAT HE AGREED WITH MR MORRIS IMMEDIATELY " + + "THAT HE IS TO TAKE POSSESSION BEFORE MICHAELMAS AND SOME OF HIS SERVANTS ARE " + + "TO BE IN THE HOUSE BY THE END OF NEXT WEEK WHAT IS HIS NAME BINGLEY IS HE " + + "MARRIED OR SINGLE OH SINGLE MY DEAR TO BE SURE A SINGLE MAN OF LARGE FORTUNE " + + "FOUR OR FIVE THOUSAND A YEAR WHAT A FINE THING FOR OUR GIRLS HOW SO HOW CAN " + + "IT AFFECT THEM MY DEAR MR BENNET REPLIED HIS WIFE HOW CAN YOU BE SO TIRESOME " + + "YOU MUST KNOW THAT I AM THINKING OF HIS MARRYING ONE OF THEM IS THAT HIS " + + "DESIGN IN SETTLING HERE DESIGN NONSENSE HOW CAN YOU TALK SO BUT IT IS VERY " + + "LIKELY THAT HE MAY FALL IN LOVE WITH ONE OF THEM AND THEREFORE YOU MUST " + + "VISIT HIM AS SOON AS HE COMES I SEE NO OCCASION FOR THAT YOU AND THE GIRLS " + + "MAY GO OR YOU MAY SEND THEM BY THEMSELVES WHICH PERHAPS WILL BE STILL BETTER " + + "FOR AS YOU ARE AS HANDSOME AS ANY OF THEM MR BINGLEY MAY LIKE YOU THE BEST " + + "OF THE PARTY MY DEAR YOU FLATTER ME I CERTAINLY HAVE HAD MY SHARE OF BEAUTY " + + "BUT I DO NOT PRETEND TO BE ANYTHING EXTRAORDINARY NOW WHEN A WOMAN HAS FIVE " + + "GROWN UP DAUGHTERS SHE OUGHT TO GIVE OVER THINKING OF HER OWN BEAUTY IN SUCH " + + "CASES A WOMAN HAS NOT OFTEN MUCH BEAUTY TO THINK OF BUT MY DEAR YOU MUST " + + "INDEED GO AND SEE MR BINGLEY WHEN HE COMES INTO THE NEIGHBOURHOOD IT IS MORE " + + "THAN I ENGAGE FOR I ASSURE YOU BUT CONSIDER YOUR DAUGHTERS ONLY THINK WHAT " + + "AN ESTABLISHMENT IT WOULD BE FOR ONE OF THEM SIR WILLIAM AND LADY LUCAS ARE " + + "DETERMINED TO GO MERELY ON THAT ACCOUNT FOR IN GENERAL YOU KNOW THEY VISIT " + + "NO NEWCOMERS INDEED YOU MUST GO FOR IT WILL BE IMPOSSIBLE FOR US TO VISIT " + + "HIM IF YOU DO NOT YOU ARE OVER SCRUPULOUS SURELY I DARE SAY MR BINGLEY WILL " + + "BE VERY GLAD TO SEE YOU AND I WILL SEND A FEW LINES BY YOU TO ASSURE HIM OF " + + "MY HEARTY CONSENT TO HIS MARRYING WHICHEVER HE CHOOSES OF THE GIRLS THOUGH I " + + "MUST THROW IN A GOOD WORD FOR MY LITTLE LIZZY "; + +// Symbol 0 is the space, 1 to 26 the letters. The text is read as a ring +// (its last space leads back to IT), so every context it holds has a +// letter after it. +const ABC = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const SRC = Uint8Array.from(TEXT, (c) => ABC.indexOf(c)); +const N = SRC.length; +// Shannon's way of drawing the higher orders: open the book at a random +// place where the last letters occur and copy the letter that follows. The +// lists below are those places, by the last letter and by the last two. +const AFTER1 = Array.from({ length: 27 }, () => []); +const AFTER2 = Array.from({ length: 27 * 27 }, () => []); +for (let i = 0; i < N; i++) { + AFTER1[SRC[i]].push(i); + AFTER2[SRC[(i + N - 1) % N] * 27 + SRC[i]].push(i); +} +// Words: the vocabulary, the running text as word ids, where each word +// occurs, and every pair of words that stands side by side in the book. +const TOKS = TEXT.trim().split(" "); +const VOCAB = new Map(), WORDS = []; +for (const w of TOKS) if (!VOCAB.has(w)) VOCAB.set(w, WORDS.length), WORDS.push(w); +const NV = WORDS.length, TID = TOKS.map((w) => VOCAB.get(w)); +const SUCC = Array.from({ length: NV }, () => []); +const PAIRS = new Set(); +for (let k = 0; k + 1 < TID.length; k++) SUCC[TID[k]].push(k), PAIRS.add(TID[k] * NV + TID[k + 1]); + +// 3x5 capitals, rows top to bottom, a 1 is a lit LED. +const GLYPHS = [ + "010101111101101", "110101110101110", "011100100100011", "110101101101110", "111100110100111", + "111100110100100", "011100101101011", "101101111101101", "111010010010111", "001001001101010", + "101101110101101", "100100100100111", "101111111101101", "111101101101101", "010101101101010", + "110101110100100", "010101101110011", "110101110101101", "011100010001110", "111010010010010", + "101101101101111", "101101101101010", "101101111111101", "101101010101101", "101101010010010", + "111001010100111", +]; +const FONT = GLYPHS.map((g) => Uint8Array.from(g, (c) => +c)); + +// The page: 16 cells across, 4 LEDs each, and 21 lines 6 LEDs apart. +const COLS = 16, ROWS = 21, CELLS = COLS * ROWS, CW = 4, CH = 6, TOP = 1; +// The fresh-ink glow fades with this time constant, and a wipe writes one +// line every WIPE seconds. +const GLOW = 1, WIPE = 0.03; + +const rnd = (n) => floor(Math.random() * n); + +// The next symbol at the current order. +function nextSymbol(S) { + const o = S.order; + let c; + if (o >= 4) { + if (S.qi >= S.q.length) { + let id = -1; + if (o === 5 && S.pw >= 0 && SUCC[S.pw].length) id = TID[SUCC[S.pw][rnd(SUCC[S.pw].length)] + 1]; + if (id < 0) id = TID[rnd(TID.length)]; + S.q = (S.h1 === 0 ? "" : " ") + WORDS[id] + " "; + S.qi = 0; + S.pw = id; + } + c = ABC.indexOf(S.q[S.qi++]); + } else { + S.q = "", S.qi = 0, S.pw = -1; + if (o === 0) c = rnd(27); + else { + let list = o === 3 ? AFTER2[S.h0 * 27 + S.h1] : o === 2 ? AFTER1[S.h1] : null; + // A context the book never has (a Q, or two letters a lower order + // put side by side) falls back to a lower order. + if (o === 3 && !list.length) list = AFTER1[S.h1]; + c = list && list.length ? SRC[(list[rnd(list.length)] + 1) % N] : SRC[rnd(N)]; + } + } + S.h0 = S.h1, S.h1 = c; + return c; +} + +// Which cells are words. The page is read as one ring of text in typing +// order, so a word may run over a line end, and from the bottom line back +// to the top. Each run of letters is looked up in the vocabulary; two words +// one space apart that stand side by side in the book are a pair. +function classify(S) { + const P = S.P, C = S.cls, L = S.len; + C.fill(0), L.fill(0); + let s0 = -1; + for (let i = 0; i < CELLS; i++) if (P[i] === 0) { s0 = i; break; } + if (s0 < 0) return C.fill(1); + const runs = []; + let i = 1; + while (i <= CELLS) { + if (P[(s0 + i) % CELLS] === 0) { i++; continue; } + const st = i; + let word = ""; + while (i <= CELLS && P[(s0 + i) % CELLS] !== 0) word += ABC[P[(s0 + i++) % CELLS]]; + const id = VOCAB.get(word); + runs.push([st, i - st, id === undefined ? -1 : id, 0]); + } + for (let r = 0; r + 1 < runs.length; r++) { + const a = runs[r], b = runs[r + 1]; + if (a[2] >= 0 && b[2] >= 0 && b[0] === a[0] + a[1] + 1 && PAIRS.has(a[2] * NV + b[2])) a[3] = b[3] = 1; + } + let words = 0, letters = 0; + for (const [st, n, id, pair] of runs) { + const k = id < 0 ? 1 : pair ? 3 : 2; + letters += n; + if (id >= 0) words += n; + for (let j = 0; j < n; j++) { + const p = (s0 + st + j) % CELLS; + C[p] = k, L[p] = n; + } + } + S.share = words / max(1, letters); +} + +// A fresh page at the current order, as if just typed at the current speed. +function fillPage(S) { + for (let i = 0; i < CELLS; i++) { + S.P[i] = nextSymbol(S); + S.T[i] = S.t - (CELLS - 1 - i) / S.cps; + } + S.cur = 0, S.acc = 0, S.dirty = true; +} + +export function setup(params) { + params.s = { + P: new Uint8Array(CELLS), T: new Float32Array(CELLS), NEXT: new Uint8Array(CELLS), + cls: new Uint8Array(CELLS), len: new Uint8Array(CELLS), + t: 0, cps: 16, order: -1, cur: 0, acc: 0, dirty: true, share: 0, + h0: 0, h1: 0, q: "", qi: 0, pw: -1, wipe: -1, wiped: 0, hue: 0, black: 0.06, + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.t += dt; + S.hue = k[0]; + S.cps = 8 * cl(k[1], 0.2, 3); + S.black = cl(k[3], 0, 1); + const order = cl(round(k[2]), 0, 5); + if (S.order < 0) { + S.order = order; + fillPage(S); + } else if (order !== S.order) { + // A new order writes a whole new page, revealed a line at a time from + // the top; the cursor then goes on from the top-left cell. + S.order = order; + for (let i = 0; i < CELLS; i++) S.NEXT[i] = nextSymbol(S); + S.wipe = S.t, S.wiped = 0; + } + if (S.wipe >= 0) { + const upto = min(ROWS, floor((S.t - S.wipe) / WIPE) + 1); + for (; S.wiped < upto; S.wiped++) + for (let c = 0; c < COLS; c++) { + const i = S.wiped * COLS + c; + S.P[i] = S.NEXT[i], S.T[i] = S.t; + } + S.dirty = true; + if (S.wiped >= ROWS) S.wipe = -1, S.cur = 0, S.acc = 0; + else return; + } + S.acc += dt * S.cps; + while (S.acc >= 1) { + S.acc -= 1; + S.P[S.cur] = nextSymbol(S); + S.T[S.cur] = S.t; + S.cur = (S.cur + 1) % CELLS; + S.dirty = true; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.order < 0) return; + if (S.dirty) classify(S), (S.dirty = false); + const h = S.hue, B = S.black; + // Colours by class: 1 noise, 2 a word, 3 a word of a pair from the book. + const noise = hsv(h + 0.62, 0.5, 0.42), word = hsv(h + 0.45, 0.8, 1), pair = hsv(h + 0.12, 0.5, 1); + const ink = hsv(h + 0.64, 0.55, 0.2 * B); + const nl = (1 - B) * (1 - B); + const blink = S.wipe < 0 && (S.t * 2.5) % 1 < 0.6; + for (let i = 0; i < CELLS; i++) { + const x0 = (i % COLS) * CW, y0 = TOP + floor(i / COLS) * CH; + const sym = S.P[i], k = S.cls[i]; + if (i === S.cur && blink) { + for (let y = 0; y < 5; y++) for (let x = 0; x < 3; x++) display.setPixel(x0 + x, y0 + y, 200, 200, 190); + continue; + } + if (sym === 0) continue; + let col, lv; + if (k === 1) (col = noise), (lv = nl); + else { + col = k === 3 ? pair : word; + const n = S.len[i]; + lv = n >= 3 ? 1 : n === 2 ? 0.8 : 0.6; + } + // Fresh ink burns toward white and cools into its class colour. + const g = exp(-(S.t - S.T[i]) / GLOW), wv = 235 * g; + const r = 255 * col[0] * lv * (1 - g) + wv, gg = 255 * col[1] * lv * (1 - g) + wv, b = 255 * col[2] * lv * (1 - g) + wv; + const bar = k === 1 && B > 0.01; + const f = FONT[sym - 1]; + for (let y = 0, j = 0; y < 5; y++) + for (let x = 0; x < 3; x++, j++) { + if (f[j]) { + // Under the ink a noise letter fades into its bar. + if (bar) display.setPixel(x0 + x, y0 + y, min(255, r + 255 * ink[0]), min(255, gg + 255 * ink[1]), min(255, b + 255 * ink[2])); + else display.setPixel(x0 + x, y0 + y, min(255, r), min(255, gg), min(255, b)); + } else if (bar) display.setPixel(x0 + x, y0 + y, 255 * ink[0], 255 * ink[1], 255 * ink[2]); + else if (g > 0.05) display.setPixel(x0 + x, y0 + y, 0.2 * wv, 0.2 * wv, 0.2 * wv); + } + } + // The bottom row is a gauge: the share of the page's letters that stand + // in real words, about 0, 2, 6 and 27% at orders 0 to 3 and 98% from 4 on. + const lit = round(W * S.share); + for (let x = 0; x < W; x++) { + const c = x < lit ? word : noise, v = x < lit ? 200 : 110; + display.setPixel(x, H - 1, v * c[0], v * c[1], v * c[2]); + } +} From ab211422ff52dc1b14c8209fb69168669c31469c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:01:28 +0000 Subject: [PATCH 03/19] incoming: Silencio draft, pattern round 67 --- incoming/pattern-study/patterns/silencio.js | 287 ++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 incoming/pattern-study/patterns/silencio.js diff --git a/incoming/pattern-study/patterns/silencio.js b/incoming/pattern-study/patterns/silencio.js new file mode 100644 index 000000000..25def7eb3 --- /dev/null +++ b/incoming/pattern-study/patterns/silencio.js @@ -0,0 +1,287 @@ +// ===== Patternflow pattern ===== +// Title: Silencio +// Family: type +// Idea: Eugen Gomringer's constellation of fourteen words round one empty place is set on the panel, the Latin words turned a quarter like book spines and the Hangul ones upright, and the empty place drifts from slot to slot, the word it enters trailing off letter by letter while the word it leaves is spoken back in from its first letter, as a slow reading light passes along every line and goes dark over the gap. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: silencio (Eugen Gomringer, 1953), the best-known constellation of concrete poetry, with schweigen and silence, its German and English versions; the Latin letters and the Hangul 침묵 and 고요 drawn for this pattern, pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Word=0..4, Hush=0..1 +// +// Knob 1: Hue. Turns the words round the wheel from a pale azure at 0; the words nearest the silence lean a little towards violet. +// Knob 2: Speed. How fast the reading light runs and the silence moves: at 2 the light takes a second over each word and pauses 0.6 between words, and the silence moves on every 3.6 seconds (5.4 when it is home); at 0.2 all of it ten times slower. The glow behind the light dies in 1.1 seconds at any speed, so a fast reading leaves whole words lit. +// Knob 3: Word. 0 silencio, 1 schweigen, 2 silence, 3 침묵, 4 고요. The Latin words stand turned in five columns of three, the Hangul in Gomringer's own three columns of five; a new word empties the field and speaks back in. +// Knob 4: Hush. Up to 0.1 one place is silent, as in the poem. Above that the silence spreads from it to its neighbours, trailing their words off, until at 1 nine of the fifteen places are silent and only the outer words still speak. +// Buttons: any button moves the silence on at once. + +const W = 64, H = 128, N = W * H; +const { floor, round, min, max, abs, sqrt, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Latin. A lowercase face 9 rows tall: rows 0 and 1 hold ascenders, +// 2 to 6 the x-height, 7 and 8 the one descender (g). Only the ten letters +// the three words use. Most are 3 wide; i is 1, l 2, w 5. +const LATIN = { + s: ["...", "...", ".##", "#..", ".#.", "..#", "##.", "...", "..."], + i: ["#", ".", "#", "#", "#", "#", "#", ".", "."], + l: ["#.", "#.", "#.", "#.", "#.", "#.", ".#", "..", ".."], + e: ["...", "...", ".#.", "#.#", "###", "#..", ".##", "...", "..."], + n: ["...", "...", "##.", "#.#", "#.#", "#.#", "#.#", "...", "..."], + c: ["...", "...", ".##", "#..", "#..", "#..", ".##", "...", "..."], + o: ["...", "...", ".#.", "#.#", "#.#", "#.#", ".#.", "...", "..."], + h: ["#..", "#..", "##.", "#.#", "#.#", "#.#", "#.#", "...", "..."], + w: [".....", ".....", "#...#", "#...#", "#.#.#", "#.#.#", ".#.#.", ".....", "....."], + g: ["...", "...", ".##", "#.#", "#.#", "#.#", ".##", "..#", "##."], +}; + +// A Latin word turned a quarter clockwise, the way a spine reads in English: +// the tops of the letters face right and the word runs down. Turned so, a +// page of lines becomes columns read top to bottom and right to left, the +// order of Korean set in vertical columns. A word is a list of letters, each +// a flat list of LED offsets x, y from the word's top left. +function latinWord(text) { + const rows = text.split("").some((ch) => LATIN[ch].slice(7).some((r) => r.includes("#"))) ? 9 : 7; + const letters = []; + let y = 0; + for (const ch of text) { + const g = LATIN[ch], px = []; + for (let gy = 0; gy < rows; gy++) + for (let gx = 0; gx < g[gy].length; gx++) if (g[gy][gx] === "#") px.push(rows - 1 - gy, y + gx); + letters.push(px); + y += g[0].length + 1; + } + return { w: rows, h: y - 1, letters }; +} + +// ---- Hangul. Each syllable is 7 by 11 and each digit marks the jamo a dot +// belongs to, in writing order, so a Hangul word goes quiet and comes back +// jamo by jamo: 침묵 has six letters (ㅊ ㅣ ㅁ, ㅁ ㅜ ㄱ), 고요 four (ㄱ ㅗ, ㅇ ㅛ). +const HANGUL = { + 침: ["..1...2", "11111.2", "..1...2", ".1.1..2", "1...1.2", "......2", ".......", ".33333.", ".3...3.", ".3...3.", ".33333."], + 묵: [".11111.", ".1...1.", ".1...1.", ".11111.", ".......", "2222222", "...2...", ".......", ".33333.", ".....3.", ".....3."], + 고: [".......", ".11111.", ".....1.", ".....1.", ".....1.", ".....1.", ".......", "...2...", "...2...", "2222222", "......."], + 요: [".......", "..111..", ".1...1.", ".1...1.", ".1...1.", "..111..", ".......", "..2.2..", "..2.2..", "2222222", "......."], +}; + +function hangulWord(text) { + const letters = []; + [...text].forEach((syl, k) => { + const g = HANGUL[syl], parts = []; + for (let y = 0; y < g.length; y++) + for (let x = 0; x < 7; x++) { + const d = +g[y][x]; + if (!d) continue; + (parts[d - 1] = parts[d - 1] || []).push(k * 8 + x, y); + } + letters.push(...parts); + }); + return { w: 15, h: 11, letters }; +} + +const WORDS = ["silencio", "schweigen", "silence", "침묵", "고요"]; + +// Fifteen places. Gomringer set three words to a line in five lines with +// the middle of the third line empty. The Latin words, turned, fill five +// columns of three (the first line is the right column); the Hangul ones +// stand in his three columns of five. Either way the empty place is the +// middle one. +function layout(k) { + const latin = k < 3; + const word = latin ? latinWord(WORDS[k]) : hangulWord(WORDS[k]); + const cols = latin ? 5 : 3, rows = latin ? 3 : 5, slots = []; + for (let r = 0; r < rows; r++) + for (let c = 0; c < cols; c++) + slots.push( + latin + ? { c, r, x: round((c + 0.5) * 12.8 - word.w / 2), y: round(((r + 0.5) * H) / 3 - word.h / 2), line: cols - 1 - c, at: r } + : { c, r, x: [3, 24, 46][c], y: 8 + 25 * r, line: r, at: c }, + ); + const n = word.letters.length; + // A reading light runs along each of the five lines word by word, a pause + // the length of 0.6 of a word between words. + const step = n * 1.6; + return { word, n, cols, rows, slots, home: (rows >> 1) * cols + (cols >> 1), step, line: step * 3 }; +} + +const HOP = 3.6, TRANS = 1.5, RATE = 1 / 1.6; + +export function setup(params) { + params.s = { T: 0, k: -1, L: null, q: null, qt: null, at: 0, prev: -1, next: 0, cx: 0, cy: 0, fx: 0, fy: 0, glide: 9, hush: 0, Lv: new Float32Array(N), Hu: new Float32Array(N), Bo: new Float32Array(N) }; +} + +// The places sorted into rings by distance from the silence; the hush +// budget (1 place at Hush 0.1 or less, up to 9) fills them ring by ring and +// shares out a ring it cannot fill, so neighbours trail off together. +function targets(S) { + const { slots, n } = S.L, a = slots[S.at]; + const budget = 1 + (max(0, S.hush - 0.1) / 0.9) * 8; + const d = slots.map((s) => round(sqrt((s.c - a.c) ** 2 + (s.r - a.r) ** 2) * 1000)); + const rings = [...new Set(d)].sort((x, y) => x - y); + let left = budget; + S.reach = 0; + for (const ring of rings) { + const idx = []; + d.forEach((v, j) => v === ring && idx.push(j)); + const f = cl(left / idx.length, 0, 1); + for (const j of idx) S.qt[j] = f * n; + if (f > 0) S.reach = ring / 1000; + left -= idx.length; + } +} + +function useWord(S, k, first) { + S.k = k; + S.L = layout(k); + const m = S.L.slots.length; + S.q = new Float32Array(m); + S.qt = new Float32Array(m); + S.at = S.L.home; + S.prev = -1; + const h = S.L.slots[S.at]; + S.cx = S.fx = h.c; + S.cy = S.fy = h.r; + S.glide = 9; + targets(S); + // The first word is there at once. A new word empties the field and comes + // back place by place in reading order, each spoken from its first letter. + S.L.slots.forEach((s, j) => (S.q[j] = first ? S.qt[j] : S.L.n + 1 + (s.line * 3 + s.at) * 0.2 * S.L.n)); + S.next = S.T + (first ? HOP * 0.3 : HOP * 1.6); +} + +// The silence moves to a neighbouring place, never straight back, and +// leans home: a place's weight falls with the square of its distance from +// the middle, so the poem's own form comes round often. +function hop(S) { + const { slots, cols, rows, home } = S.L, a = slots[S.at]; + const h = slots[home], opts = [], wts = []; + for (const [dc, dr] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { + const c = a.c + dc, r = a.r + dr; + if (c < 0 || r < 0 || c >= cols || r >= rows) continue; + const j = r * cols + c; + if (j === S.prev) continue; + opts.push(j); + wts.push(1 / (1 + (c - h.c) ** 2 + (r - h.r) ** 2)); + } + let u = Math.random() * wts.reduce((x, y) => x + y, 0), j = opts[0]; + for (let i = 0; i < opts.length; i++) if ((u -= wts[i]) <= 0) { + j = opts[i]; + break; + } + S.fx = S.cx; + S.fy = S.cy; + S.prev = S.at; + S.at = j; + S.glide = 0; + targets(S); + S.next = S.T + (j === home ? HOP * 1.5 : HOP); +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const want = cl(round(k[2]), 0, 4); + if (S.k < 0) { + S.hush = cl(k[3], 0, 1); + useWord(S, want, true); + } else if (want !== S.k) useWord(S, want, false); + if (abs(k[3] - S.hush) > 1e-4) { + S.hush = cl(k[3], 0, 1); + targets(S); + } + S.speed = cl(k[1], 0.05, 3); + const step = (dt * S.speed) / 2; + S.T += step; + if ((input.btnPressed || []).some(Boolean)) S.next = S.T; + if (S.T >= S.next) hop(S); + // Letters go and come at n letters in TRANS. + const rate = (S.L.n / TRANS) * step; + for (let j = 0; j < S.q.length; j++) { + const d = S.qt[j] - S.q[j]; + S.q[j] += abs(d) <= rate ? d : Math.sign(d) * rate; + } + S.glide = min(9, S.glide + step / TRANS); + const e = smooth(S.glide), a = S.L.slots[S.at]; + S.cx = S.fx + (a.c - S.fx) * e; + S.cy = S.fy + (a.r - S.fy) * e; +} + +export function draw(display, params) { + const S = params.s; + if (!S.L) return; + const { Lv, Hu, Bo } = S, { word, n, slots, step, line } = S.L; + Lv.fill(0); + const H0 = 0.56 + S.hue; + const pos = S.T * RATE * step; + // The sound dies away in 1.1 seconds whatever the speed, so a fast + // reader leaves a longer glow behind and a slow one barely any. + const tail = max(0.25, RATE * step * (S.speed / 2) * 1.1); + slots.forEach((s, j) => { + const quiet = S.q[j]; + if (quiet >= n) return; + // Words near the silence speak more softly: full voice two places + // beyond the edge of the silence. + const dist = sqrt((s.c - S.cx) ** 2 + (s.r - S.cy) ** 2); + const near = 1 - smooth((dist - S.reach) / 2); + const voice = 1 - 0.34 * near; + // Where this line's reading light is, in letters; lines are staggered + // by the golden ratio so the five lights never march together. + const r = (((pos + s.line * line * 0.618) % line) + line) % line; + const hue = H0 + 0.07 * near; + word.letters.forEach((px, i) => { + const a = cl(n - quiet - i, 0, 1); + if (a <= 0) return; + // A sound: quick to come, slow to die away behind the light. + let u = r - (s.at * step + i); + if (u < -line / 2) u += line; + else if (u > line / 2) u -= line; + const boost = u >= 0 ? exp(-u / tail) : exp(-u * u * 0.8); + const v = a * voice * (0.48 + 0.52 * boost); + for (let p = 0; p < px.length; p += 2) { + const o = (s.y + px[p + 1]) * W + s.x + px[p]; + Lv[o] = v; + Hu[o] = hue; + Bo[o] = boost; + } + }); + }); + // A faint glow one LED round each letter; the silent places hold no + // letters, and the places sit at least 3 LEDs apart, so it never reaches + // into the silence. + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + const o = y * W + x; + const v = Lv[o]; + if (v > 0) { + const sat = 0.58 * (1 - 0.55 * Bo[o]); + const [r, g, b] = hsv(Hu[o], sat, v); + display.setPixel(x, y, r * 255, g * 255, b * 255); + continue; + } + let best = 0, hb = 0; + for (let dy = -1; dy <= 1; dy++) { + const yy = y + dy; + if (yy < 0 || yy >= H) continue; + for (let dx = -1; dx <= 1; dx++) { + const xx = x + dx; + if (xx < 0 || xx >= W) continue; + const w = Lv[yy * W + xx] * (dx && dy ? 0.14 : 0.24); + if (w > best) (best = w), (hb = Hu[yy * W + xx]); + } + } + if (best <= 0) continue; + const [r, g, b] = hsv(hb, 0.75, best); + display.setPixel(x, y, r * 255, g * 255, b * 255); + } +} From 0dcbf19279ec61430bc8b9617b496d5bfb9c6f0c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:04:19 +0000 Subject: [PATCH 04/19] incoming: Dupin Cyclide draft, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/dupin-cyclide.js | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 incoming/pattern-study/patterns/dupin-cyclide.js diff --git a/incoming/pattern-study/patterns/dupin-cyclide.js b/incoming/pattern-study/patterns/dupin-cyclide.js new file mode 100644 index 000000000..b4a9f67f1 --- /dev/null +++ b/incoming/pattern-study/patterns/dupin-cyclide.js @@ -0,0 +1,206 @@ +// ===== Patternflow pattern ===== +// Title: Dupin Cyclide +// Family: geometry +// Idea: A torus turns at the top of the panel and below it its image under an inversion in a sphere turns with it, a Dupin cyclide drawn as the two families of circles that are its lines of curvature, every torus circle landing on a circle, while a band of light runs round both families on both surfaces at once. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Dupin cyclides (Charles Dupin, 1822), the surfaces whose lines of curvature are all circles, each the image of a torus, a circular cylinder or a circular cone under an inversion in a sphere; pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Shape=0..2, Distance=0..1 +// +// Knob 1: Hue. Turns both families round the wheel together: at 0 the circles round the tube are cyan and the circles round the hole rose. +// Knob 2: Speed. How fast the pair turns and the band of light runs: one turn in 11 seconds at 2, in 112 at 0.2. +// Knob 3: Shape. The torus tube against its centre circle, and so the cyclide's type: a ring torus and ring cyclide at 0 (tube half the radius), the horn pair at 1 where the hole closes to a point, and spindle pairs up to 2 where the tube is 1.6 times the radius and the surface passes through itself. +// Knob 4: Distance. The gap between the torus and the centre of inversion below it, in outer radii of the torus: 1.2 at 0 and 1.45 at the opening 0.06, where the cyclide's tube is 3.1 times as thick at the bottom as at the top, out to 29 at 1, where the image is the torus again to within 9 per cent. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, cos, sin, exp, PI, hypot } = Math; +const TAU = 2 * PI; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The torus: centre circle radius 1 in the plane z = 0, centred at (0, d, 0), +// so it stands on edge facing the viewer, and the centre of inversion is the +// origin, d below it. Meridians (u fixed, round the tube) and parallels (v +// fixed, round the hole) are its two families of lines of curvature. +const NU = 18, NV = 10, K = 60; +// Panel boxes: the torus above, the cyclide below. +const TOP = [1, 57], BOT = [63, 126], BW = 61; + +const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; +const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]; +const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s]; +const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; + +// The circle through three points: centre, radius and an orthonormal pair +// spanning its plane. +function circleThrough(A, B, C) { + const a = sub(A, C), b = sub(B, C), n = cross(a, b), nn = dot(n, n); + const c = add(C, scale(cross(sub(scale(b, dot(a, a)), scale(a, dot(b, b))), n), 1 / (2 * nn))); + const e1 = sub(A, c), rad = sqrt(dot(e1, e1)); + const u = scale(e1, 1 / rad), w = cross(scale(n, 1 / sqrt(nn)), u); + return { c, rad, u, w }; +} + +// One circle of the torus, as centre, radius and plane. +function torusCircle(fam, i, r, d, us) { + if (fam === 0) { + const u = us[i]; + return { c: [cos(u), d + sin(u), 0], rad: r, u: [cos(u), sin(u), 0], w: [0, 0, 1] }; + } + const v = (i / NV) * TAU; + return { c: [0, d, r * sin(v)], rad: 1 + r * cos(v), u: [1, 0, 0], w: [0, 1, 0] }; +} +const onCircle = (C, t) => add(C.c, add(scale(C.u, C.rad * cos(t)), scale(C.w, C.rad * sin(t)))); +// The inversion in the sphere of radius d about the origin, followed by the +// point reflection through it (an anti-inversion), so the image hangs below +// the centre the way the torus stands above it. Spheres go to spheres and +// circles to circles either way. +const invert = (p, k2) => scale(p, -k2 / dot(p, p)); + +function build(S) { + const r = S.r, d = (1 + r) * (1 + S.del), k2 = d * d; + // Where the meridians sit. The inversion stretches the side of the torus + // nearest the centre k2 / |p|^2 times, so meridians evenly spaced on the + // torus crowd on the image's thin side. Spacing them evenly in the square + // root of that stretch splits the crowding between the two surfaces, and + // the torus shows where the inversion pulls hardest. + const F = [0]; + for (let j = 1; j <= 720; j++) { + const u = -PI / 2 + ((j - 0.5) / 720) * TAU, q = cos(u) ** 2 + (d + sin(u)) ** 2; + F[j] = F[j - 1] + sqrt(k2 / q); + } + const us = []; + for (let i = 0, j = 0; i < NU; i++) { + const want = (i / NU) * F[720]; + while (F[j + 1] < want) j++; + us.push(-PI / 2 + ((j + (want - F[j]) / (F[j + 1] - F[j] || 1)) / 720) * TAU); + } + const objs = [[], []]; + for (let fam = 0; fam < 2; fam++) + for (let i = 0; i < (fam ? NV : NU); i++) { + const T = torusCircle(fam, i, r, d, us); + // A horn torus has a parallel of radius 0 where the hole closes, a + // point: there is no circle to draw. + if (abs(T.rad) < 1e-3) continue; + objs[0].push({ fam, i, C: T }); + // The image of a circle is a circle, so three image points fix it. + const P = [0, 1, 2].map((j) => invert(onCircle(T, (j * TAU) / 3 + 0.3), k2)); + objs[1].push({ fam, i, C: circleThrough(P[0], P[1], P[2]) }); + } + // Sample every circle and fit each surface to its box. The turn is about + // the vertical axis, which both surfaces are symmetric about, so the + // widest the drawing gets is twice the largest distance from that axis. + S.objs = objs.map((list, o) => { + let ylo = 1e9, yhi = -1e9, rad = 0; + const pts = list.map(({ C }) => { + const a = new Float32Array(K * 3); + for (let j = 0; j < K; j++) { + const p = onCircle(C, (j / K) * TAU); + a.set(p, j * 3); + ylo = min(ylo, p[1]); + yhi = max(yhi, p[1]); + rad = max(rad, hypot(p[0], p[2])); + } + return a; + }); + const box = o ? BOT : TOP; + const s = min(BW / (2 * rad), (box[1] - box[0]) / (yhi - ylo)); + return { list, pts, s, rad, ym: (ylo + yhi) / 2, cy: (box[0] + box[1]) / 2 }; + }); +} + +export function setup(params) { + params.s = { phi: 0.5, band: 0, key: "", R: new Float32Array(W * H), G: new Float32Array(W * H), B: new Float32Array(W * H) }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + const speed = cl(k[1], 0.2, 3); + S.phi += dt * speed * 0.28; + S.band += dt * speed * 1.1; + // Shape: tube over centre radius 0.5 (ring) to 1 (horn) to 1.6 (spindle). + const sh = cl(k[2], 0, 2); + S.r = sh <= 1 ? 0.5 + 0.5 * sh : 1 + 0.6 * (sh - 1); + // Distance: the gap between the torus and the centre, in torus sizes. + S.del = 1.2 * exp(3.2 * cl(k[3], 0, 1)); + const key = S.r.toFixed(4) + "," + S.del.toFixed(4); + if (key !== S.key) { + S.key = key; + build(S); + } +} + +// An antialiased line one LED wide, kept as the brightest colour per LED. +function line(S, x0, y0, x1, y1, r, g, b) { + const dx = x1 - x0, dy = y1 - y0, L = dx * dx + dy * dy; + const xa = max(0, floor(min(x0, x1) - 1)), xb = min(W - 1, floor(max(x0, x1) + 1)); + const ya = max(0, floor(min(y0, y1) - 1)), yb = min(H - 1, floor(max(y0, y1) + 1)); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const px = x - x0, py = y - y0; + const t = L ? cl((px * dx + py * dy) / L, 0, 1) : 0; + const ex = px - t * dx, ey = py - t * dy; + const a = cl(1.05 - sqrt(ex * ex + ey * ey), 0, 1); + if (a <= 0) continue; + const p = y * W + x; + if (r * a > S.R[p]) S.R[p] = r * a; + if (g * a > S.G[p]) S.G[p] = g * a; + if (b * a > S.B[p]) S.B[p] = b * a; + } +} + +export function draw(display, params) { + if (!params.s || !params.s.objs) return; + const S = params.s; + S.R.fill(0); + S.G.fill(0); + S.B.fill(0); + const cphi = cos(S.phi), sphi = sin(S.phi); + const cols = [hsv(S.hue + 0.5, 0.8, 1), hsv(S.hue + 0.88, 0.62, 1)]; + const sx = [], sy = [], sz = []; + S.objs.forEach((O, o) => { + const dim = o ? 1 : 0.8; + O.list.forEach(({ fam, i }, n) => { + const a = O.pts[n]; + // The band of light: a soft bump running round each family's index, + // the same circles lit on the torus and on its image. + const N = fam ? NV : NU; + let g = (i - S.band * (fam ? 0.5 : 1)) % N; + if (g < 0) g += N; + g = min(g, N - g); + const hl = exp(-g * g * 0.8); + const col = cols[fam]; + for (let j = 0; j < K; j++) { + const x = a[j * 3], y = a[j * 3 + 1], z = a[j * 3 + 2]; + const xr = x * cphi + z * sphi, zr = -x * sphi + z * cphi; + sx[j] = W / 2 + xr * O.s; + sy[j] = O.cy - (y - O.ym) * O.s; + sz[j] = zr / O.rad; + } + for (let j = 0; j < K; j++) { + const q = (j + 1) % K; + // Depth: the near side bright, the far side down to a fifth. + const depth = cl(0.5 + 0.25 * (sz[j] + sz[q]), 0, 1); + const v = dim * (0.1 + 0.9 * depth * depth) * (0.75 + 0.25 * hl); + const wv = 0.55 * hl * depth; + line(S, sx[j], sy[j], sx[q], sy[q], 255 * v * (col[0] * (1 - wv) + wv), 255 * v * (col[1] * (1 - wv) + wv), 255 * v * (col[2] * (1 - wv) + wv)); + } + }); + }); + for (let p = 0; p < W * H; p++) { + if (S.R[p] + S.G[p] + S.B[p] < 1) continue; + display.setPixel(p % W, floor(p / W), min(255, S.R[p]) | 0, min(255, S.G[p]) | 0, min(255, S.B[p]) | 0); + } +} From 74783df52dbaf9bdadeb5c4a5502078715fdbf5d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:05:48 +0000 Subject: [PATCH 05/19] incoming: Shannon's Approximations and Radon Transform, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/radon-transform.js | 302 ++++++++++++++++++ .../patterns/shannon-approximations.js | 2 +- 2 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 incoming/pattern-study/patterns/radon-transform.js diff --git a/incoming/pattern-study/patterns/radon-transform.js b/incoming/pattern-study/patterns/radon-transform.js new file mode 100644 index 000000000..8c802196d --- /dev/null +++ b/incoming/pattern-study/patterns/radon-transform.js @@ -0,0 +1,302 @@ +// ===== Patternflow pattern ===== +// Title: Radon Transform +// Family: algo +// Idea: A beam sweeps half a turn round a few ellipses of different densities, the bottom square fills row by row with their sinogram, and each projection is filtered and smeared back across the top square along its own angle, so the ellipses build up out of crossing streaks, hold, and give way to the next, slightly moved phantom. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the Radon transform and its inversion (Johann Radon, 1917), filtered back-projection with the ramp filter (G. N. Ramachandran and A. V. Lakshminarayanan, 1971), pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Angles=0..64, Filter=0..1 +// +// Knob 1: Hue. Turns both squares round the wheel: at 0 the reconstruction is ice blue running to white and the sinogram violet. +// Knob 2: Speed. How fast the beam turns: half a turn in 6 s at 2, then the picture holds 1.5 s; at 0.2 a half turn takes 60 s, at 3 it takes 4 s. +// Knob 3: Angles. How many projections a half turn takes. 0 takes turns, doubling each cycle from 16: 16, 32, 64, then 4, 8 and round again. 1 to 64 hold that count; with few the shapes are crossed by the star of streaks, one per angle. +// Knob 4: Filter. The filter each projection passes before it is smeared back: (1 - f) c + f |w|, with c the ramp's height at 1/400 of a cycle a sample. At 0 it is plain back-projection, every shape in a blurred 1/r halo; at 0.06 the edges already come through; at 1 it is the ramp alone, sharp, with dark lobes beside every streak. + +const W = 64, Q = 64, H = 128; +const { floor, min, max, sqrt, sin, cos, exp, round, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); + +function hsv(h, s, v, o) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + o[0] = i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p; + o[1] = i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p; + o[2] = i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p; + return o; +} + +// The detector: NB bins one LED wide, wide enough to cover the square's +// diagonal, so the corners get their share of every projection. The +// sinogram shows the middle 64. +const NB = 92, MID = (NB - 1) / 2, OFF = (NB - Q) / 2, C = (Q - 1) / 2; +// Ramachandran and Lakshminarayanan's ramp filter in space, one bin apart: +// 1/4 at 0, -1/(pi n)^2 at odd n, 0 at even n. Its response rises as |w| +// from 0 to 1/2 at the Nyquist frequency. +const KER = new Float32Array(2 * NB - 1); +for (let n = -(NB - 1); n < NB; n++) KER[n + NB - 1] = n === 0 ? 0.25 : n & 1 ? -1 / (PI * PI * n * n) : 0; +// The flat part of the Filter knob's blend, in the ramp's units. +const FLAT = 1 / 400; + +// A phantom is a list of ellipses [x0, y0, a, b, phi, density]. The line +// integral of one along s at angle th is closed form: with +// r2 = a^2 cos^2(th - phi) + b^2 sin^2(th - phi) and t = s - (x0 cos th + +// y0 sin th) the line's distance from the centre, it is +// 2 rho a b sqrt(r2 - t^2) / r2 where t^2 < r2, and 0 elsewhere. +// One projection, each bin the mean of the lines a quarter bin either side +// of its middle. +function project(E, th, out) { + out.fill(0); + const c = cos(th), sn = sin(th); + for (const [x0, y0, a, b, phi, rho] of E) { + const ca = cos(th - phi), sa = sin(th - phi); + const r2 = a * a * ca * ca + b * b * sa * sa, r = sqrt(r2), k = (rho * a * b) / r2, s0 = x0 * c + y0 * sn + MID; + for (let i = max(0, floor(s0 - r - 1)), e = min(NB - 1, floor(s0 + r + 1)); i <= e; i++) + for (const d of [-0.25, 0.25]) { + const t = i + d - s0; + if (t * t < r2) out[i] += k * sqrt(r2 - t * t); + } + } + return out; +} + +function ramp(p, q) { + for (let i = 0; i < NB; i++) { + let s = 0; + for (let j = 0; j < NB; j++) s += p[j] * KER[i - j + NB - 1]; + q[i] = s; + } + return q; +} + +// Every LED of the top square, (x, y) with y up, takes the value of the +// projection at s = x cos th + y sin th, read between bins. +function smear(acc, p, th) { + const c = cos(th), sn = sin(th); + for (let y = 0; y < Q; y++) { + const u0 = -C * c + (C - y) * sn + MID; + for (let x = 0, k = y * Q; x < Q; x++, k++) { + const u = u0 + x * c, i = floor(u); + if (i < 0 || i >= NB - 1) continue; + const f = u - i; + acc[k] += p[i] + (p[i + 1] - p[i]) * f; + } + } +} + +// The first phantom: a body and five inner shapes, one of them a hole. +function firstPhantom() { + return [ + [0, 0, 26, 19, 0.3, 0.5], + [-9, 6, 9, 5, 0.9, 0.35], + [8, 7, 7, 7, 0, -0.3], + [5, -9, 10, 4, -0.4, 0.3], + [-8, -8, 3, 3, 0, 0.7], + [12, -4, 2, 3.5, 0.5, 0.8], + ]; +} + +// The next phantom: the whole turns 26 to 43 degrees, the inner shapes +// drift, and one of them is drawn anew. Everything stays inside a circle of +// 28 LEDs, so every shadow falls inside the 64 bins the sinogram shows. +function nextPhantom(E) { + const turn = 0.45 + Math.random() * 0.3, ct = cos(turn), st = sin(turn); + const out = E.map(([x, y, a, b, phi, rho], i) => { + let nx = x * ct - y * st, ny = x * st + y * ct; + if (i > 0) { + nx += (Math.random() - 0.5) * 6, ny += (Math.random() - 0.5) * 6; + const r = sqrt(nx * nx + ny * ny), lim = 15 - max(a, b) * 0.4; + if (r > lim) (nx *= lim / r), (ny *= lim / r); + } + return [nx, ny, a, b, phi + turn, rho]; + }); + const k = 1 + floor(Math.random() * (out.length - 1)), small = Math.random() < 0.4; + const a = small ? 2 + Math.random() * 2 : 4 + Math.random() * 6, b = small ? 2 + Math.random() * 2 : 3 + Math.random() * 4; + const ang = Math.random() * 2 * PI, r = Math.random() * 10; + const rho = small ? 0.6 + Math.random() * 0.3 : Math.random() < 0.4 ? -0.3 : 0.25 + Math.random() * 0.2; + out[k] = [r * cos(ang), r * sin(ang), a, b, Math.random() * PI, rho]; + // The body breathes between rounder and longer. + out[0][2] = 24 + Math.random() * 4, out[0][3] = 16 + Math.random() * 5; + return out; +} + +// The continuous sinogram of a phantom, 64 angles down and the middle 64 +// bins across, and its peak. +function sinogram(E, out) { + const p = new Float32Array(NB); + let peak = 1e-6; + for (let r = 0; r < Q; r++) { + project(E, (r * PI) / Q, p); + for (let x = 0; x < Q; x++) (out[r * Q + x] = p[x + OFF]), (peak = max(peak, p[x + OFF])); + } + return peak; +} + +const TURNS = [4, 8, 16, 32, 64]; +// At Speed 2 a half turn takes SWEEP seconds and the result holds HOLD. +const SWEEP = 6, HOLD = 1.5; + +function startCycle(S, E) { + S.prevSino.set(S.sino); + S.prevPeak = S.peak; + S.E = E; + S.peak = sinogram(E, S.sino); + S.cycle++; + S.prog = 0, S.hold = 0; + S.fadeFrom.set(S.shown), S.fade = S.first ? 1 : 0; + rebuild(S); +} + +function anglesFor(S) { + return S.knobN > 0 ? S.knobN : TURNS[(S.cycle + 2) % TURNS.length]; +} + +// Start the reconstruction over for the current angle count and take every +// projection the beam has already passed. +function rebuild(S) { + S.N = anglesFor(S); + S.P.fill(0), S.R.fill(0); + S.done = 0, S.taken.fill(-1); + take(S); +} + +function take(S) { + while (S.done < S.N && (S.done * PI) / S.N <= S.prog * PI + 1e-9) { + const th = (S.done * PI) / S.N; + project(S.E, th, S.p); + ramp(S.p, S.q); + smear(S.P, S.p, th); + smear(S.R, S.q, th); + S.lastP.set(S.p), S.lastQ.set(S.q), S.lastTh = th, S.lastT = S.t; + // Its row of the sinogram, drawn up to 3 rows deep when the rows + // between projections leave room. + const r0 = round((S.done / S.N) * Q), deep = cl(floor(Q / S.N / 3), 1, 3); + for (let j = 0; j < deep; j++) { + const r = (r0 + j) % Q; + S.taken[r] = S.done, S.depth[r] = j; + S.rows[r].set(S.p.subarray(OFF, OFF + Q)); + } + S.done++; + } +} + +export function setup(params) { + const S = (params.s = { + t: 0, hue: 0, speed: 2, filt: 0.06, knobN: -1, N: 4, cycle: -1, prog: 0, hold: 0, done: 0, first: true, + E: null, P: new Float32Array(Q * Q), R: new Float32Array(Q * Q), shown: new Float32Array(Q * Q), fadeFrom: new Float32Array(Q * Q), fade: 1, + sino: new Float32Array(Q * Q), prevSino: new Float32Array(Q * Q), peak: 1, prevPeak: 1, + p: new Float32Array(NB), q: new Float32Array(NB), lastP: new Float32Array(NB), lastQ: new Float32Array(NB), lastTh: 0, lastT: -9, + taken: new Int16Array(Q).fill(-1), depth: new Uint8Array(Q), rows: Array.from({ length: Q }, () => new Float32Array(Q)), gain: 0, c: [0, 0, 0], + }); + // The panel opens on a sinogram already under way: a phantom before the + // first stands in the rows the beam has not reached. + const E = firstPhantom(); + S.peak = sinogram(nextPhantom(E), S.sino); + S.E = E; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.t += dt; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.filt = cl(k[3], 0, 1); + const kn = cl(round(k[2]), 0, 64); + if (S.first) { + // The first cycle starts 60% of the way round, so the picture is mostly + // built when the panel opens. + S.knobN = kn; + startCycle(S, S.E); + S.prog = 0.6; + take(S); + S.first = false; + } else if (kn !== S.knobN) { + S.knobN = kn; + if (anglesFor(S) !== S.N) rebuild(S); + } + const rate = S.speed / 2; + S.fade = min(1, S.fade + dt / 0.6); + if (S.prog < 1) { + S.prog = min(1, S.prog + (dt * rate) / SWEEP); + take(S); + } else { + S.hold += dt * rate; + if (S.hold >= HOLD) startCycle(S, nextPhantom(S.E)); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S || S.first) return; + const c = S.c, f = S.filt, n = max(1, S.done), A = (1 - f) * FLAT, B = f; + const shown = S.shown; + // The reconstruction, the mean of the smeared projections so far, and + // its gain eased toward its peak so a new streak does not flash the scale. + let peak = 1e-6; + for (let i = 0; i < Q * Q; i++) { + const v = (A * S.P[i] + B * S.R[i]) / n; + shown[i] = v; + if (v > peak) peak = v; + } + S.gain = S.gain > 0 ? S.gain + (1 / peak - S.gain) * 0.25 : 1 / peak; + const g = S.gain; + // The newest projection flashes along its rays for a moment. + const fl = exp(-(S.t - S.lastT) / 0.25) * 0.5, cth = cos(S.lastTh), sth = sin(S.lastTh); + const hr = 0.55 + S.hue, hn = 0.78 + S.hue; + for (let y = 0; y < Q; y++) { + const u0 = -C * cth + (C - y) * sth + MID; + for (let x = 0; x < Q; x++) { + const i = y * Q + x; + let u = shown[i] * g; + if (fl > 0.01) { + const s = u0 + x * cth, j = floor(s); + if (j >= 0 && j < NB - 1) { + const w = s - j, pv = S.lastP[j] + (S.lastP[j + 1] - S.lastP[j]) * w, qv = S.lastQ[j] + (S.lastQ[j + 1] - S.lastQ[j]) * w; + u += fl * (A * pv + B * qv) * g; + } + } + if (S.fade < 1) u = S.fadeFrom[i] + (u - S.fadeFrom[i]) * S.fade; + shown[i] = u; + if (u >= 0) { + const v = min(1, u); + hsv(hr, 0.85 - 0.75 * v * v, 0.06 + 0.94 * v, c); + } else hsv(hn, 0.65, min(0.5, -u * 0.9), c); + display.setPixel(x, y, 255 * c[0], 255 * c[1], 255 * c[2]); + } + } + // The sinogram, angle 0 at the top to 180 degrees at the bottom. Rows the + // beam has passed show the full sinogram dimly and the rows of the + // projections taken brightly; rows ahead still hold the last phantom's. + const swept = S.prog >= 1 ? Q : floor(S.prog * Q + 1e-6) + 1, hs = 0.72 + S.hue; + for (let r = 0; r < Q; r++) { + const tk = S.taken[r] >= 0 && r < swept; + const lv = tk ? (S.depth[r] ? 0.75 : 1) : r < swept ? 0.5 : 0.3; + const src = tk ? S.rows[r] : r < swept ? S.sino.subarray(r * Q, r * Q + Q) : S.prevSino.subarray(r * Q, r * Q + Q); + const pk = r < swept ? S.peak : S.prevPeak; + const hot = tk && S.taken[r] === S.done - 1 ? exp(-(S.t - S.lastT) / 0.3) : 0; + for (let x = 0; x < Q; x++) { + const v = cl(src[x] / pk, 0, 1); + hsv(hs, max(0, 0.8 - 0.55 * v * v - 0.6 * hot), lv * (0.05 + 0.95 * v) * (1 - hot) + hot * (0.3 + 0.7 * v), c); + display.setPixel(x, Q + r, 255 * c[0], 255 * c[1], 255 * c[2]); + } + } + // The beam's place in the sweep, a tick at each side of the sinogram, and + // its direction across the reconstruction as a faint line. + if (S.prog < 1) { + const r = min(Q - 1, floor(S.prog * Q)); + for (const x of [0, 1, Q - 2, Q - 1]) display.setPixel(x, Q + r, 255, 255, 255); + const th = S.prog * PI, dx = -sin(th), dy = -cos(th); + for (let s = -44; s <= 44; s += 0.5) { + const x = round(C + s * dx), y = round(C + s * dy); + if (x < 0 || x >= Q || y < 0 || y >= Q) continue; + const u = shown[y * Q + x]; + hsv(hr, 0.3, min(1, 0.35 + max(0, u)), c); + display.setPixel(x, y, 255 * c[0], 255 * c[1], 255 * c[2]); + } + } +} diff --git a/incoming/pattern-study/patterns/shannon-approximations.js b/incoming/pattern-study/patterns/shannon-approximations.js index 7026994cc..0d76f1b49 100644 --- a/incoming/pattern-study/patterns/shannon-approximations.js +++ b/incoming/pattern-study/patterns/shannon-approximations.js @@ -13,7 +13,7 @@ // // Knob 1: Hue. Turns the page round the wheel: at 0 the words are sea green, word pairs from the book warm gold and the noise between them slate. // Knob 2: Speed. Letters a second, eight times the knob: 16 at 2, so the cursor goes round the 336 cells of the page in 21 s; 1.6 at 0.2, 24 at 3. Fresh letters burn white and cool over a second. -// Knob 3: Order. Shannon's six steps, in whole steps: 0 the 27 symbols equiprobable, 1 letter frequencies, 2 digrams, 3 trigrams, 4 words by their frequency, 5 each word drawn after the word before it. A change rewrites the whole page from the top in 0.6 s. +// Knob 3: Order. Shannon's six steps, in whole steps: 0 the 27 symbols equiprobable, 1 letter frequencies, 2 digrams, 3 trigrams, 4 words by their frequency, 5 each word drawn after the word before it. A change rewrites the whole page from the top in 0.6 s. The bottom row is a gauge of the share of the page's letters that stand in real words. // Knob 4: Blackout. How the runs that are not words show: dim letters at 0, and turned up they sink under dark ink bars until at 1 only the real words stand on a blacked-out page. const W = 64, H = 128; From 7c7db864d66be3d62b010f1973f536187a399ee3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:06:51 +0000 Subject: [PATCH 06/19] incoming: Distance Field and Walsh Functions, pattern round 67 (draft) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/distance-field.js | 280 ++++++++++++++++++ .../pattern-study/patterns/walsh-functions.js | 240 +++++++++++++++ 2 files changed, 520 insertions(+) create mode 100644 incoming/pattern-study/patterns/distance-field.js create mode 100644 incoming/pattern-study/patterns/walsh-functions.js diff --git a/incoming/pattern-study/patterns/distance-field.js b/incoming/pattern-study/patterns/distance-field.js new file mode 100644 index 000000000..6759cb24b --- /dev/null +++ b/incoming/pattern-study/patterns/distance-field.js @@ -0,0 +1,280 @@ +// ===== Patternflow pattern ===== +// Title: Distance Field +// Family: type +// Idea: One glyph fills the panel, kept only as a coarse grid of signed distances whose texel centres show as dim dots, and every LED is drawn by magnifying that grid bilinearly and cutting it at a threshold, so W, S, K, 3, 빛 and ? keep crisp edges at any grid size while their corners round off and thin strokes vanish as the grid coarsens; an outline band and an outer glow come from the same field, and each glyph melts into the next by blending the two fields. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: signed distance fields for alpha-tested magnification (Chris Green, Valve, "Improved Alpha-Tested Magnification for Vector Textures and Special Effects", SIGGRAPH 2007), glyphs drawn for this pattern, pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Magnify=-1.5..1.8, Weight=-4..5 +// +// Knob 1: Hue. Turns all three colours round the wheel together: at 0 a cyan glyph with a pale rim and a magenta glow. +// Knob 2: Speed. How fast the glyphs come: at 2 each holds 2 seconds and melts into the next over 0.5, at 0.2 ten times slower; the glow's ripples move with it. +// Knob 3: Magnify. How far the stored field is blown up, in octaves around 6 LEDs a texel where it opens (12 by 23 texels): at -1.5 a texel is 2 LEDs and the glyph is sharp, at 1 it is 12 LEDs and the corners are round, at 1.8 it is 21 LEDs, 4 by 8 texels, and the thin parts are gone. +// Knob 4: Weight. Where the field is cut, in LEDs off the true outline: -4 thins every stroke by 8 LEDs, 0 is the drawn weight, 5 makes it 10 LEDs bolder and closes the small counters. + +const W = 64, H = 128; +const { floor, ceil, round, min, abs, sqrt, exp, sin, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- Glyphs, as exact geometry in panel LEDs (x 0..64, y 0..128 down). +// Each glyph is a union of simple polygons that may overlap: rectangles, +// slanted bars cut level at both ends, and bands along an ellipse whose +// arcs are flattened into chords of at most 5 degrees. The box is about +// 56 by 116, x 4..60 and y 6..122. Strokes are 11 to 13 LEDs wide, the +// Hangul ones 9 to 10, so at 12 LEDs a texel the thin ones go first. +const rect = (x0, y0, x1, y1) => [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]; +// A bar from a top edge centred on xt, wt wide, to a bottom edge on xb. +const bar = (xt, yt, wt, xb, yb, wb) => [[xt - wt / 2, yt], [xt + wt / 2, yt], [xb + wb / 2, yb], [xb - wb / 2, yb]]; +// A band hw either side of an ellipse from angle a0 to a1 in degrees, +// clockwise on the panel (0 is right, 90 down), cut square to the curve. +function arc(cx, cy, rx, ry, hw, a0, a1) { + const n = ceil(abs(a1 - a0) / 5), out = [], inn = []; + for (let i = 0; i <= n; i++) { + const a = ((a0 + ((a1 - a0) * i) / n) * PI) / 180, c = cos(a), s = sin(a); + out.push([cx + (rx + hw) * c, cy + (ry + hw) * s]); + inn.push([cx + (rx - hw) * c, cy + (ry - hw) * s]); + } + return out.concat(inn.reverse()); +} +function disc(cx, cy, r) { + const p = []; + for (let i = 0; i < 48; i++) p.push([cx + r * cos((i * PI) / 24), cy + r * sin((i * PI) / 24)]); + return p; +} + +// The order alternates straight and round, so every melt turns a corner +// into a curve or back. +const GLYPHS = [ + // W: four bars, the inner two thinner and sharing the top apex. + [bar(9.5, 6, 11, 20, 122, 11), bar(31, 6, 8, 20, 122, 9), bar(33, 6, 8, 44, 122, 9), bar(54.5, 6, 11, 44, 122, 11)], + // S: two elliptic bowls meeting in the middle stroke at y 64. + [arc(32, 38, 22, 26, 6, -25, -280), arc(32, 90, 22, 26, 6, -100, 155)], + // K: the arm ends inside the stem, the leg starts inside the arm. + [rect(5, 6, 18, 122), bar(51, 6, 14, 12, 84, 12), bar(26, 56, 10, 51, 122, 15)], + // 3: a small bowl over a big one, a short tongue to the left where they + // meet, and a notch on the right between them. + [arc(30, 37, 22, 25, 6, -155, 100), arc(30, 89, 24, 27, 6, -100, 150), rect(17, 56, 32, 68)], + // 빛: ㅂ and ㅣ over the final ㅊ, whose top tick is the smallest stroke. + [ + rect(5, 6, 15, 62), rect(28, 6, 38, 62), rect(5, 29, 38, 38), rect(5, 53, 38, 62), rect(47, 6, 57, 68), + rect(27, 69, 37, 77), rect(8, 81, 56, 90), bar(31, 88, 11, 10, 122, 12), bar(28, 92, 9, 54, 122, 12), + ], + // ?: a hook that turns into the stem, and a round dot. + [arc(32, 37, 22, 25, 6, -160, 92), rect(26, 58, 38, 92), disc(32, 112, 8.5)], +]; + +// Even-odd test against one simple polygon. +function inPoly(P, x, y) { + let c = false; + for (let i = 0, j = P.length - 1; i < P.length; j = i++) { + const [xi, yi] = P[i], [xj, yj] = P[j]; + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) c = !c; + } + return c; +} + +// The outline of a union. Every polygon edge is split where any other +// polygon's edge crosses it (or runs along it), and a piece is kept when a +// point a hair to one side is in the union and a point to the other side is +// not. Edges buried inside another polygon go, and so do edges where two +// polygons only touch. The distance to the kept pieces is then the exact +// distance to the glyph's outline, inside as well as outside: the plain +// minimum of each polygon's own distance would be wrong inside, wherever +// two strokes join. +function outline(polys) { + const inside = (x, y) => polys.some((P) => inPoly(P, x, y)); + const segs = []; + polys.forEach((P, i) => { + for (let k = 0; k < P.length; k++) { + const [ax, ay] = P[k], [bx, by] = P[(k + 1) % P.length]; + const dx = bx - ax, dy = by - ay, L2 = dx * dx + dy * dy; + if (L2 < 1e-12) continue; + const ts = [0, 1]; + polys.forEach((Q, j) => { + if (j === i) return; + for (let m = 0; m < Q.length; m++) { + const [cx, cy] = Q[m], [ex, ey] = Q[(m + 1) % Q.length]; + const fx = ex - cx, fy = ey - cy, den = dx * fy - dy * fx; + if (abs(den) < 1e-12) { + if (abs((cx - ax) * dy - (cy - ay) * dx) < 1e-9) ts.push(((cx - ax) * dx + (cy - ay) * dy) / L2, ((ex - ax) * dx + (ey - ay) * dy) / L2); + continue; + } + const u = ((cx - ax) * dy - (cy - ay) * dx) / den; + if (u >= 0 && u <= 1) ts.push(((cx - ax) * fy - (cy - ay) * fx) / den); + } + }); + const tt = ts.filter((t) => t >= 0 && t <= 1).sort((a, b) => a - b); + const L = sqrt(L2), nx = (-dy / L) * 1e-4, ny = (dx / L) * 1e-4; + for (let q = 1; q < tt.length; q++) { + if (tt[q] - tt[q - 1] < 1e-9) continue; + const tm = (tt[q] + tt[q - 1]) / 2, mx = ax + dx * tm, my = ay + dy * tm; + if (inside(mx + nx, my + ny) !== inside(mx - nx, my - ny)) + segs.push(ax + dx * tt[q - 1], ay + dy * tt[q - 1], ax + dx * tt[q], ay + dy * tt[q]); + } + } + }); + return { segs: Float64Array.from(segs), inside }; +} + +// Signed distance in LEDs, negative inside. +function sdf(G, x, y) { + const s = G.segs; + let d2 = Infinity; + for (let i = 0; i < s.length; i += 4) { + const ax = s[i], ay = s[i + 1], dx = s[i + 2] - ax, dy = s[i + 3] - ay; + const t = cl(((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy), 0, 1); + const ex = x - ax - t * dx, ey = y - ay - t * dy, e = ex * ex + ey * ey; + if (e < d2) d2 = e; + } + return G.inside(x, y) ? -sqrt(d2) : sqrt(d2); +} + +// The texel grid for a texel size of s LEDs: centres on LED centres, s +// apart, the first at or left of the panel's first LED and the last at or +// right of its last, so the reconstruction never runs off the field. +function grid(s) { + const nx = ceil((W - 1) / s) + 1, ny = ceil((H - 1) / s) + 1; + const ox = -floor(((nx - 1) * s - (W - 1)) / 2), oy = -floor(((ny - 1) * s - (H - 1)) / 2); + // For each LED column and row: the texel to its left (above) and how far + // along to the next one, for the bilinear weights. + const xi = new Int16Array(W), xf = new Float32Array(W), yi = new Int16Array(H), yf = new Float32Array(H); + for (let x = 0; x < W; x++) { + const g = (x - ox) / s, i = min(nx - 2, floor(g)); + xi[x] = i; + xf[x] = g - i; + } + for (let y = 0; y < H; y++) { + const g = (y - oy) / s, j = min(ny - 2, floor(g)); + yi[y] = j; + yf[y] = g - j; + } + return { s, nx, ny, ox, oy, xi, xf, yi, yf, fields: [] }; +} + +// Sample a glyph's exact distance at every texel centre (LED centres sit at +// x + 0.5). This is the whole texture: nothing else of the glyph survives. +function field(G, glyph) { + if (G.fields[glyph]) return G.fields[glyph]; + const F = new Float32Array(G.nx * G.ny), O = OUTLINES[glyph]; + for (let j = 0; j < G.ny; j++) + for (let i = 0; i < G.nx; i++) F[j * G.nx + i] = sdf(O, G.ox + i * G.s + 0.5, G.oy + j * G.s + 0.5); + return (G.fields[glyph] = F); +} + +const OUTLINES = GLYPHS.map(outline); +const NG = GLYPHS.length; +// Seconds at Speed 2: a glyph holds HOLD, then melts into the next over MELT. +const HOLD = 2, MELT = 0.5, BEAT = HOLD + MELT; +// The rim: a band this many LEDs wide just inside the cut. +const RIM = 1.3; + +export function setup(params) { + params.s = { T: 0, ripple: 0, G: grid(6), F: null, hue: 0, weight: 0 }; +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + const sp = cl(k[1], 0.05, 3) / 2; + S.T += dt * sp; + S.ripple += dt * sp * 0.55; + S.hue = k[0]; + S.weight = cl(k[3], -6, 8); + const s = cl(round(6 * 2 ** cl(k[2], -2, 2)), 2, 21); + if (s !== S.G.s) S.G = grid(s); + // The texture of the moment: one glyph's field, or two blended while it + // melts. A blend of two distance fields is not itself a distance field, + // but its zero set slides from one outline to the other. + const n = floor(S.T / BEAT), u = smooth((S.T - n * BEAT - HOLD) / MELT); + const A = field(S.G, n % NG); + if (u <= 0) S.F = A; + else { + const B = field(S.G, (n + 1) % NG); + if (!S.F || S.F === A || S.F === B || S.F.length !== A.length) S.F = new Float32Array(A.length); + for (let i = 0; i < A.length; i++) S.F[i] = A[i] + (B[i] - A[i]) * u; + } +} + +export function draw(display, params) { + const S = params.s; + if (!S.F) return; + const { G, F, weight } = S; + const { nx, xi, xf, yi, yf } = G; + const buf = S.buf || (S.buf = new Float32Array(W * H * 3)); + const [fr, fg, fb] = hsv(0.5 + S.hue, 0.78, 1); + const [rr, rg, rb] = hsv(0.47 + S.hue, 0.22, 1); + const [gr, gg, gb] = hsv(0.87 + S.hue, 0.72, 1); + const ph = S.ripple; + for (let y = 0, p = 0; y < H; y++) { + const r0 = yi[y] * nx, r1 = r0 + nx, v = yf[y]; + for (let x = 0; x < W; x++, p += 3) { + // Bilinear magnification, what the texture unit does for free. + const i = xi[x], h = xf[x]; + const top = F[r0 + i] + (F[r0 + i + 1] - F[r0 + i]) * h; + const bot = F[r1 + i] + (F[r1 + i + 1] - F[r1 + i]) * h; + const e = top + (bot - top) * v - weight; + // Alpha test, softened over one LED so the cut edge is crisp but not + // stair-stepped. + const a = cl(0.5 - e, 0, 1); + let R = 0, Gc = 0, B = 0; + if (a > 0) { + // Deeper inside is brighter, so the spine of each stroke shows. + const lv = 0.5 + 0.5 * smooth(-e / 6), rim = cl(e + RIM + 0.5, 0, 1); + R = a * (fr * lv + (rr - fr * lv) * rim); + Gc = a * (fg * lv + (rg - fg * lv) * rim); + B = a * (fb * lv + (rb - fb * lv) * rim); + } + if (a < 1) { + // Outer glow from the same field: falls off with distance, with + // slow rings at fixed distances running outward. + const d = e > 0 ? e : 0; + const g = (1 - a) * 0.8 * exp(-d / 4.2) * (0.6 + 0.4 * cos(2 * PI * (d / 7 - ph))); + R += gr * g; + Gc += gg * g; + B += gb * g; + } + buf[p] = R; + buf[p + 1] = Gc; + buf[p + 2] = B; + } + } + // The texel centres, dim dots: dark holes where they fall in the glyph, + // faint light where they fall outside. Too dense to read below 4 LEDs. + const fade = cl((G.s - 2) / 3, 0, 1); + if (fade > 0) { + const [dr, dg, db] = hsv(0.6 + S.hue, 0.25, 0.28 * fade); + for (let j = 0; j < G.ny; j++) { + const y = G.oy + j * G.s; + if (y < 0 || y >= H) continue; + for (let i = 0; i < nx; i++) { + const x = G.ox + i * G.s; + if (x < 0 || x >= W) continue; + const e = F[j * nx + i] - weight, p = (y * W + x) * 3; + if (e < 0) { + const k = 1 - 0.55 * fade; + buf[p] *= k; + buf[p + 1] *= k; + buf[p + 2] *= k; + } else { + buf[p] += dr; + buf[p + 1] += dg; + buf[p + 2] += db; + } + } + } + } + for (let y = 0, p = 0; y < H; y++) + for (let x = 0; x < W; x++, p += 3) display.setPixel(x, y, min(255, buf[p] * 255), min(255, buf[p + 1] * 255), min(255, buf[p + 2] * 255)); +} diff --git a/incoming/pattern-study/patterns/walsh-functions.js b/incoming/pattern-study/patterns/walsh-functions.js new file mode 100644 index 000000000..9c3a43530 --- /dev/null +++ b/incoming/pattern-study/patterns/walsh-functions.js @@ -0,0 +1,240 @@ +// ===== Patternflow pattern ===== +// Title: Walsh Functions +// Family: wave +// Idea: The top half is the chart of all 64 two-dimensional Walsh functions on 8 by 8 LEDs, laid out in Walsh's sequency order, Paley's dyadic order or Sylvester's natural order, and the bottom half rebuilds a moving abstract picture from its Walsh-Hadamard coefficients, adding them one at a time in zigzag order over the chart, so the picture sharpens out of 8 by 8 blocks while the tile being added lights up above. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: Walsh functions (Joseph L. Walsh, "A closed set of normal orthogonal functions", 1923), Sylvester's Hadamard matrices (James Joseph Sylvester, 1867), Paley's dyadic ordering (Raymond Paley, 1932), pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Order=0..2, Drop=0..63 +// +// Knob 1: Hue. Turns both halves round the wheel: at 0 the chart is a checkerboard of blue and pink tiles and the picture runs from navy through teal to pale cyan. +// Knob 2: Speed. How fast coefficients come: 6 a second at 2, so all 64 are in after 10.5 seconds; they leave three times faster and the count starts again. The picture drifts with it. +// Knob 3: Order. How the 64 functions are laid out, and so the order they are added in: 0 sequency (Walsh, 1923), where the number of sign changes grows to the right and down; 1 dyadic (Paley, 1932); 2 natural, the rows of Sylvester's Hadamard matrix. Turning it slides every tile to its new place and starts the count again. +// Knob 4: Drop. How many coefficients at the end of the zigzag are never added: 0 keeps all 64 and the picture comes back exactly; 48 keeps 16; 63 keeps only the mean of each 8 by 8 block. Dropped tiles go dark in the chart. + +const S8 = 8; +const { floor, round, min, max, abs, sqrt, sin, cos, exp, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// ---- The functions. Sylvester's matrix of order 8: row h, column x is +// +1 or -1 by the parity of the bits h and x share. That is the natural +// order. Paley's dyadic order takes row bitreverse(k), which multiplies +// the Rademacher square waves named by the bits of k; Walsh's sequency +// order takes row bitreverse(gray(k)), and its row k changes sign exactly +// k times. setup() counts the sign changes and throws if any is wrong. +const bits = (v) => { let n = 0; for (; v; v &= v - 1) n++; return n; }; +const HAD = (h, x) => (bits(h & x) & 1 ? -1 : 1); +const rev3 = (k) => ((k & 1) << 2) | (k & 2) | ((k >> 2) & 1); +const gray = (k) => k ^ (k >> 1); +const ORDERS = [ + Array.from({ length: S8 }, (_, k) => rev3(gray(k))), // sequency + Array.from({ length: S8 }, (_, k) => rev3(k)), // dyadic + Array.from({ length: S8 }, (_, k) => k), // natural +]; +const changes = (h) => { let n = 0; for (let x = 1; x < S8; x++) if (HAD(h, x) !== HAD(h, x - 1)) n++; return n; }; +// Sequency of each natural row: 0 7 3 4 1 6 2 5. +const SEQ = Array.from({ length: S8 }, (_, h) => changes(h)); + +// JPEG's zigzag over the 8 by 8 chart: [column, row] in the order added. +const ZIG = []; +for (let s = 0; s < 2 * S8 - 1; s++) + for (let i = 0; i <= s; i++) { + const a = s & 1 ? i : s - i, b = s - a; + if (a < S8 && b < S8) ZIG.push([b, a]); + } + +// In-place fast Walsh-Hadamard transform of an 8 by 8 block, rows then +// columns, natural order, whole numbers only: three rounds of sums and +// differences each way. Applied twice it gives the block back times 64. +function fwht(B) { + for (let r = 0; r < 64; r += 8) + for (let len = 1; len < 8; len <<= 1) + for (let i = 0; i < 8; i += len << 1) + for (let j = i; j < i + len; j++) { + const a = B[r + j], b = B[r + j + len]; + B[r + j] = a + b; + B[r + j + len] = a - b; + } + for (let c = 0; c < 8; c++) + for (let len = 1; len < 8; len <<= 1) + for (let i = 0; i < 8; i += len << 1) + for (let j = i; j < i + len; j++) { + const a = B[c + j * 8], b = B[c + (j + len) * 8]; + B[c + j * 8] = a + b; + B[c + (j + len) * 8] = a - b; + } +} + +export function setup(params) { + // The check the knob names promise: sequency rows change sign 0..7 + // times in order; a dyadic row's count is the Gray decoding of its index; + // the rows are orthogonal. + ORDERS[0].forEach((h, k) => { if (SEQ[h] !== k) throw new Error("sequency order is wrong at " + k); }); + ORDERS[1].forEach((h, k) => { let g = k; for (let m = k >> 1; m; m >>= 1) g ^= m; if (SEQ[h] !== g) throw new Error("dyadic order is wrong at " + k); }); + for (let a = 0; a < S8; a++) + for (let b = 0; b < S8; b++) { + let dot = 0; + for (let x = 0; x < S8; x++) dot += HAD(a, x) * HAD(b, x); + if (dot !== (a === b ? S8 : 0)) throw new Error("rows " + a + " and " + b + " are not orthogonal"); + } + params.s = { + T: 1, dir: 1, wait: 0, clock: 0, order: 0, knob: -1, slide: 1, + from: null, img: new Int32Array(4096), rec: new Float64Array(4096), add: new Float64Array(4096), bump: 0, + blk: new Int32Array(64), R: new Float64Array(64), keepW: new Float64Array(64), hue: 0, keep: 64, + }; +} + +// Where each natural pair (hu, hv) sits on the chart in an order: +// [column, row, zigzag index]. +function places(o) { + const inv = new Int8Array(S8); + ORDERS[o].forEach((h, k) => (inv[h] = k)); + const at = new Int8Array(S8 * S8); + ZIG.forEach(([c, r], i) => (at[r * S8 + c] = i)); + const P = []; + for (let hv = 0; hv < S8; hv++) + for (let hu = 0; hu < S8; hu++) { + const c = inv[hu], r = inv[hv]; + P.push([c, r, at[r * S8 + c]]); + } + return P; +} +const PLACES = [0, 1, 2].map(places); + +// The picture, whole numbers 0..255 on 64 by 64: a turning square and a +// disc drifting past each other over a slowly turning ramp, brighter where +// they overlap. +function picture(S) { + const t = S.clock, img = S.img; + const sx = 30 + 10 * cos(t * 0.23), sy = 29 + 9 * sin(t * 0.31), a = t * 0.2, ca = cos(a), sa = sin(a); + const dx = 33 + 14 * cos(t * 0.37 + 2), dy = 35 + 13 * sin(t * 0.29 + 1); + const th = t * 0.13, ux = cos(th), uy = sin(th); + for (let y = 0; y < 64; y++) + for (let x = 0; x < 64; x++) { + const px = x + 0.5, py = y + 0.5; + const lx = (px - sx) * ca + (py - sy) * sa, ly = (py - sy) * ca - (px - sx) * sa; + const sq = cl(0.5 - max(abs(lx), abs(ly)) + 12.5, 0, 1); + const dc = cl(11.5 - sqrt((px - dx) ** 2 + (py - dy) ** 2), 0, 1); + const v = 0.1 + 0.14 * (((px - 32) * ux + (py - 32) * uy) / 45) + 0.5 * sq + 0.4 * dc; + img[y * 64 + x] = round(cl(v, 0, 1) * 255); + } +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 0, 0.06]; + dt = cl(dt || 0, 0, 0.1); + const sp = cl(k[1], 0.05, 3); + S.hue = k[0]; + S.keep = 64 - cl(round(k[3]), 0, 63); + const o = cl(round(k[2]), 0, 2); + if (S.knob < 0) (S.knob = S.order = o), (S.from = PLACES[o]); + else if (o !== S.order) { + // Slide from wherever the tiles are now, and count again from one. + S.from = current(S); + S.order = o; + S.slide = 0; + S.T = 1; + S.dir = 1; + S.wait = 0.4; + } + S.slide = min(1, S.slide + dt / 0.9); + S.clock += dt * sp * 0.5; + // The count: up at 3 x Speed a second to keep, a pause, down three times + // faster, a pause, again. + const rate = 3 * sp; + if (S.wait > 0) S.wait -= dt; + else if (S.dir > 0) { + S.T = min(S.keep, S.T + dt * rate); + if (S.T >= S.keep) (S.dir = -1), (S.wait = 1.4); + } else { + S.T = max(1, S.T - dt * rate * 3); + if (S.T <= 1) (S.dir = 1), (S.wait = 0.6); + } + if (S.T > S.keep) S.T = S.keep; + + // Transform every 8 by 8 block of the picture, then rebuild it from the + // first floor(T) coefficients in zigzag order and a share of the next. + picture(S); + const { img, rec, add, blk, keepW, R } = S; + const n = floor(S.T), part = S.T - n, P = PLACES[S.order]; + let fN = -1; + P.forEach(([, , i], f) => { + keepW[f] = i < n ? 1 : i === n && n < S.keep ? part : 0; + if (i === n && n < S.keep) fN = f; + }); + S.bump = fN < 0 ? 0 : sin(PI * part); + for (let by = 0; by < 8; by++) + for (let bx = 0; bx < 8; bx++) { + const o0 = by * 512 + bx * 8; + for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) blk[y * 8 + x] = img[o0 + y * 64 + x]; + fwht(blk); + // What the coefficient now fading in adds to this block: its own + // basis image, as large as the coefficient. + const c = fN < 0 ? 0 : blk[fN] / 64; + for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) add[o0 + y * 64 + x] = c * HAD(fN & 7, x) * HAD(fN >> 3, y); + // Inverse: the same transform on the kept coefficients, over 64. Kept + // whole, they stay whole numbers; only the one fading in is a share. + for (let f = 0; f < 64; f++) R[f] = blk[f] * keepW[f]; + fwht(R); + for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) rec[o0 + y * 64 + x] = R[y * 8 + x] / 64; + } +} + +// Where the tiles are drawn now: on the way from the old layout to the new. +function current(S) { + const to = PLACES[S.order], e = smooth(S.slide); + return to.map(([c, r, i], f) => [S.from[f][0] + (c - S.from[f][0]) * e, S.from[f][1] + (r - S.from[f][1]) * e, i]); +} + +export function draw(display, params) { + const S = params.s; + if (!S.from) return; + const pos = current(S); + const T = S.T, keep = S.keep; + // The chart. Tile f is the natural pair (hu, hv) = (f % 8, f / 8); its + // colour belongs to the function, by its two sequencies, so it travels + // with the tile: a checkerboard of two hues in sequency order. + for (let f = 0; f < 64; f++) { + const hu = f & 7, hv = f >> 3, su = SEQ[hu], sv = SEQ[hv], i = PLACES[S.order][f][2]; + const hueF = 0.59 + S.hue + 0.31 * ((su + sv) & 1) + (0.06 * (su + sv)) / 14; + let L, flash = 0; + if (i >= keep) L = 0.1; + else { + const inn = cl(T - i, 0, 1); + flash = T > i ? exp(-(T - i) * 0.8) : 0; + L = 0.34 + 0.5 * inn; + } + const [pr, pg, pb] = hsv(hueF, 0.75 - 0.6 * flash, min(1, L + 0.5 * flash)); + const [nr, ng, nb] = hsv(hueF, 0.6, 0.04 + 0.06 * L + 0.25 * flash); + const ox = round(pos[f][0] * 8), oy = round(pos[f][1] * 8); + for (let y = 0; y < 8; y++) { + const sy = HAD(hv, y); + for (let x = 0; x < 8; x++) { + const plus = HAD(hu, x) * sy > 0; + display.setPixel(ox + x, oy + y, min(255, (plus ? pr : nr) * 255), min(255, (plus ? pg : ng) * 255), min(255, (plus ? pb : nb) * 255)); + } + } + } + // The rebuilt picture below. + const rec = S.rec; + for (let y = 0; y < 64; y++) + for (let x = 0; x < 64; x++) { + const v = cl(rec[y * 64 + x] / 255, 0, 1); + const [r, g, b] = hsv(0.6 + S.hue - 0.16 * v, 0.85 - 0.6 * v * v, 0.13 + 0.87 * v); + // A white stamp of the term coming in, where it changes the picture. + const w = S.bump * min(1, abs(S.add[y * 64 + x]) / 48) * 0.45; + display.setPixel(x, 64 + y, min(255, (r + w) * 255), min(255, (g + w) * 255), min(255, (b + w) * 255)); + } +} From 22ffd0b3c27ebce96bc8fdacbaf1e33d0462171f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:07:15 +0000 Subject: [PATCH 07/19] incoming: Silencio and Artin Braids, pattern round 67 --- .../pattern-study/patterns/artin-braids.js | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 incoming/pattern-study/patterns/artin-braids.js diff --git a/incoming/pattern-study/patterns/artin-braids.js b/incoming/pattern-study/patterns/artin-braids.js new file mode 100644 index 000000000..9e497c144 --- /dev/null +++ b/incoming/pattern-study/patterns/artin-braids.js @@ -0,0 +1,272 @@ +// ===== Patternflow pattern ===== +// Title: Artin Braids +// Family: topology +// Idea: A strip at the top holds three to seven coloured points seen from above, and generators of the braid group swap neighbours by half turns about their midpoints, while below it their history scrolls down the panel as a braid, each crossing drawn with the strand that passed in front whole and the one behind broken. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: braid groups (Emil Artin, 1925), the braid group as the fundamental group of the configuration space of n points in the plane (Ralph Fox and Lee Neuwirth, 1962), pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Strands=3..7, Word=0..4 +// +// Knob 1: Hue. Turns the strands' colours round the wheel. Strand k sits 0.8 k / (n - 1) of a turn on from the first, so first and last never meet; at 0 the first is azure, and three strands are azure, rose and yellow-green. +// Knob 2: Speed. A generator takes 1.25 seconds at 2, 12.5 at 0.2 and 0.83 at 3. The braid keeps its shape at any speed; only the scrolling changes. +// Knob 3: Strands. 3 to 7 points. Turning it starts the braid again with every point at home. +// Knob 4: Word. Which braid word plays, round and round, starting again with every point home when it is turned: 0 random generators and inverses; 1 the full twist (s1 s2 ... s(n-1))^n, a pure braid that brings every colour home after n(n-1) crossings; 2 the Borromean braid (s1 s2^-1)^3 on three neighbouring strands, the three moving along; 3 the braid relation as a loop, s1 s2 s1 followed by the inverse of s2 s1 s2, six crossings that are the trivial braid; 4 a random positive braid, every crossing the same hand. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, exp, sin, cos, PI } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); + +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// The strip is rows 0 to BAND - 1; below it, one row of the space-time +// picture per DT of braid time, the newest at the top. +const BAND = 30, CY = 14.5, ROWS = H - BAND, KEEP = ROWS + 3; +// One generator takes a unit of braid time: the half turn runs over the +// first 0.9 of it, nearly at an even pace, and the points rest for the rest. +// An even turn makes x a cosine of time, so each crossing is a smooth S. +const TURN = 0.9; +const ease = (p) => 0.7 * cl(p, 0, 1) + 0.3 * smooth(p); +// Seconds a generator takes at Speed 2. +const GEN = 1.25; + +// 3 by 5 digits for the generator's index, and a minus for an inverse. +const DIGITS = ["111101101101111", "010110010010111", "111001111100111", "111001111001111", "101101111001001", "111100111001111", "111100111101111"]; + +// Convention. Seen from above, the strip's lower edge is the side nearer +// the viewer of the braid below, so a point lower in the strip is in front. +// s_i turns the points at places i and i+1 (counted by x) half a turn +// counterclockwise about their midpoint: the left one goes round by the +// lower side, in front, and the right one by the upper side, behind. So in +// s_i the strand moving right crosses over, and in its inverse, turning +// clockwise, under. Places and generators count from 0 in the code and from +// 1 on the panel. + +// The next letter of the word, as [place, +1 or -1]. +function nextLetter(S) { + const n = S.n, c = S.c++; + switch (S.word) { + case 1: // the full twist, (s1 s2 ... s(n-1))^n + return [c % (n - 1), 1]; + case 2: { // the Borromean braid (s1 s2^-1)^3 on places j, j+1, j+2 + const j = floor(c / 6) % (n - 2); + return c % 2 ? [j + 1, -1] : [j, 1]; + } + case 3: { // s1 s2 s1 s2^-1 s1^-1 s2^-1 on places j, j+1, j+2 + const j = floor(c / 6) % (n - 2), k = c % 6; + return [j + [0, 1, 0, 1, 0, 1][k], k < 3 ? 1 : -1]; + } + case 4: // a random positive braid + return [floor(Math.random() * (n - 1)), 1]; + default: { + // Random, but never a letter straight after its own inverse. + let i, s; + do { + i = floor(Math.random() * (n - 1)); + s = Math.random() < 0.5 ? 1 : -1; + } while (S.last && i === S.last[0] && s === -S.last[1]); + return [i, s]; + } + } +} + +function reset(S, n, word) { + S.n = n; + S.word = word; + S.sp = W / n; + S.r = S.sp / 2; + // Rows a generator takes: the crossings keep about the same slope for any + // number of strands, about 1.7 LEDs across a row at the steepest. + S.dt = 1 / cl(round(S.sp * 1.15), 11, 26); + S.perm = Array.from({ length: n }, (_, k) => k); + S.c = 0; + S.last = null; + S.g0 = 0; + S.gen = nextLetter(S); + S.home = 0; + // Samples of every strand's x and depth, KEEP of them in a ring. + S.hx = new Float32Array(KEEP * n); + S.hz = new Float32Array(KEEP * n); + S.k = 0; + S.t = 0; + // Pre-fill: run the braid until the picture reaches the bottom. + while (S.k < KEEP) push(S); + S.t = (S.k - 1) * S.dt; + S.nowX = new Float32Array(n); + S.nowZ = new Float32Array(n); +} + +const placeX = (S, k) => (k + 0.5) * S.sp; + +// Move the braid on to time t, finishing generators on the way. +function advance(S, t) { + while (t >= S.g0 + 1) { + const [i] = S.gen; + const p = S.perm; + [p[i], p[i + 1]] = [p[i + 1], p[i]]; + S.last = S.gen; + S.g0 += 1; + S.gen = nextLetter(S); + if (p.every((s, k) => s === k)) S.home = 1; + } +} + +// Each strand's x and depth at time t (after advance). +function state(S, t, X, Z) { + const { n, perm, gen, r } = S; + for (let k = 0; k < n; k++) { + X[perm[k]] = placeX(S, k); + Z[perm[k]] = 0; + } + const [i, s] = gen; + const th = PI * ease((t - S.g0) / TURN); + const m = (placeX(S, i) + placeX(S, i + 1)) / 2; + const a = perm[i], b = perm[i + 1]; + X[a] = m - r * cos(th); + X[b] = m + r * cos(th); + // Depth is how far below the strip's middle line a point is. + Z[a] = s * r * sin(th); + Z[b] = -s * r * sin(th); +} + +function push(S) { + const t = S.k * S.dt, o = (S.k % KEEP) * S.n; + advance(S, t); + state(S, t, S.hx.subarray(o, o + S.n), S.hz.subarray(o, o + S.n)); + S.k++; +} + +export function setup(params) { + params.s = { n: 0, word: -1, rowZ: new Float32Array(W), rowS: new Int8Array(W), rowA: new Float32Array(W), px: new Float32Array(8), pz: new Float32Array(8), qx: new Float32Array(8), qz: new Float32Array(8) }; +} + +export function update(dt, input, params) { + const S = params.s; + const k = input.knobValues || [0, 2, 3, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + const n = cl(round(k[2]), 3, 7), word = cl(round(k[3]), 0, 4); + if (n !== S.n || word !== S.word) reset(S, n, word); + S.t += (dt * cl(k[1], 0.05, 3)) / 2 / GEN; + while (S.k * S.dt <= S.t) push(S); + advance(S, S.t); + state(S, S.t, S.nowX, S.nowZ); + S.home = max(0, S.home - dt * 1.2); +} + +function setDisc(display, cx, cy, rad, rgb, lv) { + for (let y = max(0, floor(cy - rad - 1)); y <= min(BAND - 1, floor(cy + rad + 1)); y++) + for (let x = max(0, floor(cx - rad - 1)); x <= min(W - 1, floor(cx + rad + 1)); x++) { + const d = sqrt((x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2); + const a = cl(rad - d + 0.5, 0, 1); + if (a <= 0) continue; + // A little white in the middle, like a bead lit from above. + const w = 0.45 * max(0, 1 - d / (rad * 0.7)); + const v = a * lv; + display.setPixel(x, y, (rgb[0] * (1 - w) + w) * v * 255, (rgb[1] * (1 - w) + w) * v * 255, (rgb[2] * (1 - w) + w) * v * 255); + } +} + +export function draw(display, params) { + const S = params.s; + if (!S.n) return; + const { n, sp, r, dt, rowZ, rowS, rowA } = S; + const cols = []; + for (let s = 0; s < n; s++) cols.push(hsv(0.55 + S.hue + (0.8 * s) / (n - 1), 0.78, 1)); + + // ---- The space-time picture. Row y shows the braid u = y + 0.5 - BAND + // rows ago. The newest sample sits frac of a row below the strip. + const last = S.k - 1, frac = (S.t - last * dt) / dt; + const hw = 0.15 * sp, gw = 0.07 * sp + 0.8, eps = 0.2 * r; + const { px, pz, qx, qz } = S; + for (let y = BAND; y < H; y++) { + const u = y + 0.5 - BAND, mm = u - frac; + let w; + if (mm < 0) { + // Between now and the newest sample. + const o = (last % KEEP) * n; + for (let s = 0; s < n; s++) (px[s] = S.nowX[s]), (pz[s] = S.nowZ[s]), (qx[s] = S.hx[o + s]), (qz[s] = S.hz[o + s]); + w = (mm + frac) / max(frac, 1e-6); + } else { + const j = floor(mm); + if (last - j - 1 < 0) break; + const o0 = ((last - j) % KEEP) * n, o1 = ((last - j - 1) % KEEP) * n; + for (let s = 0; s < n; s++) (px[s] = S.hx[o0 + s]), (pz[s] = S.hz[o0 + s]), (qx[s] = S.hx[o1 + s]), (qz[s] = S.hz[o1 + s]); + w = mm - j; + } + rowS.fill(-1); + for (let s = 0; s < n; s++) { + const xc = px[s] + (qx[s] - px[s]) * w, z = pz[s] + (qz[s] - pz[s]) * w; + // Across a sloped strand a row cuts a wider piece. + const sec = sqrt(1 + (qx[s] - px[s]) ** 2); + const reach = (hw + gw) * sec + 1; + for (let x = max(0, floor(xc - reach)); x <= min(W - 1, floor(xc + reach)); x++) { + const d = abs(x + 0.5 - xc) / sec; + if (d >= hw + gw) continue; + const a = cl(hw - d + 0.5, 0, 1) * (1 - 0.4 * (d / hw) ** 2); + const held = rowS[x]; + if (held < 0 || (held !== s && z > rowZ[x] + eps)) { + // In front: its core, or the dark gap round it, covers what is behind. + rowS[x] = s; + rowZ[x] = z; + rowA[x] = max(0, a); + } else if (held === s) rowA[x] = max(rowA[x], a); + else if (z >= rowZ[x] - eps && a > rowA[x]) { + // Level with it: the brighter wins, and no gap is cut. + rowS[x] = s; + rowZ[x] = z; + rowA[x] = a; + } + } + } + // Older rows dim a little, and a strand behind is a little darker. + const fade = 1 - (0.4 * u) / ROWS; + for (let x = 0; x < W; x++) { + const s = rowS[x]; + if (s < 0 || rowA[x] <= 0) continue; + const v = rowA[x] * fade * (0.86 + (0.14 * rowZ[x]) / r); + const c = cols[s]; + display.setPixel(x, y, c[0] * v * 255, c[1] * v * 255, c[2] * v * 255); + } + } + + // ---- The strip. A faint floor, the home of each colour as a tick on the + // near edge (bright when its own strand stands on it), and a flash of the + // edges when every strand is home. + const home = S.home; + for (let x = 0; x < W; x++) { + const e = 34 + 150 * home; + display.setPixel(x, 0, e, e, e * 1.1); + display.setPixel(x, BAND - 1, e, e, e * 1.1); + for (let y = 1; y < BAND - 1; y++) display.setPixel(x, y, 5, 6, 12); + } + for (let k = 0; k < n; k++) { + const c = cols[k], x0 = placeX(S, k), lv = S.perm[k] === k ? 0.9 : 0.3; + for (let x = floor(x0 - 2); x < x0 + 2; x++) display.setPixel(x, BAND - 2, c[0] * lv * 255, c[1] * lv * 255, c[2] * lv * 255); + } + // The circle the turning pair runs on, while it turns. + const [gi, gs] = S.gen, p = (S.t - S.g0) / TURN; + if (p < 1) { + const m = (placeX(S, gi) + placeX(S, gi + 1)) / 2, lv = 70 * sin(PI * cl(p, 0, 1)) ** 0.5; + for (let y = 1; y < BAND - 1; y++) + for (let x = max(0, floor(m - r - 1)); x <= min(W - 1, floor(m + r + 1)); x++) { + const d = abs(sqrt((x + 0.5 - m) ** 2 + (y + 0.5 - CY) ** 2) - r); + if (d < 0.7) display.setPixel(x, y, lv, lv, lv * 1.15); + } + } + // The points. + const pr = cl(0.3 * sp, 2.3, 3.6); + for (let s = 0; s < n; s++) setDisc(display, S.nowX[s], CY + S.nowZ[s], pr, cols[s], 1); + // The generator playing, as its index with a minus for an inverse. + const dg = DIGITS[gi + 1] || DIGITS[0]; + for (let b = 0; b < 15; b++) if (dg[b] === "1") display.setPixel(5 + (b % 3), 2 + floor(b / 3), 150, 150, 165); + if (gs < 0) for (let x = 1; x < 4; x++) display.setPixel(x, 4, 150, 150, 165); +} From bbd5304d1cf0eec714aa2cd5c6443739c99cb59a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:07:30 +0000 Subject: [PATCH 08/19] incoming: Silencio and Artin Braids, pattern round 67 --- incoming/pattern-study/patterns/artin-braids.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/artin-braids.js b/incoming/pattern-study/patterns/artin-braids.js index 9e497c144..7b909a830 100644 --- a/incoming/pattern-study/patterns/artin-braids.js +++ b/incoming/pattern-study/patterns/artin-braids.js @@ -17,7 +17,7 @@ // Knob 4: Word. Which braid word plays, round and round, starting again with every point home when it is turned: 0 random generators and inverses; 1 the full twist (s1 s2 ... s(n-1))^n, a pure braid that brings every colour home after n(n-1) crossings; 2 the Borromean braid (s1 s2^-1)^3 on three neighbouring strands, the three moving along; 3 the braid relation as a loop, s1 s2 s1 followed by the inverse of s2 s1 s2, six crossings that are the trivial braid; 4 a random positive braid, every crossing the same hand. const W = 64, H = 128; -const { floor, round, min, max, abs, sqrt, exp, sin, cos, PI } = Math; +const { floor, round, min, max, abs, sqrt, sin, cos, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); const smooth = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u)); @@ -28,7 +28,7 @@ function hsv(h, s, v) { } // The strip is rows 0 to BAND - 1; below it, one row of the space-time -// picture per DT of braid time, the newest at the top. +// picture per S.dt of braid time, the newest at the top. const BAND = 30, CY = 14.5, ROWS = H - BAND, KEEP = ROWS + 3; // One generator takes a unit of braid time: the half turn runs over the // first 0.9 of it, nearly at an even pace, and the points rest for the rest. From a054cae9fcc59b0c32f23b076739c30913e97ed8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:10:18 +0000 Subject: [PATCH 09/19] incoming: Distance Field and Walsh Functions, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/distance-field.js | 41 +++++++++++++++---- .../pattern-study/patterns/walsh-functions.js | 2 +- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/incoming/pattern-study/patterns/distance-field.js b/incoming/pattern-study/patterns/distance-field.js index 6759cb24b..3b7482d81 100644 --- a/incoming/pattern-study/patterns/distance-field.js +++ b/incoming/pattern-study/patterns/distance-field.js @@ -31,13 +31,15 @@ function hsv(h, s, v) { // Each glyph is a union of simple polygons that may overlap: rectangles, // slanted bars cut level at both ends, and bands along an ellipse whose // arcs are flattened into chords of at most 5 degrees. The box is about -// 56 by 116, x 4..60 and y 6..122. Strokes are 11 to 13 LEDs wide, the -// Hangul ones 9 to 10, so at 12 LEDs a texel the thin ones go first. +// 56 by 116, x 4..60 and y 6..122. The round strokes are 12 LEDs wide, the +// straight ones 8 to 15 and the Hangul ones 8 to 12, so at 12 LEDs a +// texel the thin ones go first. const rect = (x0, y0, x1, y1) => [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]; // A bar from a top edge centred on xt, wt wide, to a bottom edge on xb. const bar = (xt, yt, wt, xb, yb, wb) => [[xt - wt / 2, yt], [xt + wt / 2, yt], [xb + wb / 2, yb], [xb - wb / 2, yb]]; // A band hw either side of an ellipse from angle a0 to a1 in degrees, -// clockwise on the panel (0 is right, 90 down), cut square to the curve. +// clockwise on the panel (0 is right, 90 down), its ends cut along the +// radius. function arc(cx, cy, rx, ry, hw, a0, a1) { const n = ceil(abs(a1 - a0) / 5), out = [], inn = []; for (let i = 0; i <= n; i++) { @@ -74,11 +76,27 @@ const GLYPHS = [ [arc(32, 37, 22, 25, 6, -160, 92), rect(26, 58, 38, 92), disc(32, 112, 8.5)], ]; -// Even-odd test against one simple polygon. -function inPoly(P, x, y) { +// Even-odd test against one simple polygon, kept flat as x0 y0 x1 y1 ... +// after its bounding box. +function flat(P) { + const f = new Float64Array(4 + 2 * P.length); + f[0] = f[1] = Infinity; + f[2] = f[3] = -Infinity; + P.forEach(([x, y], i) => { + f[4 + 2 * i] = x; + f[5 + 2 * i] = y; + if (x < f[0]) f[0] = x; + if (y < f[1]) f[1] = y; + if (x > f[2]) f[2] = x; + if (y > f[3]) f[3] = y; + }); + return f; +} +function inPoly(f, x, y) { + if (x < f[0] || y < f[1] || x > f[2] || y > f[3]) return false; let c = false; - for (let i = 0, j = P.length - 1; i < P.length; j = i++) { - const [xi, yi] = P[i], [xj, yj] = P[j]; + for (let i = 4, j = f.length - 2; i < f.length; j = i, i += 2) { + const xi = f[i], yi = f[i + 1], xj = f[j], yj = f[j + 1]; if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) c = !c; } return c; @@ -93,7 +111,11 @@ function inPoly(P, x, y) { // minimum of each polygon's own distance would be wrong inside, wherever // two strokes join. function outline(polys) { - const inside = (x, y) => polys.some((P) => inPoly(P, x, y)); + const flats = polys.map(flat); + const inside = (x, y) => { + for (const f of flats) if (inPoly(f, x, y)) return true; + return false; + }; const segs = []; polys.forEach((P, i) => { for (let k = 0; k < P.length; k++) { @@ -133,7 +155,8 @@ function sdf(G, x, y) { let d2 = Infinity; for (let i = 0; i < s.length; i += 4) { const ax = s[i], ay = s[i + 1], dx = s[i + 2] - ax, dy = s[i + 3] - ay; - const t = cl(((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy), 0, 1); + let t = ((x - ax) * dx + (y - ay) * dy) / (dx * dx + dy * dy); + t = t < 0 ? 0 : t > 1 ? 1 : t; const ex = x - ax - t * dx, ey = y - ay - t * dy, e = ex * ex + ey * ey; if (e < d2) d2 = e; } diff --git a/incoming/pattern-study/patterns/walsh-functions.js b/incoming/pattern-study/patterns/walsh-functions.js index 9c3a43530..b20d029b4 100644 --- a/incoming/pattern-study/patterns/walsh-functions.js +++ b/incoming/pattern-study/patterns/walsh-functions.js @@ -5,7 +5,7 @@ // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 -// Lineage: Walsh functions (Joseph L. Walsh, "A closed set of normal orthogonal functions", 1923), Sylvester's Hadamard matrices (James Joseph Sylvester, 1867), Paley's dyadic ordering (Raymond Paley, 1932), pattern round 67 +// Lineage: Walsh functions (Joseph L. Walsh, "A closed set of normal orthogonal functions", 1923), Sylvester's construction of Hadamard matrices (James Joseph Sylvester, 1867), Paley's dyadic ordering (Raymond Paley, 1932), pattern round 67 // Round: 67 // =============================== // @matrix 64x128 From e22b2839890aebb3c83c877ff1aea0bf21210013 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:10:24 +0000 Subject: [PATCH 10/19] incoming: Braille Relief and Blinking Vortex, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/blinking-vortex.js | 213 +++++++++++ .../pattern-study/patterns/braille-relief.js | 336 ++++++++++++++++++ 2 files changed, 549 insertions(+) create mode 100644 incoming/pattern-study/patterns/blinking-vortex.js create mode 100644 incoming/pattern-study/patterns/braille-relief.js diff --git a/incoming/pattern-study/patterns/blinking-vortex.js b/incoming/pattern-study/patterns/blinking-vortex.js new file mode 100644 index 000000000..16f6136ef --- /dev/null +++ b/incoming/pattern-study/patterns/blinking-vortex.js @@ -0,0 +1,213 @@ +// ===== Patternflow pattern ===== +// Title: Blinking Vortex +// Family: sim +// Idea: Two point vortices one above the other take turns, each spinning the fluid round itself for half a period while the other rests, and the bands of dye on the panel, traced back exactly through every turn, are stretched and folded into filaments until they are finer than an LED and blend to grey, when a fresh dye is laid. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: the blinking vortex flow (Hassan Aref, Stirring by chaotic advection, J. Fluid Mech. 143, 1984), pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Strength=1..16, Dye=0..3 +// +// Knob 1: Hue. Turns both dyes round the wheel: at 0 one is azure and the other amber, nearly opposite, so where they are mixed finer than an LED they average to a neutral grey. +// Knob 2: Speed. Half periods a second: 1 at 2, 0.1 at 0.2, 1.5 at 3. +// Knob 3: Strength. The stirring strength mu = Gamma T / (2 pi a^2), with 2a the distance between the vortices: fluid at the midpoint turns mu/2 radians each half period. Near 1 each vortex winds the bands into its own spiral round a grey core and the bands between and beyond stay whole; from about 4 the grey joins up into one S round both vortices with a few islands of dye holding out inside it; at 16 the middle is grey within a few half periods. A dye lasts 48/mu half periods, 10 at mu up to 4.8 and 4 from 11 up. The whole history is traced again at the new strength, so the picture answers at once. +// Knob 4: Dye. How the dye is laid: 0 eight bands across the panel, 16 rows each, azure, water, amber, water and again; 1 two halves either side of the line through the vortices; 2 eight stripes down the panel, 8 columns each; 3 rings round the midpoint, 6 LEDs each. +// Buttons: any knob's button lays a fresh dye now. + +const W = 64, H = 128; +const { floor, min, max, abs, sqrt, sin, cos, 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 [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]; +} + +// Aref's flow had a circular wall; this one is the unbounded plane, so each +// vortex alone is the exact point-vortex field: fluid at distance r turns +// about it at Gamma / (2 pi r^2) radians a second and r never changes. A half +// period is then an exact rotation by an angle that depends on r alone, +// theta(r) = Gamma T / (4 pi r^2) = mu a^2 / (2 r^2), +// with 2a the distance between the vortices. The vortices themselves never +// move: only one is on at a time, and a vortex is not moved by its own field. +const AV = 24, CX = 32, YA = 64 - AV, YB = 64 + AV, A2 = AV * AV; + +// A dye lasts 48 / mu half periods (4 to 10), then a fresh one fades in at +// the start of the next. By then most of the middle has been mixed finer +// than an LED. +const life = (mu) => cl(round(48 / mu), 4, 10); +// A fresh start begins this far into a dye, so the stirring shows at once. +const AHEAD = 2; + +// The angles reach thousands of radians next to a vortex, so sine and cosine +// come from a table of 4096 steps with linear interpolation: an error near +// 3e-7, far below an LED, at half the cost of Math.sin and Math.cos. +const TB = 4096, MASK = TB - 1, KT = TB / (2 * PI); +const SN = new Float64Array(TB + 1), CS = new Float64Array(TB + 1); +for (let i = 0; i <= TB; i++) (SN[i] = sin(i / KT)), (CS[i] = cos(i / KT)); + +// One sample an LED, at its centre, and the LED's own square traced back +// with it: each half period's rotation has an exact Jacobian, so the square's +// two edges are carried back step by step and the dye is averaged over the +// strip of the old dye they span. Where the stirring has pulled one LED's +// worth of fluid out over many bands, the LED shows their mean colour +// instead of one band picked at random, and the mixed middle reads as a +// blend rather than as sparkle. +// +// A dye is a pattern along one coordinate u: bands (u = y), the two halves +// either side of the vortices (u = x - 32), stripes (u = x) or rings round +// the midpoint (u = distance). Within a period P, dye A fills [0, a1) and dye +// B fills [b0, b1); the rest is clear water. +const LAYOUTS = [ + { u: 1, P: 64, a1: 16, b0: 32, b1: 48 }, + { u: 3 }, + { u: 0, P: 32, a1: 8, b0: 16, b1: 24 }, + { u: 2, P: 24, a1: 6, b0: 12, b1: 18 }, +]; + +// The length of [lo, hi) mod P that lies below u. +function below(u, P, lo, hi) { + const k = floor(u / P), r = u - k * P - lo; + return k * (hi - lo) + (r < 0 ? 0 : r > hi - lo ? hi - lo : r); +} + +// The share of dye A and dye B in a box of width w round u, into out. +function cover(L, u, w, out) { + if (L.u === 3) { + const b = w < 1e-6 ? (u < 0 ? 0 : 1) : cl(u / w + 0.5, 0, 1); + out[0] = 1 - b, out[1] = b; + return; + } + if (w < 1e-3) { + const r = u - floor(u / L.P) * L.P; + out[0] = r < L.a1 ? 1 : 0, out[1] = r >= L.b0 && r < L.b1 ? 1 : 0; + return; + } + if (w > 1e7) { + out[0] = L.a1 / L.P, out[1] = (L.b1 - L.b0) / L.P; + return; + } + const lo = u - w / 2, hi = u + w / 2; + out[0] = (below(hi, L.P, 0, L.a1) - below(lo, L.P, 0, L.a1)) / w; + out[1] = (below(hi, L.P, L.b0, L.b1) - below(lo, L.P, L.b0, L.b1)) / w; +} + +export function setup(params) { + params.s = { + n: AHEAD, f: 0, born: 0, gen: 0, old: -1, fade: 1, + hue: 0, mu: 1, layout: 0, + cv: [0, 0], + }; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + S.hue = k[0]; + S.mu = cl(k[2], 1, 16); + S.layout = cl(round(k[3]), 0, 3); + const rate = d * cl(k[1], 0.2, 3) * 0.5; + S.f += rate; + // A new dye fades in over 0.4 of a half period while the old one, still + // being stirred under it, fades out. + if (S.old >= 0 && (S.fade += rate * 2.5) >= 1) S.old = -1; + const lay = () => ((S.old = S.born), (S.born = S.n), (S.gen ^= 1), (S.fade = 0)); + while (S.f >= 1) { + S.f -= 1; + S.n++; + if (S.old < 0 && S.n - S.born >= life(S.mu)) lay(); + } + // A press lays a fresh dye now. + if (input.btnPressed && input.btnPressed.some(Boolean) && S.old < 0) lay(); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const n = S.n, f = S.f, c = (S.mu * A2) / 2, L = LAYOUTS[S.layout], gen = S.gen, cv = S.cv; + // The new dye was laid at the start of half period born: it has been turned + // by the whole half periods born..n-1 and part of n. An old dye still + // fading out has been turned longer. + const sNew = n - S.born + 1, sOld = S.old >= 0 ? n - S.old + 1 : -1; + const steps = max(sNew, sOld); + const fi = S.fade, wNew = sOld >= 0 ? fi * fi * (3 - 2 * fi) : 1; + const dyeA = hsv(0.58 + S.hue, 0.75, 0.95), dyeB = hsv(0.09 + S.hue, 0.8, 0.95), water = [0.02, 0.03, 0.07]; + // Colour of a box holding shares a of the first dye and b of the second. + const mix = (a, b, g, ch) => (g ? dyeB[ch] * a + dyeA[ch] * b : dyeA[ch] * a + dyeB[ch] * b) + water[ch] * (1 - a - b); + for (let j = 0; j < H; j++) + for (let i = 0; i < W; i++) { + let x = i + 0.5, y = j + 0.5; + // The LED's two edges, carried back with it. + let ux = 1, uy = 0, vx = 0, vy = 1; + // Undo the part of the current half period already turned, then the + // whole half periods before it, newest first. Half period m is + // stirred by vortex A when m is even and by B when it is odd. + let frac = f, m = n, aN = 0, bN = 0; + for (let s = 0; s < steps; s++) { + const cy = m & 1 ? YB : YA, dx = x - CX, dy = y - cy, r2 = dx * dx + dy * dy + 1e-9; + const th = (frac * c) / r2; + let a = th * KT; + if (a > 1e9) a %= TB; + const a0 = a | 0, t = a - a0, q = a0 & MASK; + const co = CS[q] + t * (CS[q + 1] - CS[q]), sn = SN[q] + t * (SN[q + 1] - SN[q]); + // Back in time: rotate by minus the angle. The angle falls off as + // 1/r^2, so an edge e first shears by (2 theta / r^2)(d . e) along + // the circle and then turns with the point. + const sh = (2 * th) / r2, pu = sh * (dx * ux + dy * uy), pv = sh * (dx * vx + dy * vy); + const ux1 = ux - dy * pu, uy1 = uy + dx * pu, vx1 = vx - dy * pv, vy1 = vy + dx * pv; + ux = co * ux1 + sn * uy1, uy = co * uy1 - sn * ux1; + vx = co * vx1 + sn * vy1, vy = co * vy1 - sn * vx1; + x = CX + dx * co + dy * sn; + y = cy - dx * sn + dy * co; + frac = 1; + m--; + if (s + 1 === sNew) coverAt(L, x, y, ux, uy, vx, vy, cv), (aN = cv[0]), (bN = cv[1]); + } + let r = mix(aN, bN, gen, 0), g = mix(aN, bN, gen, 1), b = mix(aN, bN, gen, 2); + if (wNew < 1) { + coverAt(L, x, y, ux, uy, vx, vy, cv); + const k = 1 - wNew; + r = r * wNew + k * mix(cv[0], cv[1], gen ^ 1, 0); + g = g * wNew + k * mix(cv[0], cv[1], gen ^ 1, 1); + b = b * wNew + k * mix(cv[0], cv[1], gen ^ 1, 2); + } + display.setPixel(i, j, (255 * min(1, r)) | 0, (255 * min(1, g)) | 0, (255 * min(1, b)) | 0); + } + // The vortices: the one stirring burns white, the resting one is a dim ring. + const on = n & 1 ? 1 : 0; + for (let v = 0; v < 2; v++) { + const cy = v ? YB : YA, act = v === on; + for (let dy = -2; dy <= 1; dy++) + for (let dx = -2; dx <= 1; dx++) { + const r = sqrt((dx + 0.5) * (dx + 0.5) + (dy + 0.5) * (dy + 0.5)); + const x = CX + dx, y = cy + dy; + if (act) { + const b = r < 1 ? 255 : r < 2 ? 150 : 0; + if (b) display.setPixel(x, y, b, b, b); + } else if (r > 1 && r < 2.2) display.setPixel(x, y, 120, 120, 130); + else if (r < 1) display.setPixel(x, y, 0, 0, 0); + } + } +} + +// The dye shares at the traced-back point (x, y), averaged over the width the +// LED's traced-back edges cover along the layout's coordinate. +function coverAt(L, x, y, ux, uy, vx, vy, out) { + let u, w; + if (L.u === 1) (u = y), (w = abs(uy) + abs(vy)); + else if (L.u === 0) (u = x), (w = abs(ux) + abs(vx)); + else if (L.u === 3) (u = x - CX), (w = abs(ux) + abs(vx)); + else { + const dx = x - CX, dy = y - 64, r = sqrt(dx * dx + dy * dy) + 1e-9, gx = dx / r, gy = dy / r; + (u = r), (w = abs(gx * ux + gy * uy) + abs(gx * vx + gy * vy)); + } + // A square pulled out longer than a period does not stay straight: the + // flow folds it, so it crosses the bands even when its two traced edges + // happen to lie along them. Past a period, its length counts as width. + const s = sqrt(ux * ux + uy * uy + vx * vx + vy * vy), P = L.P || 32; + cover(L, u, max(w, (s * s) / (s + P)), out); +} diff --git a/incoming/pattern-study/patterns/braille-relief.js b/incoming/pattern-study/patterns/braille-relief.js new file mode 100644 index 000000000..0dccc3139 --- /dev/null +++ b/incoming/pattern-study/patterns/braille-relief.js @@ -0,0 +1,336 @@ +// ===== Patternflow pattern ===== +// Title: Braille Relief +// Family: type +// Idea: A page of embossed six-dot braille lies in the dark under a low lamp that circles it, so each raised dot shows only as a lit side and a long shadow, while a fingertip glow reads along the lines cell by cell and the print letter it has just felt surfaces small under the cell and fades. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: six-dot braille (Louis Braille, Procédé pour écrire les paroles, la musique et le plain-chant au moyen de points, 1829), uncontracted English letters; text from Helen Keller, The Story of My Life (1903); pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Relief=0.4..2.4, Light=0..1 +// +// Knob 1: Hue. Turns the lamp, the shade and the print round the wheel together: at 0 a cream lamp on cool grey paper, blue-violet in the shadows, the print letters pale sky blue. +// Knob 2: Speed. How fast the fingertip reads: 2.5 cells a second at 2, so the 40 cells of the first page take 16 s, 0.25 at 0.2 and 3.75 at 3. The lamp circles the page once in 16 s at 2, once in 36 s at 0.2, so the light moves however slowly the page is read. +// Knob 3: Relief. The height of each dot, from 0.4 LED, barely raised, where the lamp finds a lit rim and a short shadow, to 2.4 LEDs, where every dot throws a shadow down the page across the dots behind it. +// Knob 4: Light. How high the lamp stands: 3 degrees above the paper at 0 (4 at the opening 0.06), where the dots blaze on one side and their shadows run 19 times their height (14 at the opening); 20 degrees at 1, where the shadows shrink to under 3 times and the dots fade to a faint crescent. +// Buttons: any knob's button turns to the next page. + +const W = 64, H = 128; +const { floor, ceil, min, max, sqrt, sin, cos, exp, 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]; +} + +// ---- Braille. Dots are numbered 1-2-3 down the left column and 4-5-6 down +// the right. The first ten letters use only dots 1, 2, 4 and 5; k to t are +// a to j with dot 3 added; u, v, x, y and z are a to e with dots 3 and 6 +// added; w stands outside the series as 2-4-5-6. A word broken at the end of +// a line takes the hyphen, dots 3-6. Checked against Unicode's braille block: +// a to z come out as U+2801 2803 2809 2819 2811 280B 281B 2813 280A 281A 2805 +// 2807 280D 281D 2815 280F 281F 2817 280E 281E 2825 2827 283A 282D 283D 2835. +const AJ = [[1], [1, 2], [1, 4], [1, 4, 5], [1, 5], [1, 2, 4], [1, 2, 4, 5], [1, 2, 5], [2, 4], [2, 4, 5]]; +const DOTS = { " ": [], "-": [3, 6], w: [2, 4, 5, 6] }; +for (let i = 0; i < 10; i++) { + DOTS["abcdefghij"[i]] = AJ[i]; + DOTS["klmnopqrst"[i]] = [...AJ[i], 3]; + if (i < 5) DOTS["uvxyz"[i]] = [...AJ[i], 3, 6]; +} + +// Helen Keller at the well-house, from chapter IV of The Story of My Life. +// Grade 1 braille, letters and spaces only: the capital sign (dot 6) before +// Everything and I is left out, and so are the comma and the full stops. No +// number sign is needed, as the text has no numbers. 119 letters in 29 words +// fill four pages with 4 hyphens and 354 raised dots. +const TEXT = + "every|thing had a name and each name gave birth to a new thought as we re|turned to the house every ob|ject which i touched seemed to quiv|er with life"; + +// ---- The page. Dots 4 LEDs apart inside a cell, cells 10 apart, six to a +// line; lines 18 apart, seven to a page, which leaves a gap of 7 rows under +// each line for the print letter. Embossed braille keeps about 2.5 mm, 6 mm +// and 10 mm; this page keeps the first two near that ratio and sets its lines +// a little wider for the letters. +const DP = 4, CP = 10, NC = 6, LP = 18, NL = 7, X0 = 5, Y0 = 3, R = 1.75; +const PER = NC * NL; + +// Words go whole onto a line when they fit. A word that does not fit the +// room left is broken at a syllable marked | in the text if its first part +// and a hyphen fit, and otherwise starts a new line. A word longer than the +// whole line (thought, touched) has no other way and is broken after five +// letters. +function paginate(text) { + const cells = []; + let col = 0; + const put = (ch) => { + cells.push(ch); + col = (col + 1) % NC; + }; + const newline = () => { + while (col > 0) put(" "); + }; + for (const word of text.split(" ")) { + const parts = word.split("|"); + let w = parts.join(""); + if (w.length > NC - col) { + let cut = 0; + for (let i = 1, k = 0; i < parts.length; i++) { + k += parts[i - 1].length; + if (k + 1 <= NC - col) cut = k; + } + if (cut) { + for (const ch of w.slice(0, cut)) put(ch); + put("-"); + w = w.slice(cut); + } else newline(); + } + while (w.length > NC) { + for (const ch of w.slice(0, NC - 1)) put(ch); + put("-"); + w = w.slice(NC - 1); + } + for (const ch of w) put(ch); + if (col > 0) put(" "); + } + while (cells.length % PER) cells.push(" "); + const pages = []; + for (let i = 0; i < cells.length; i += PER) pages.push(cells.slice(i, i + PER)); + return pages; +} +const PAGES = paginate(TEXT); +// The fingertip stops at the last letter of a page, not at the blank cells +// that pad the last page out. +const ENDS = PAGES.map((pg) => { + let e = PER; + while (e > 1 && pg[e - 1] === " ") e--; + return e; +}); + +// Dot slot s = (line * NC + cell) * 6 + (dot - 1); its centre falls on a +// corner between LEDs, so a dot is a round 4 by 4 patch. +const NS = PER * 6, SX = new Float32Array(NS), SY = new Float32Array(NS); +for (let s = 0; s < NS; s++) { + const c = floor(s / 6), k = s % 6; + SX[s] = X0 + (c % NC) * CP + floor(k / 3) * DP; + SY[s] = Y0 + floor(c / NC) * LP + (k % 3) * DP; +} +const raised = (page, s) => DOTS[page[floor(s / 6)]].includes((s % 6) + 1); + +// ---- Print letters, 3 by 5, as small capitals: lowercase would need +// descenders the gap under a line has no room for. +const FONT = { + a: "010101111101101", b: "110101110101110", c: "011100100100011", d: "110101101101110", + e: "111100110100111", f: "111100110100100", g: "011100101101011", h: "101101111101101", + i: "111010010010111", j: "001001001101010", k: "101101110101101", l: "100100100100111", + m: "101111111101101", n: "110101101101101", o: "010101101101010", p: "110101110100100", + q: "010101101110011", r: "110101110101101", s: "011100010001110", t: "111010010010010", + u: "101101101101111", v: "101101101101010", w: "101101111111101", x: "101101010101101", + y: "101101010010010", z: "111001010100111", "-": "000000111000000", +}; + +// ---- Paper grain: a fixed field of tiny slopes from a hash, smoothed, so the +// paper glints a little where its fibres face the lamp. +const GX = new Float32Array(W * H), GY = new Float32Array(W * H); +{ + const hz = new Float32Array(W * H); + for (let i = 0; i < W * H; i++) { + let h = (i * 2654435761) >>> 0; + h = Math.imul(h ^ (h >>> 15), 2246822519) >>> 0; + h = Math.imul(h ^ (h >>> 13), 3266489917) >>> 0; + hz[i] = ((h ^ (h >>> 16)) >>> 0) / 4294967296 - 0.5; + } + const sm = new Float32Array(W * H); + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + let a = 0; + for (let dy = -1; dy <= 1; dy++) + for (let dx = -1; dx <= 1; dx++) a += hz[cl(y + dy, 0, H - 1) * W + cl(x + dx, 0, W - 1)]; + sm[y * W + x] = a / 9; + } + for (let y = 0; y < H; y++) + for (let x = 0; x < W; x++) { + GX[y * W + x] = 0.04 * (sm[y * W + min(W - 1, x + 1)] - sm[y * W + max(0, x - 1)]); + GY[y * W + x] = 0.04 * (sm[min(H - 1, y + 1) * W + x] - sm[max(0, y - 1) * W + x]); + } +} + +export function setup(params) { + params.s = { + t: 0, hue: 0, speed: 2, relief: 0.4, light: 0.06, az: -2.4, + page: 0, read: 0, cell: 0, hold: 0, turn: 0, + seen: new Float32Array(PER).fill(-99), + lift: new Float32Array(NS), + cov: new Float32Array(W * H), lit: new Float32Array(W * H), hgt: new Float32Array(W * H), + own: new Int16Array(W * H), sh: new Float32Array(W * H), rgb: new Float32Array(W * H * 3), + }; + const S = params.s; + for (let s = 0; s < NS; s++) S.lift[s] = raised(PAGES[0], s) ? 1 : 0; + S.seen[0] = 0; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 0.4, 0.06], d = cl(dt || 0, 0, 0.1); + S.t += d; + S.hue = k[0]; + S.speed = cl(k[1], 0.2, 3); + S.relief = cl(k[2], 0.4, 2.4); + S.light = cl(k[3], 0, 1); + // The lamp keeps circling however slowly the page is read. + S.az += d * (0.15 + 0.12 * S.speed); + let page = PAGES[S.page]; + if (S.turn === 0) { + // Reading: the fingertip steps cell by cell, gliding over the first + // quarter of each step. + S.read += d * 1.25 * S.speed; + const c = floor(S.read); + if (c >= ENDS[S.page]) (S.turn = 1), (S.hold = 0), (S.read = ENDS[S.page] - 1e-6); + else if (c !== S.cell) (S.cell = c), (S.seen[c] = S.t); + } else { + // The page turns: hold 1.2 s, the dots sink in 0.8 s, and the next + // page is embossed line by line from the top. + S.hold += d; + if (S.turn === 1) { + const u = cl((S.hold - 1.2) / 0.8, 0, 1); + for (let s = 0; s < NS; s++) S.lift[s] = raised(page, s) ? 1 - u * u * (3 - 2 * u) : 0; + if (u >= 1) (S.turn = 2), (S.hold = 0), (S.page = (S.page + 1) % PAGES.length), (page = PAGES[S.page]), S.seen.fill(-99); + } else { + for (let s = 0; s < NS; s++) { + const u = cl((S.hold - 0.18 * floor(s / 6 / NC)) / 0.5, 0, 1); + S.lift[s] = raised(page, s) ? u * u * (3 - 2 * u) : 0; + } + if (S.hold > 0.18 * NL + 0.5) { + S.turn = 0, S.read = 0, S.cell = 0; + S.seen.fill(-99); + S.seen[0] = S.t; + } + } + } + // Any button turns to the next page now. + if (S.turn === 0 && input.btnPressed && input.btnPressed.some(Boolean)) (S.turn = 1), (S.hold = 1.2); +} + +export function draw(display, params) { + const S = params.s; + if (!S) return; + const { cov, lit, hgt, own, sh, lift, rgb } = S; + // The lamp: elevation e, toward-the-lamp direction (ux, uy) on the page. + const e = ((3 + 17 * S.light) * PI) / 180, se = sin(e), ce = cos(e), te = se / ce; + const ux = cos(S.az), uy = sin(S.az), Lx = ce * ux, Ly = ce * uy; + cov.fill(0), lit.fill(0), hgt.fill(0), own.fill(-1), sh.fill(0); + + // Domes: h = Hd (1 - r^2 / R^2), shaded at 3 by 3 points in each LED by + // the lamp's angle to the surface. + for (let s = 0; s < NS; s++) { + const Hd = S.relief * lift[s]; + if (Hd < 0.01) continue; + const cx = SX[s], cy = SY[s], g = (2 * Hd) / (R * R); + for (let py = cy - 2; py < cy + 2; py++) + for (let px = cx - 2; px < cx + 2; px++) { + const p = py * W + px; + let n = 0, v = 0; + for (let sy = 0; sy < 3; sy++) + for (let sx = 0; sx < 3; sx++) { + const dx = px + (sx + 0.5) / 3 - cx, dy = py + (sy + 0.5) / 3 - cy, r2 = dx * dx + dy * dy; + if (r2 >= R * R) continue; + // Normal (g dx, g dy, 1), unnormalised. + const hx = g * dx, hy = g * dy; + v += max(0, (hx * Lx + hy * Ly + se) / sqrt(hx * hx + hy * hy + 1)); + n++; + } + if (!n) continue; + cov[p] = n / 9, lit[p] = v / 9; + const dx = px + 0.5 - cx, dy = py + 0.5 - cy, r2 = dx * dx + dy * dy; + if (r2 < R * R) (hgt[p] = Hd * (1 - r2 / (R * R))), (own[p] = s); + } + } + + // Cast shadows. A point at height z0 is in the shadow of a dome when the + // ray from it toward the lamp, climbing te per LED, passes under the dome. + // For the paraboloid this has a closed form: with d the offset from the + // dome's centre split along (dp) and across (dq) the lamp's direction, the + // ray's lowest clearance is + // f = Hd + te^2 R^2 / (4 Hd) - Hd dq^2 / R^2 + te dp - z0, + // reached at distance s = -dp - te R^2 / (2 Hd) along the ray, so the + // shadow on flat paper is a parabola Hd / te long. f > 0 is shadow, softened + // over a penumbra that widens with s. + for (let s = 0; s < NS; s++) { + const Hd = S.relief * lift[s]; + if (Hd < 0.01) continue; + const cx = SX[s], cy = SY[s], len = Hd / te + R + 1; + const x0 = max(0, floor(min(cx, cx - len * ux) - R - 1)), x1 = min(W - 1, ceil(max(cx, cx - len * ux) + R + 1)); + const y0 = max(0, floor(min(cy, cy - len * uy) - R - 1)), y1 = min(H - 1, ceil(max(cy, cy - len * uy) + R + 1)); + const sStar0 = (te * R * R) / (2 * Hd), k0 = Hd + (te * te * R * R) / (4 * Hd), kq = Hd / (R * R); + for (let py = y0; py <= y1; py++) + for (let px = x0; px <= x1; px++) { + const p = py * W + px; + if (own[p] === s) continue; + const dx = px + 0.5 - cx, dy = py + 0.5 - cy, dp = dx * ux + dy * uy, dq = -dx * uy + dy * ux; + const ss = -dp - sStar0; + if (ss <= 0) continue; + const f = k0 - kq * dq * dq + te * dp - hgt[p]; + if (f <= -0.3) continue; + const a = cl(f / (0.1 + 0.05 * ss) + 0.5, 0, 1); + if (a > sh[p]) sh[p] = a; + } + } + + // The fingertip rests on the cell being read and glides on to the next + // one over the first quarter of each step. + const cell = min(PER - 1, floor(S.read)), fr = S.read - cell, gl = min(1, fr * 4); + let fx = X0 + (cell % NC) * CP + 2, fy = Y0 + floor(cell / NC) * LP + 4; + if (S.turn === 0 && cell > 0 && gl < 1) { + const pc = cell - 1, sm = gl * gl * (3 - 2 * gl); + const qx = X0 + (pc % NC) * CP + 2, qy = Y0 + floor(pc / NC) * LP + 4; + (fx = qx + (fx - qx) * sm), (fy = qy + (fy - qy) * sm); + } + // The hand lifts while the page turns. + const touch = S.turn === 0 ? 1 : S.turn === 1 ? max(0, 1 - S.hold / 1.2) : 0; + + // Compose. Brightness goes by the ratio of light on a surface to the + // light on bare paper, so the paper stays dim at every elevation; a + // distant lamp falls off a little across the page. + const lamp = hsv(0.11 + S.hue, 0.45, 1), shade = hsv(0.68 + S.hue, 0.7, 1), paperC = hsv(0.62 + S.hue, 0.12, 0.95); + for (let py = 0; py < H; py++) + for (let px = 0; px < W; px++) { + const p = py * W + px; + const gx = GX[p], gy = GY[p]; + const paper = max(0, (-gx * Lx - gy * Ly + se) / sqrt(1 + gx * gx + gy * gy)); + let v = (lit[p] + (1 - cov[p]) * paper) * (1 - 0.92 * sh[p]); + let rho = v / se; + rho *= 1 + (0.3 * ((px - 32) * ux + (py - 64) * uy)) / 64; + const dx = px + 0.5 - fx, dy = py + 0.5 - fy, gf = touch * exp(-(dx * dx) / 12 - (dy * dy) / 30); + rho = rho * (1 + 1.2 * gf) + 0.5 * gf; + const b = 1 - exp(-0.16 * rho * rho), amb = 0.04 + 0.03 * cov[p]; + // Dim light reads brown on LEDs, so the lamp's colour comes in with + // its brightness: the paper is a cool grey, the lit sides cream. + const w = b < 0.15 ? 0 : b > 0.6 ? 1 : (b - 0.15) / 0.45; + rgb[p * 3] = (paperC[0] + (lamp[0] - paperC[0]) * w) * b + shade[0] * amb; + rgb[p * 3 + 1] = (paperC[1] + (lamp[1] - paperC[1]) * w) * b + shade[1] * amb; + rgb[p * 3 + 2] = (paperC[2] + (lamp[2] - paperC[2]) * w) * b + shade[2] * amb; + } + + // The print letters, under the cells read so far, each surfacing as the + // fingertip reaches it and fading over 3 s. + const page = PAGES[S.page], ink = hsv(0.56 + S.hue, 0.45, 1); + for (let c = 0; c < PER; c++) { + const age = S.t - S.seen[c], glyph = FONT[page[c]]; + if (!glyph || age < 0 || age > 3.2) continue; + let a = age < 0.15 ? age / 0.15 : age < 0.8 ? 1 : max(0, 1 - (age - 0.8) / 2.4); + if (S.turn) a *= touch; + if (a <= 0.01) continue; + const lx = X0 + (c % NC) * CP + 1, ly = Y0 + floor(c / NC) * LP + 11; + for (let r = 0; r < 5; r++) + for (let q = 0; q < 3; q++) { + if (glyph[r * 3 + q] !== "1") continue; + const o = ((ly + r) * W + lx + q) * 3; + rgb[o] += ink[0] * a, rgb[o + 1] += ink[1] * a, rgb[o + 2] += ink[2] * a; + } + } + for (let p = 0; p < W * H; p++) + display.setPixel(p % W, floor(p / W), (255 * min(1, rgb[p * 3])) | 0, (255 * min(1, rgb[p * 3 + 1])) | 0, (255 * min(1, rgb[p * 3 + 2])) | 0); +} From c866eaa2318cadb3160099c6e3354390ecda66fa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:11:47 +0000 Subject: [PATCH 11/19] incoming: Hanging Line draft, Dupin Cyclide comments, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/dupin-cyclide.js | 7 +- .../pattern-study/patterns/hanging-line.js | 349 ++++++++++++++++++ 2 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 incoming/pattern-study/patterns/hanging-line.js diff --git a/incoming/pattern-study/patterns/dupin-cyclide.js b/incoming/pattern-study/patterns/dupin-cyclide.js index b4a9f67f1..e5f2f4e8b 100644 --- a/incoming/pattern-study/patterns/dupin-cyclide.js +++ b/incoming/pattern-study/patterns/dupin-cyclide.js @@ -97,8 +97,9 @@ function build(S) { objs[1].push({ fam, i, C: circleThrough(P[0], P[1], P[2]) }); } // Sample every circle and fit each surface to its box. The turn is about - // the vertical axis, which both surfaces are symmetric about, so the - // widest the drawing gets is twice the largest distance from that axis. + // the vertical axis, and a half turn about it maps each surface onto + // itself, so the drawing is never wider than twice the largest distance + // from that axis. S.objs = objs.map((list, o) => { let ylo = 1e9, yhi = -1e9, rad = 0; const pts = list.map(({ C }) => { @@ -133,7 +134,7 @@ export function update(dt, input, params) { // Shape: tube over centre radius 0.5 (ring) to 1 (horn) to 1.6 (spindle). const sh = cl(k[2], 0, 2); S.r = sh <= 1 ? 0.5 + 0.5 * sh : 1 + 0.6 * (sh - 1); - // Distance: the gap between the torus and the centre, in torus sizes. + // Distance: the gap between the torus and the centre, in outer radii. S.del = 1.2 * exp(3.2 * cl(k[3], 0, 1)); const key = S.r.toFixed(4) + "," + S.del.toFixed(4); if (key !== S.key) { diff --git a/incoming/pattern-study/patterns/hanging-line.js b/incoming/pattern-study/patterns/hanging-line.js new file mode 100644 index 000000000..492b88464 --- /dev/null +++ b/incoming/pattern-study/patterns/hanging-line.js @@ -0,0 +1,349 @@ +// ===== Patternflow pattern ===== +// Title: Hanging Line +// Family: type +// Idea: Kim Sowol's poem Azaleas types itself jamo by jamo, initial, vowel, final, in a geometric out-of-the-square Hangul after Ahn Sang-soo's face, every jamo one fixed shape so the syllables hang from a common top line with ragged bottoms, until the Frame knob squeezes each syllable into the same square box the way a conventional face does. +// Author: patternflow-lab +// Date: 2026-10-29 +// SPDX-License-Identifier: CC-BY-SA-4.0 +// Lineage: 안상수체, the out-of-the-square (탈네모꼴) three-set Hangul face of Ahn Sang-soo (1985), whose 19 initials, 21 vowels and 27 finals each keep one shape; the jamo here are drawn after it for this pattern, not copied; the text is Kim Sowol's 진달래꽃 (Azaleas, 1922); pattern round 67 +// Round: 67 +// =============================== +// @matrix 64x128 +// @knobs Hue=0..1, Speed=0.2..3, Size=1..2, Frame=0..1 +// +// Knob 1: Hue. Turns the ink round the wheel: at 0 the text is a pale azalea pink and each new jamo lands in full azalea magenta, under dim teal hanging lines. +// Knob 2: Speed. Steps a second, a step being one jamo, a space or a line break: 4.4 at the opening 2, so the poem's 264 steps take a minute, 0.44 at 0.2 and 6.6 at 3. +// Knob 3: Size. The jamo cell: 7 LEDs at 1, where up to five syllables fit a line and five and a half lines the panel, 13 LEDs at 2, where two or three do and the lines wrap again. +// Knob 4: Frame. Out of the square at 0: each jamo one shape and size, every syllable hanging from its line's top. At 1 each syllable is stretched into the same 13 by 13 square, as a conventional (네모꼴) face resizes its jamo per syllable, and a syllable still being typed jumps in size as each jamo arrives. The opening 0.06 sits near the hanging line. + +const W = 64, H = 128; +const { floor, round, min, max, abs, sqrt, exp } = Math; +const cl = (v, a, b) => (v < a ? a : v > b ? b : v); +function hsv(h, s, v) { + h = (h - floor(h)) * 6; + const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); + return [i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p, i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p, i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p]; +} + +// 진달래꽃 in today's spelling: 94 syllables, 222 jamo. +const POEM = [ + "나 보기가 역겨워", "가실 때에는", "말없이 고이 보내 드리우리다", "", + "영변에 약산", "진달래꽃", "아름 따다 가실 길에 뿌리우리다", "", + "가시는 걸음걸음", "놓인 그 꽃을", "사뿐히 즈려밟고 가시옵소서", "", + "나 보기가 역겨워", "가실 때에는", "죽어도 아니 눈물 흘리우리다", "", +]; + +// ---- The face. Drawn after 안상수체, not copied from it: straight strokes +// and circles on a grid one LED apart at Size 1. A consonant fills a 7 by 7 +// cell (0..6), the same cell as an initial and as a final. Strokes are +// polylines [x0, y0, x1, y1, ...]; a circle is [cx, cy, r, 0, 0] tagged by +// length 5. Only the 16 consonant shapes and 11 vowels the poem uses are here. +const C = { + g: [[0, 0, 6, 0, 6, 6]], + n: [[0, 0, 0, 6, 6, 6]], + d: [[6, 0, 0, 0, 0, 6, 6, 6]], + r: [[0, 0, 6, 0, 6, 3, 0, 3, 0, 6, 6, 6]], + m: [[0, 0, 6, 0, 6, 6, 0, 6, 0, 0]], + b: [[0, 0, 0, 6, 6, 6, 6, 0], [0, 3, 6, 3]], + s: [[3, 0, 0, 6], [3, 0, 6, 6]], + o: [[3, 3, 3, 0, 0]], + j: [[0, 0, 6, 0], [3, 0, 0, 6], [3, 0, 6, 6]], + c: [[2, 0, 4, 0], [0, 2, 6, 2], [3, 2, 0, 6], [3, 2, 6, 6]], + // ㅎ's circle is too small for a round ring one LED wide to stay open at + // this size, so it is the eight-sided ring those LEDs make. + h: [[2, 0, 4, 0], [0, 1, 6, 1], [2, 3, 4, 3, 5, 4, 5, 5, 4, 6, 2, 6, 1, 5, 1, 4, 2, 3]], +}; +// A double or a cluster sets two consonants side by side, each squeezed to +// three LEDs. +function pair(a, b) { + const sq = (st, dx) => (st.length === 5 ? [dx + st[0] / 3, st[1], st[2], 0, 0] : st.map((v, i) => (i % 2 ? v : dx + v / 3))); + return [...C[a].map((s) => sq(s, 0)), ...C[b].map((s) => sq(s, 4))]; +} +const INI = { 0: C.g, 1: pair("g", "g"), 2: C.n, 3: C.d, 4: pair("d", "d"), 5: C.r, 6: C.m, 7: C.b, 8: pair("b", "b"), 9: C.s, 11: C.o, 12: C.j, 18: C.h }; +const FIN = { 1: C.g, 4: C.n, 8: C.r, 11: pair("r", "b"), 16: C.m, 17: C.b, 18: pair("b", "s"), 21: C.o, 23: C.c, 27: C.h }; +// Vertical vowels stand right of the initial from x = 8; their stem runs +// from the top down to y = 11, the middle of a final set under the initial +// at y = 8..14, whether the syllable has a final or not: one shape. +const V = (list, x0, len) => list.map((s) => s.map((v, i) => (i % 2 ? (v === 99 ? len : v) : v + x0))); +const STEM = (x) => [x, 0, x, 99]; +const VERT = { + 0: [STEM(0), [0, 3, 2, 3]], // ㅏ + 1: [STEM(0), [0, 3, 2, 3], STEM(4)], // ㅐ + 2: [STEM(0), [0, 2, 2, 2], [0, 4, 2, 4]], // ㅑ + 4: [[0, 3, 2, 3], STEM(2)], // ㅓ + 5: [[0, 3, 2, 3], STEM(2), STEM(4)], // ㅔ + 6: [[0, 2, 2, 2], [0, 4, 2, 4], STEM(2)], // ㅕ + 20: [STEM(0)], // ㅣ +}; +// Horizontal vowels hang under the initial from y = 8; the final goes two +// rows under the vowel's lowest row. +const HORI = { + 8: [[[3, 8, 3, 9], [0, 10, 6, 10]], 10], // ㅗ + 13: [[[0, 8, 6, 8], [3, 8, 3, 10]], 10], // ㅜ + 18: [[[0, 8, 6, 8]], 8], // ㅡ +}; +// Compounds: the horizontal part under the initial, the vertical part +// beside it with its stem down to the middle of where the final would sit. +const COMP = { 14: [13, 4] }; // ㅝ = ㅜ + ㅓ + +// A syllable as three stroke lists (initial, vowel, final) in grid units. +function syllable(ch) { + const k = ch.charCodeAt(0) - 0xac00, ci = floor(k / 588), vi = floor((k % 588) / 28), fi = k % 28; + let vow, fy; + if (VERT[vi]) { + vow = V(VERT[vi], 8, 11); + fy = 8; + } else if (HORI[vi]) { + vow = HORI[vi][0]; + fy = HORI[vi][1] + 2; + } else { + const [h, v] = COMP[vi]; + fy = HORI[h][1] + 2; + vow = [...HORI[h][0], ...V(VERT[v], 8, fy + 3)]; + } + const fin = fi ? FIN[fi].map((s) => (s.length === 5 ? [s[0], s[1] + fy, s[2], 0, 0] : s.map((v, i) => (i % 2 ? v + fy : v)))) : []; + return [INI[ci], vow, fin]; +} +// The box a list of strokes fills: right and bottom edges, in LEDs. +function extent(lists) { + let w = 1, h = 1; + for (const L of lists) + for (const s of L) { + if (s.length === 5) { + w = max(w, s[0] + s[2] + 1); + h = max(h, s[1] + s[2] + 1); + } else + for (let i = 0; i < s.length; i += 2) { + w = max(w, s[i] + 1); + h = max(h, s[i + 1] + 1); + } + } + return [w, h]; +} + +// ---- The page. Each copy of the poem starts on a fresh line, so the +// layout repeats copy after copy and the cursor can wrap round unseen. +const COPIES = 3, SQ = 13, TRACK = 2, SPACE = 4, LEAD = 4, MARGIN = 2; + +function tokens() { + const out = []; + let step = 0; + for (let c = 0; c < COPIES; c++) + for (const line of POEM) { + if (line === "") { + out.push({ t: "stanza", step }); + step += 2; + continue; + } + line.split(" ").forEach((word, wi) => { + if (wi) out.push({ t: "space", step: step++ }); + const syl = [...word].map((ch) => { + const parts = syllable(ch); + const n = parts[2].length ? 3 : 2; + const tok = { t: "syl", step, n, parts, ext: [extent([parts[0]]), extent([parts[0], parts[1]]), extent(parts)] }; + step += n; + return tok; + }); + out.push({ t: "word", syl }); + }); + out.push({ t: "line", step: step++ }); + } + return { list: out, steps: step }; +} + +export function setup(params) { + const { list, steps } = tokens(); + const S = (params.s = { list, steps, copySteps: steps / COPIES, cur: 0, scroll: 0, key: "", blink: 0 }); + S.maxH = 0; + for (const tk of list) if (tk.t === "word") for (const s of tk.syl) S.maxH = max(S.maxH, s.ext[2][1]); + // Open in the second copy, on 「아름 따다 가실 길에」, with the first + // copy's lines filling the page above. + let n = 0; + for (const tk of list) + if (tk.t === "word") + for (const s of tk.syl) + if (++n === 94 + 38) S.cur = s.step; + S.R = new Float32Array(W * H); + S.G = new Float32Array(W * H); + S.B = new Float32Array(W * H); +} + +// Where every syllable goes for this Size and Frame: slot x, line top y, +// slot width, in LEDs. +function layout(S) { + const sc = S.size, f = S.frame; + const pitch = ((1 - f) * S.maxH + f * SQ + LEAD) * sc; + const adv = (w) => ((1 - f) * w + f * SQ + TRACK) * sc; + let x = MARGIN, y = 0, lineIx = 0; + S.lines = []; + const newLine = (gap) => { + x = MARGIN; + y += pitch * gap; + lineIx++; + }; + for (const tk of S.list) { + if (tk.t === "stanza") newLine(0.45); + else if (tk.t === "line") newLine(1); + else if (tk.t === "space") { + if (x > MARGIN) x += SPACE * sc; + } else if (tk.t === "word") { + const wW = tk.syl.reduce((a, s) => a + adv(s.ext[2][0]), 0) - TRACK * sc; + if (x > MARGIN && x + wW > W - MARGIN) newLine(1); + for (const s of tk.syl) { + const aw = adv(s.ext[2][0]); + if (x > MARGIN && x + aw - TRACK * sc > W - MARGIN) newLine(1); + s.x = x; + s.y = y; + s.line = lineIx; + s.w = aw - TRACK * sc; + x += aw; + } + } + } + S.pitch = pitch; + // The height of one copy: where the first syllable of each copy sits. + const firsts = []; + let k = 0; + for (const tk of S.list) if (tk.t === "word") for (const s of tk.syl) if (k++ % 94 === 0) firsts.push(s.y); + S.period = firsts[1] - firsts[0]; +} + +// The syllable the cursor is on, or the last one before it. +function caretSyl(S) { + let last = null; + for (const tk of S.list) { + if (tk.t !== "word") { + if (tk.step !== undefined && tk.step > S.cur) break; + continue; + } + for (const s of tk.syl) { + if (s.step > S.cur) return last; + last = s; + } + } + return last; +} + +export function update(dt, input, params) { + if (!params.s) setup(params); + const S = params.s, k = input.knobValues || [0, 2, 1, 0.06]; + dt = cl(dt || 0, 0, 0.1); + S.hue = k[0] || 0; + const speed = cl(k[1], 0.2, 3); + S.size = cl(k[2], 1, 2); + S.frame = cl(k[3], 0, 1); + S.blink += dt; + S.cur += dt * speed * 2.2; + // Past the second copy, step back a copy: the page looks the same. + let jumped = false; + if (S.cur >= 2 * S.copySteps) { + S.cur -= S.copySteps; + jumped = true; + } + const key = S.size.toFixed(3) + "," + S.frame.toFixed(3); + const relaid = key !== S.key; + if (relaid) { + S.key = key; + layout(S); + } + if (jumped && !relaid) S.scroll -= S.period; + // Keep the cursor's line on the panel: its tallest possible syllable + // ends two rows above the bottom edge. + const cs = caretSyl(S); + S.caret = cs; + const lineH = ((1 - S.frame) * S.maxH + S.frame * SQ) * S.size; + const target = cs ? cs.y + lineH - (H - 3) : 0; + if (relaid || S.first === undefined) S.scroll = target; + else S.scroll += (target - S.scroll) * (1 - exp(-dt * 5)); + S.first = 1; +} + +// ---- Drawing: strokes as distances, one LED wide at Size 1. +function plot(S, p, a, col) { + if (a <= 0) return; + if (col[0] * a > S.R[p]) S.R[p] = col[0] * a; + if (col[1] * a > S.G[p]) S.G[p] = col[1] * a; + if (col[2] * a > S.B[p]) S.B[p] = col[2] * a; +} +function seg(S, x0, y0, x1, y1, hw, col) { + const dx = x1 - x0, dy = y1 - y0, L = dx * dx + dy * dy; + const xa = max(0, floor(min(x0, x1) - hw - 1)), xb = min(W - 1, floor(max(x0, x1) + hw + 1)); + const ya = max(0, floor(min(y0, y1) - hw - 1)), yb = min(H - 1, floor(max(y0, y1) + hw + 1)); + for (let y = ya; y <= yb; y++) + for (let x = xa; x <= xb; x++) { + const px = x - x0, py = y - y0, t = L ? cl((px * dx + py * dy) / L, 0, 1) : 0; + const ex = px - t * dx, ey = py - t * dy; + plot(S, y * W + x, cl(hw + 0.5 - sqrt(ex * ex + ey * ey), 0, 1), col); + } +} +// An ellipse ring; the distance to it is taken to first order. +function ring(S, cx, cy, rx, ry, hw, col) { + for (let y = max(0, floor(cy - ry - hw - 1)); y <= min(H - 1, cy + ry + hw + 1); y++) + for (let x = max(0, floor(cx - rx - hw - 1)); x <= min(W - 1, cx + rx + hw + 1); x++) { + const u = (x - cx) / rx, v = (y - cy) / ry, k = sqrt(u * u + v * v); + const g = sqrt((u / rx) ** 2 + (v / ry) ** 2); + const d = g > 1e-6 ? (k * k - k) / g : min(rx, ry); + plot(S, y * W + x, cl(hw + 0.5 - abs(d), 0, 1), col); + } +} + +export function draw(display, params) { + if (!params.s || !params.s.lines) return; + const S = params.s, sc = S.size, f = S.frame, hw = 0.5 + 0.35 * (sc - 1); + S.R.fill(0); + S.G.fill(0); + S.B.fill(0); + const ink = hsv(S.hue + 0.93, 0.42, 0.9).map((v) => v * 255); + const hot = hsv(S.hue + 0.88, 0.85, 1).map((v) => v * 255); + const guide = hsv(S.hue + 0.5, 0.7, 0.2).map((v) => v * 255); + const lineH = ((1 - f) * S.maxH + f * SQ) * sc; + const top = S.scroll; + // The hanging lines: where each line's typed text ends. + const ends = new Map(); + for (const tk of S.list) { + if (tk.t !== "word") continue; + for (const s of tk.syl) { + if (s.step > S.cur) break; + const y = s.y - top; + if (y + lineH < 0 || y > H) continue; + ends.set(s.line, [y, max(ends.get(s.line)?.[1] ?? 0, s.x + s.w)]); + const typed = min(s.n, floor(S.cur - s.step) + 1); + // The box this much of the syllable fills, and the stretch that puts + // it in the square: none at Frame 0, all of it at 1. + const [bw, bh] = s.ext[typed - 1]; + const fx = (1 - f + (f * SQ) / bw) * sc, fy = (1 - f + (f * SQ) / bh) * sc; + const ox = round(s.x), oy = round(y); + for (let j = 0; j < typed; j++) { + const age = S.cur - s.step - j; + const glow = exp(-age / 4); + const col = ink.map((v, i) => v + (hot[i] - v) * glow); + for (const st of s.parts[j]) { + if (st.length === 5) ring(S, ox + round(st[0] * fx * 2) / 2, oy + round(st[1] * fy * 2) / 2, st[2] * fx, st[2] * fy, hw, col); + else + for (let i = 0; i + 3 < st.length; i += 2) + seg(S, ox + round(st[i] * fx), oy + round(st[i + 1] * fy), ox + round(st[i + 2] * fx), oy + round(st[i + 3] * fy), hw, col); + } + } + } + } + // The guide: the top line every syllable hangs from, fading out toward + // the square, where a bottom line fades in. + for (const [, [y, xe]] of ends) { + const yt = round(y) - 2, yb = round(y + SQ * sc) + 1; + for (let x = MARGIN; x < min(W, xe); x++) { + if (yt >= 0 && yt < H) plot(S, yt * W + x, 1 - 0.6 * f, guide); + if (f > 0 && yb >= 0 && yb < H) plot(S, yb * W + x, f, guide); + } + } + // The caret: the slot being typed, lit on its hanging line. + const c = S.caret; + if (c) { + const on = 0.55 + 0.45 * Math.cos(S.blink * 7); + const y = round(c.y - top) - 2; + if (y >= 0 && y < H) for (let x = round(c.x); x < min(W, c.x + c.w); x++) plot(S, y * W + x, on, hot); + } + for (let p = 0; p < W * H; p++) { + if (S.R[p] + S.G[p] + S.B[p] < 1) continue; + display.setPixel(p % W, floor(p / W), min(255, S.R[p]) | 0, min(255, S.G[p]) | 0, min(255, S.B[p]) | 0); + } +} From 9c45ae07825a2bef86e4bd273c4c118d563127ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:13:35 +0000 Subject: [PATCH 12/19] incoming: Hanging Line and Dupin Cyclide, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- incoming/pattern-study/patterns/hanging-line.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/patterns/hanging-line.js b/incoming/pattern-study/patterns/hanging-line.js index 492b88464..7d50083bc 100644 --- a/incoming/pattern-study/patterns/hanging-line.js +++ b/incoming/pattern-study/patterns/hanging-line.js @@ -175,7 +175,7 @@ function layout(S) { const pitch = ((1 - f) * S.maxH + f * SQ + LEAD) * sc; const adv = (w) => ((1 - f) * w + f * SQ + TRACK) * sc; let x = MARGIN, y = 0, lineIx = 0; - S.lines = []; + S.laid = true; const newLine = (gap) => { x = MARGIN; y += pitch * gap; @@ -288,7 +288,7 @@ function ring(S, cx, cy, rx, ry, hw, col) { } export function draw(display, params) { - if (!params.s || !params.s.lines) return; + if (!params.s || !params.s.laid) return; const S = params.s, sc = S.size, f = S.frame, hw = 0.5 + 0.35 * (sc - 1); S.R.fill(0); S.G.fill(0); From f68412ec2cd3a6186875b4e18cd03d4616f6b275 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:25:11 +0000 Subject: [PATCH 13/19] incoming: Blinking Vortex with soft shaded dye for the edges x motion cell, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/blinking-vortex.js | 141 +++++++++--------- 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/incoming/pattern-study/patterns/blinking-vortex.js b/incoming/pattern-study/patterns/blinking-vortex.js index 16f6136ef..a6ca67aef 100644 --- a/incoming/pattern-study/patterns/blinking-vortex.js +++ b/incoming/pattern-study/patterns/blinking-vortex.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Blinking Vortex // Family: sim -// Idea: Two point vortices one above the other take turns, each spinning the fluid round itself for half a period while the other rests, and the bands of dye on the panel, traced back exactly through every turn, are stretched and folded into filaments until they are finer than an LED and blend to grey, when a fresh dye is laid. +// Idea: Two point vortices one above the other take turns, each spinning the fluid round itself for half a period while the other rests, and soft bands of two dyes, traced back exactly through every turn, are stretched and folded into filaments until they are finer than an LED and blend to one even colour, when a fresh dye is laid. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -9,16 +9,16 @@ // Round: 67 // =============================== // @matrix 64x128 -// @knobs Hue=0..1, Speed=0.2..3, Strength=1..16, Dye=0..3 +// @knobs Hue=0..1, Speed=0.2..3, Strength=2..16, Dye=0..3 // -// Knob 1: Hue. Turns both dyes round the wheel: at 0 one is azure and the other amber, nearly opposite, so where they are mixed finer than an LED they average to a neutral grey. -// Knob 2: Speed. Half periods a second: 1 at 2, 0.1 at 0.2, 1.5 at 3. -// Knob 3: Strength. The stirring strength mu = Gamma T / (2 pi a^2), with 2a the distance between the vortices: fluid at the midpoint turns mu/2 radians each half period. Near 1 each vortex winds the bands into its own spiral round a grey core and the bands between and beyond stay whole; from about 4 the grey joins up into one S round both vortices with a few islands of dye holding out inside it; at 16 the middle is grey within a few half periods. A dye lasts 48/mu half periods, 10 at mu up to 4.8 and 4 from 11 up. The whole history is traced again at the new strength, so the picture answers at once. -// Knob 4: Dye. How the dye is laid: 0 eight bands across the panel, 16 rows each, azure, water, amber, water and again; 1 two halves either side of the line through the vortices; 2 eight stripes down the panel, 8 columns each; 3 rings round the midpoint, 6 LEDs each. +// Knob 1: Hue. Turns both dyes round the wheel: at 0 one is turquoise and the other violet, both brightest in blue, so where they are mixed finer than an LED the brightness holds and only the colour blends to periwinkle. +// Knob 2: Speed. Half periods a second: 2.2 at 2, 0.22 at 0.2, 3.3 at 3. +// Knob 3: Strength. The stirring strength mu = Gamma T / (2 pi a^2), with 2a the distance between the vortices: fluid at the midpoint turns mu/2 radians each half period. At 2 each vortex winds the bands into its own spiral and the bands beyond them stay whole while an S of mixed dye grows between; past about 5 the S fills the middle within two half periods. A dye lasts 12/mu half periods, 6 at 2 and 3 from 4 up. The whole history is traced again at the new strength, so the picture answers at once. +// Knob 4: Dye. How the dye is laid: 0 bands across the panel, 20 rows each; 1 two halves either side of the line through the vortices, shaded in bands of 32 rows; 2 stripes down the panel, 16 columns each; 3 rings round the midpoint, 16 LEDs each. Every band is brightest along its middle and dark where the colour changes. // Buttons: any knob's button lays a fresh dye now. const W = 64, H = 128; -const { floor, min, max, abs, sqrt, sin, cos, round, PI } = Math; +const { floor, min, max, abs, sqrt, sin, cos, exp, round, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); function hsv(h, s, v) { @@ -34,12 +34,12 @@ function hsv(h, s, v) { // theta(r) = Gamma T / (4 pi r^2) = mu a^2 / (2 r^2), // with 2a the distance between the vortices. The vortices themselves never // move: only one is on at a time, and a vortex is not moved by its own field. -const AV = 24, CX = 32, YA = 64 - AV, YB = 64 + AV, A2 = AV * AV; +const AV = 30, CX = 32, YA = 64 - AV, YB = 64 + AV, A2 = AV * AV; -// A dye lasts 48 / mu half periods (4 to 10), then a fresh one fades in at -// the start of the next. By then most of the middle has been mixed finer -// than an LED. -const life = (mu) => cl(round(48 / mu), 4, 10); +// A dye lasts 12 / mu half periods (3 to 6), then a fresh one fades in at +// the start of the next. By then an S of dye mixed finer than an LED joins +// the two vortices. +const life = (mu) => cl(round(12 / mu), 3, 6); // A fresh start begins this far into a dye, so the stirring shows at once. const AHEAD = 2; @@ -52,65 +52,62 @@ for (let i = 0; i <= TB; i++) (SN[i] = sin(i / KT)), (CS[i] = cos(i / KT)); // One sample an LED, at its centre, and the LED's own square traced back // with it: each half period's rotation has an exact Jacobian, so the square's -// two edges are carried back step by step and the dye is averaged over the -// strip of the old dye they span. Where the stirring has pulled one LED's -// worth of fluid out over many bands, the LED shows their mean colour -// instead of one band picked at random, and the mixed middle reads as a -// blend rather than as sparkle. +// two edges are carried back step by step, and the dye is averaged over the +// patch of the first dye they cover. Where the stirring has pulled one LED's +// worth of fluid out over many bands, the LED shows their mean instead of +// one band picked at random. // -// A dye is a pattern along one coordinate u: bands (u = y), the two halves -// either side of the vortices (u = x - 32), stripes (u = x) or rings round -// the midpoint (u = distance). Within a period P, dye A fills [0, a1) and dye -// B fills [b0, b1); the rest is clear water. +// The dye has two parts laid along one coordinate u: bands (u = y), stripes +// (u = x) or rings round the midpoint (u = distance), or two halves either +// side of the vortices (u = x - 32, with the shade along y). Its colour, +// dye A or dye B, changes sharply every P / 2 along u. Its shade rises and +// falls as a cosine of period P / 2, darkest where the colour changes and +// brightest in the middle of each band. The two colours share +// their brightest channel, so a colour edge never shows as a step in +// brightness; only the shade does, and the shade is soft. const LAYOUTS = [ - { u: 1, P: 64, a1: 16, b0: 32, b1: 48 }, - { u: 3 }, - { u: 0, P: 32, a1: 8, b0: 16, b1: 24 }, - { u: 2, P: 24, a1: 6, b0: 12, b1: 18 }, + { u: 1, P: 40 }, + { u: 3, P: 64 }, + { u: 0, P: 32 }, + { u: 2, P: 32 }, ]; +// Shade: L0 - L1 cos(4 pi u / P), from 0.17 to 0.97. +const L0 = 0.57, L1 = 0.4; +// The shade is averaged as if the LED were BLUR times its traced size, so a +// band of shade fades into its mean before it is squeezed steep enough to +// step more than 40 levels from one LED to the next; the colour is averaged +// over the LED's own traced size. +const BLUR = 3; -// The length of [lo, hi) mod P that lies below u. -function below(u, P, lo, hi) { - const k = floor(u / P), r = u - k * P - lo; - return k * (hi - lo) + (r < 0 ? 0 : r > hi - lo ? hi - lo : r); +// The length of [0, P/2) mod P that lies below u. +function below(u, P) { + const k = floor(u / P), r = u - k * P; + return k * (P / 2) + (r > P / 2 ? P / 2 : r); } -// The share of dye A and dye B in a box of width w round u, into out. -function cover(L, u, w, out) { - if (L.u === 3) { - const b = w < 1e-6 ? (u < 0 ? 0 : 1) : cl(u / w + 0.5, 0, 1); - out[0] = 1 - b, out[1] = b; - return; - } - if (w < 1e-3) { - const r = u - floor(u / L.P) * L.P; - out[0] = r < L.a1 ? 1 : 0, out[1] = r >= L.b0 && r < L.b1 ? 1 : 0; - return; - } - if (w > 1e7) { - out[0] = L.a1 / L.P, out[1] = (L.b1 - L.b0) / L.P; - return; - } - const lo = u - w / 2, hi = u + w / 2; - out[0] = (below(hi, L.P, 0, L.a1) - below(lo, L.P, 0, L.a1)) / w; - out[1] = (below(hi, L.P, L.b0, L.b1) - below(lo, L.P, L.b0, L.b1)) / w; +// The share of dye A in a box of width w round u. +function shareA(halves, u, w, P) { + if (halves) return w < 1e-6 ? (u < 0 ? 1 : 0) : cl(0.5 - u / w, 0, 1); + if (w < 1e-3) return u - floor(u / P) * P < P / 2 ? 1 : 0; + if (w > 1e7) return 0.5; + return (below(u + w / 2, P) - below(u - w / 2, P)) / w; } export function setup(params) { params.s = { n: AHEAD, f: 0, born: 0, gen: 0, old: -1, fade: 1, - hue: 0, mu: 1, layout: 0, + hue: 0, mu: 2, layout: 0, cv: [0, 0], }; } export function update(dt, input, params) { if (!params.s) setup(params); - const S = params.s, k = input.knobValues || [0, 2, 1, 0.06], d = cl(dt || 0, 0, 0.1); + const S = params.s, k = input.knobValues || [0, 2, 2, 0.06], d = cl(dt || 0, 0, 0.1); S.hue = k[0]; - S.mu = cl(k[2], 1, 16); + S.mu = cl(k[2], 2, 16); S.layout = cl(round(k[3]), 0, 3); - const rate = d * cl(k[1], 0.2, 3) * 0.5; + const rate = d * cl(k[1], 0.2, 3) * 1.1; S.f += rate; // A new dye fades in over 0.4 of a half period while the old one, still // being stirred under it, fades out. @@ -135,9 +132,9 @@ export function draw(display, params) { const sNew = n - S.born + 1, sOld = S.old >= 0 ? n - S.old + 1 : -1; const steps = max(sNew, sOld); const fi = S.fade, wNew = sOld >= 0 ? fi * fi * (3 - 2 * fi) : 1; - const dyeA = hsv(0.58 + S.hue, 0.75, 0.95), dyeB = hsv(0.09 + S.hue, 0.8, 0.95), water = [0.02, 0.03, 0.07]; - // Colour of a box holding shares a of the first dye and b of the second. - const mix = (a, b, g, ch) => (g ? dyeB[ch] * a + dyeA[ch] * b : dyeA[ch] * a + dyeB[ch] * b) + water[ch] * (1 - a - b); + // Both dyes keep blue as their brightest channel at the opening hue. + const dyeA = hsv(0.51 + S.hue, 0.75, 1), dyeB = hsv(0.79 + S.hue, 0.6, 1); + const col = (a, lv, g, ch) => lv * (g ? dyeB[ch] * a + dyeA[ch] * (1 - a) : dyeA[ch] * a + dyeB[ch] * (1 - a)); for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) { let x = i + 0.5, y = j + 0.5; @@ -146,7 +143,7 @@ export function draw(display, params) { // Undo the part of the current half period already turned, then the // whole half periods before it, newest first. Half period m is // stirred by vortex A when m is even and by B when it is odd. - let frac = f, m = n, aN = 0, bN = 0; + let frac = f, m = n, aN = 0, lN = 0; for (let s = 0; s < steps; s++) { const cy = m & 1 ? YB : YA, dx = x - CX, dy = y - cy, r2 = dx * dx + dy * dy + 1e-9; const th = (frac * c) / r2; @@ -165,15 +162,15 @@ export function draw(display, params) { y = cy - dx * sn + dy * co; frac = 1; m--; - if (s + 1 === sNew) coverAt(L, x, y, ux, uy, vx, vy, cv), (aN = cv[0]), (bN = cv[1]); + if (s + 1 === sNew) dyeAt(L, x, y, ux, uy, vx, vy, cv), (aN = cv[0]), (lN = cv[1]); } - let r = mix(aN, bN, gen, 0), g = mix(aN, bN, gen, 1), b = mix(aN, bN, gen, 2); + let r = col(aN, lN, gen, 0), g = col(aN, lN, gen, 1), b = col(aN, lN, gen, 2); if (wNew < 1) { - coverAt(L, x, y, ux, uy, vx, vy, cv); + dyeAt(L, x, y, ux, uy, vx, vy, cv); const k = 1 - wNew; - r = r * wNew + k * mix(cv[0], cv[1], gen ^ 1, 0); - g = g * wNew + k * mix(cv[0], cv[1], gen ^ 1, 1); - b = b * wNew + k * mix(cv[0], cv[1], gen ^ 1, 2); + r = r * wNew + k * col(cv[0], cv[1], gen ^ 1, 0); + g = g * wNew + k * col(cv[0], cv[1], gen ^ 1, 1); + b = b * wNew + k * col(cv[0], cv[1], gen ^ 1, 2); } display.setPixel(i, j, (255 * min(1, r)) | 0, (255 * min(1, g)) | 0, (255 * min(1, b)) | 0); } @@ -194,13 +191,12 @@ export function draw(display, params) { } } -// The dye shares at the traced-back point (x, y), averaged over the width the -// LED's traced-back edges cover along the layout's coordinate. -function coverAt(L, x, y, ux, uy, vx, vy, out) { +// The dye at the traced-back point (x, y), averaged over what the LED's +// traced-back edges cover: out[0] the share of dye A, out[1] the shade. +function dyeAt(L, x, y, ux, uy, vx, vy, out) { let u, w; if (L.u === 1) (u = y), (w = abs(uy) + abs(vy)); - else if (L.u === 0) (u = x), (w = abs(ux) + abs(vx)); - else if (L.u === 3) (u = x - CX), (w = abs(ux) + abs(vx)); + else if (L.u === 0 || L.u === 3) (u = L.u === 3 ? x - CX : x), (w = abs(ux) + abs(vx)); else { const dx = x - CX, dy = y - 64, r = sqrt(dx * dx + dy * dy) + 1e-9, gx = dx / r, gy = dy / r; (u = r), (w = abs(gx * ux + gy * uy) + abs(gx * vx + gy * vy)); @@ -208,6 +204,13 @@ function coverAt(L, x, y, ux, uy, vx, vy, out) { // A square pulled out longer than a period does not stay straight: the // flow folds it, so it crosses the bands even when its two traced edges // happen to lie along them. Past a period, its length counts as width. - const s = sqrt(ux * ux + uy * uy + vx * vx + vy * vy), P = L.P || 32; - cover(L, u, max(w, (s * s) / (s + P)), out); + const s = sqrt(ux * ux + uy * uy + vx * vx + vy * vy), fold = (s * s) / (s + L.P); + out[0] = shareA(L.u === 3, u, max(w, fold), L.P); + // The halves take their shade from bands along y. + let us = u, ws = w; + if (L.u === 3) (us = y), (ws = abs(uy) + abs(vy)); + // A cosine averaged over a Gaussian as wide as the box keeps + // exp(-(k w)^2 / 24) of its swing, k its wavenumber. + const kw = ((4 * PI) / L.P) * BLUR * max(ws, fold); + out[1] = L0 - L1 * exp(-(kw * kw) / 24) * cos(((4 * PI) / L.P) * us); } From 3cda36d966eace4a158821af5cee10d74dfd9655 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:29:19 +0000 Subject: [PATCH 14/19] incoming: Radon Transform counts photons and clears between scans, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- .../pattern-study/patterns/radon-transform.js | 187 +++++++++++------- 1 file changed, 116 insertions(+), 71 deletions(-) diff --git a/incoming/pattern-study/patterns/radon-transform.js b/incoming/pattern-study/patterns/radon-transform.js index 8c802196d..0b98eff00 100644 --- a/incoming/pattern-study/patterns/radon-transform.js +++ b/incoming/pattern-study/patterns/radon-transform.js @@ -1,7 +1,7 @@ // ===== Patternflow pattern ===== // Title: Radon Transform // Family: algo -// Idea: A beam sweeps half a turn round a few ellipses of different densities, the bottom square fills row by row with their sinogram, and each projection is filtered and smeared back across the top square along its own angle, so the ellipses build up out of crossing streaks, hold, and give way to the next, slightly moved phantom. +// Idea: A beam takes half a turn round a few ellipses of different densities and counts the photons that get through, filling the bottom square row by row with a grainy sinogram, then each projection is filtered and smeared back across the top square along its own angle, so the ellipses build up out of crossing streaks, hold, and the panel clears for the next, slightly moved phantom. // Author: patternflow-lab // Date: 2026-10-29 // SPDX-License-Identifier: CC-BY-SA-4.0 @@ -11,13 +11,13 @@ // @matrix 64x128 // @knobs Hue=0..1, Speed=0.2..3, Angles=0..64, Filter=0..1 // -// Knob 1: Hue. Turns both squares round the wheel: at 0 the reconstruction is ice blue running to white and the sinogram violet. -// Knob 2: Speed. How fast the beam turns: half a turn in 6 s at 2, then the picture holds 1.5 s; at 0.2 a half turn takes 60 s, at 3 it takes 4 s. -// Knob 3: Angles. How many projections a half turn takes. 0 takes turns, doubling each cycle from 16: 16, 32, 64, then 4, 8 and round again. 1 to 64 hold that count; with few the shapes are crossed by the star of streaks, one per angle. -// Knob 4: Filter. The filter each projection passes before it is smeared back: (1 - f) c + f |w|, with c the ramp's height at 1/400 of a cycle a sample. At 0 it is plain back-projection, every shape in a blurred 1/r halo; at 0.06 the edges already come through; at 1 it is the ramp alone, sharp, with dark lobes beside every streak. +// Knob 1: Hue. Turns both squares round the wheel: at 0 the reconstruction runs from dark cyan to ice white and the sinogram from blue violet to purple. +// Knob 2: Speed. At 2 the beam's half turn takes 1 s, reading the projections back takes 6 s, and the picture holds 1.5 s, fading out over its last 0.3 s; at 0.2 everything takes ten times as long, at 3 two thirds as long. +// Knob 3: Angles. How many projections a half turn takes. 0 takes turns, doubling each cycle from 16: 16, 32, 64, then 4, 8 and round again. 1 to 64 hold that count; with few the shapes are crossed by the star of streaks, one per angle. The half turn has a fixed dose of photons shared by its projections: at 4 angles each bin gets 2,000 and the rows are clean, at 64 it gets 125 and they are grainy. +// Knob 4: Filter. The filter each projection passes before it is smeared back: (1 - f) c + f |w|, with c the ramp's height at 1/400 of a cycle a sample. At 0 it is plain back-projection, every shape in a blurred 1/r halo; at 0.06 the edges already come through; at 1 it is the ramp alone, sharp, with dark lobes beside every streak. Plain back-projection smooths the photon noise away; the ramp turns it into fine grain. const W = 64, Q = 64, H = 128; -const { floor, min, max, sqrt, sin, cos, exp, round, PI } = Math; +const { floor, min, max, sqrt, sin, cos, exp, log, round, PI } = Math; const cl = (v, a, b) => (v < a ? a : v > b ? b : v); function hsv(h, s, v, o) { @@ -63,6 +63,27 @@ function project(E, th, out) { return out; } +// Photon counts. A line of attenuation p lets through exp(-MU p) of the +// beam, so a bin that should count m photons counts a Poisson draw from m, +// and the projection is read back as -ln(count / I0) / MU, as a scanner +// reads it. One half turn has a fixed DOSE, shared by its projections: each +// of N gets DOSE / N photons a bin, so few angles give clean rows and many +// give grainy ones. The thickest line through the first phantom lets 5.4% +// of the beam through. +const MU = 0.08, DOSE = 8000; +const gauss = () => sqrt(-2 * log(1 - Math.random())) * cos(2 * PI * Math.random()); +function poisson(m) { + if (m > 40) return max(0, round(m + sqrt(m) * gauss())); + const L = exp(-m); + let k = 0; + for (let q = Math.random(); q > L; q *= Math.random()) k++; + return k; +} +function expose(p, I0) { + for (let i = 0; i < p.length; i++) p[i] = -log(max(0.5, poisson(I0 * exp(-MU * p[i]))) / I0) / MU; + return p; +} + function ramp(p, q) { for (let i = 0; i < NB; i++) { let s = 0; @@ -90,7 +111,7 @@ function smear(acc, p, th) { // The first phantom: a body and five inner shapes, one of them a hole. function firstPhantom() { return [ - [0, 0, 26, 19, 0.3, 0.5], + [0, 0, 27, 22, 0.3, 0.5], [-9, 6, 9, 5, 0.9, 0.35], [8, 7, 7, 7, 0, -0.3], [5, -9, 10, 4, -0.4, 0.3], @@ -119,34 +140,35 @@ function nextPhantom(E) { const rho = small ? 0.6 + Math.random() * 0.3 : Math.random() < 0.4 ? -0.3 : 0.25 + Math.random() * 0.2; out[k] = [r * cos(ang), r * sin(ang), a, b, Math.random() * PI, rho]; // The body breathes between rounder and longer. - out[0][2] = 24 + Math.random() * 4, out[0][3] = 16 + Math.random() * 5; + out[0][2] = 25 + Math.random() * 3, out[0][3] = 19 + Math.random() * 5; return out; } -// The continuous sinogram of a phantom, 64 angles down and the middle 64 -// bins across, and its peak. -function sinogram(E, out) { +// The sinogram a scan at all 64 angles would count at the same dose a +// projection, the middle 64 bins across, and its peak before the noise. +function sinogram(E, out, I0) { const p = new Float32Array(NB); let peak = 1e-6; for (let r = 0; r < Q; r++) { project(E, (r * PI) / Q, p); - for (let x = 0; x < Q; x++) (out[r * Q + x] = p[x + OFF]), (peak = max(peak, p[x + OFF])); + for (let x = OFF; x < OFF + Q; x++) peak = max(peak, p[x]); + expose(p, I0); + out.set(p.subarray(OFF, OFF + Q), r * Q); } return peak; } const TURNS = [4, 8, 16, 32, 64]; -// At Speed 2 a half turn takes SWEEP seconds and the result holds HOLD. -const SWEEP = 6, HOLD = 1.5; +// At Speed 2 the beam's half turn takes SCAN seconds, reading the sinogram +// back into the picture takes SWEEP, the result holds HOLD and the last +// FADE of the hold clears both squares. +const SCAN = 1, SWEEP = 6, HOLD = 1.5, FADE = 0.25, P0 = 0.75; +// A new scan starts on a cleared panel. function startCycle(S, E) { - S.prevSino.set(S.sino); - S.prevPeak = S.peak; S.E = E; - S.peak = sinogram(E, S.sino); S.cycle++; - S.prog = 0, S.hold = 0; - S.fadeFrom.set(S.shown), S.fade = S.first ? 1 : 0; + S.scan = 0, S.prog = 0, S.hold = 0; rebuild(S); } @@ -154,48 +176,54 @@ function anglesFor(S) { return S.knobN > 0 ? S.knobN : TURNS[(S.cycle + 2) % TURNS.length]; } -// Start the reconstruction over for the current angle count and take every -// projection the beam has already passed. +// Start over for the current angle count: measure every projection the +// beam has passed and smear back every one the reading has reached. function rebuild(S) { S.N = anglesFor(S); + S.peak = sinogram(S.E, S.sino, DOSE / S.N); S.P.fill(0), S.R.fill(0); - S.done = 0, S.taken.fill(-1); + S.done = 0, S.measured = 0, S.taken.fill(-1); + measure(S); take(S); } -function take(S) { - while (S.done < S.N && (S.done * PI) / S.N <= S.prog * PI + 1e-9) { - const th = (S.done * PI) / S.N; - project(S.E, th, S.p); - ramp(S.p, S.q); - smear(S.P, S.p, th); - smear(S.R, S.q, th); - S.lastP.set(S.p), S.lastQ.set(S.q), S.lastTh = th, S.lastT = S.t; +// The beam passes angle k pi / N and the detector counts its projection. +function measure(S) { + while (S.measured < S.N && S.measured / S.N <= S.scan + 1e-9) { + const k = S.measured++; + expose(project(S.E, (k * PI) / S.N, S.proj[k]), DOSE / S.N); // Its row of the sinogram, drawn up to 3 rows deep when the rows // between projections leave room. - const r0 = round((S.done / S.N) * Q), deep = cl(floor(Q / S.N / 3), 1, 3); + const r0 = round((k / S.N) * Q), deep = cl(floor(Q / S.N / 3), 1, 3); for (let j = 0; j < deep; j++) { const r = (r0 + j) % Q; - S.taken[r] = S.done, S.depth[r] = j; - S.rows[r].set(S.p.subarray(OFF, OFF + Q)); + S.taken[r] = k, S.depth[r] = j; + S.rows[r].set(S.proj[k].subarray(OFF, OFF + Q)); } + } +} + +// The reading reaches projection k: it is filtered and smeared back. +function take(S) { + while (S.done < S.measured && S.done / S.N <= S.prog + 1e-9) { + const th = (S.done * PI) / S.N, p = S.proj[S.done]; + ramp(p, S.q); + smear(S.P, p, th); + smear(S.R, S.q, th); + S.lastP.set(p), S.lastQ.set(S.q), S.lastTh = th, S.lastT = S.t; S.done++; } } export function setup(params) { const S = (params.s = { - t: 0, hue: 0, speed: 2, filt: 0.06, knobN: -1, N: 4, cycle: -1, prog: 0, hold: 0, done: 0, first: true, - E: null, P: new Float32Array(Q * Q), R: new Float32Array(Q * Q), shown: new Float32Array(Q * Q), fadeFrom: new Float32Array(Q * Q), fade: 1, - sino: new Float32Array(Q * Q), prevSino: new Float32Array(Q * Q), peak: 1, prevPeak: 1, - p: new Float32Array(NB), q: new Float32Array(NB), lastP: new Float32Array(NB), lastQ: new Float32Array(NB), lastTh: 0, lastT: -9, - taken: new Int16Array(Q).fill(-1), depth: new Uint8Array(Q), rows: Array.from({ length: Q }, () => new Float32Array(Q)), gain: 0, c: [0, 0, 0], + t: 0, hue: 0, speed: 2, filt: 0.06, knobN: -1, N: 4, cycle: -1, scan: 0, prog: 0, hold: 0, done: 0, measured: 0, first: true, + proj: Array.from({ length: 64 }, () => new Float32Array(NB)), + E: firstPhantom(), P: new Float32Array(Q * Q), R: new Float32Array(Q * Q), shown: new Float32Array(Q * Q), + sino: new Float32Array(Q * Q), peak: 1, clear: 1, + q: new Float32Array(NB), lastP: new Float32Array(NB), lastQ: new Float32Array(NB), lastTh: 0, lastT: -9, + taken: new Int16Array(Q).fill(-1), depth: new Uint8Array(Q), rows: Array.from({ length: Q }, () => new Float32Array(Q)), gain: 0, hist: new Int32Array(256), c: [0, 0, 0], }); - // The panel opens on a sinogram already under way: a phantom before the - // first stands in the rows the beam has not reached. - const E = firstPhantom(); - S.peak = sinogram(nextPhantom(E), S.sino); - S.E = E; } export function update(dt, input, params) { @@ -208,11 +236,13 @@ export function update(dt, input, params) { S.filt = cl(k[3], 0, 1); const kn = cl(round(k[2]), 0, 64); if (S.first) { - // The first cycle starts 60% of the way round, so the picture is mostly - // built when the panel opens. + // The panel opens with the scan done and the reading P0 of the way + // through, so the picture is built by 1.5 s and the first clearing + // falls at 3 s. S.knobN = kn; startCycle(S, S.E); - S.prog = 0.6; + S.scan = 1, S.prog = P0; + measure(S); take(S); S.first = false; } else if (kn !== S.knobN) { @@ -220,7 +250,10 @@ export function update(dt, input, params) { if (anglesFor(S) !== S.N) rebuild(S); } const rate = S.speed / 2; - S.fade = min(1, S.fade + dt / 0.6); + if (S.scan < 1) { + S.scan = min(1, S.scan + (dt * rate) / SCAN); + measure(S); + } if (S.prog < 1) { S.prog = min(1, S.prog + (dt * rate) / SWEEP); take(S); @@ -228,6 +261,9 @@ export function update(dt, input, params) { S.hold += dt * rate; if (S.hold >= HOLD) startCycle(S, nextPhantom(S.E)); } + // Near the end of the hold both squares fade out over FADE seconds and + // stay dark for the last 0.07 s. + S.clear = S.prog < 1 ? 1 : cl((HOLD - S.hold - 0.07) / FADE, 0, 1); } export function draw(display, params) { @@ -235,24 +271,32 @@ export function draw(display, params) { if (!S || S.first) return; const c = S.c, f = S.filt, n = max(1, S.done), A = (1 - f) * FLAT, B = f; const shown = S.shown; - // The reconstruction, the mean of the smeared projections so far, and - // its gain eased toward its peak so a new streak does not flash the scale. - let peak = 1e-6; + // The reconstruction is the sum of the smeared projections so far over + // the full count, so it builds up from black. The gain follows the peak + // of their mean, eased so a new streak does not flash the scale. + let top = 1e-6; for (let i = 0; i < Q * Q; i++) { const v = (A * S.P[i] + B * S.R[i]) / n; shown[i] = v; - if (v > peak) peak = v; + if (v > top) top = v; } + // The scale is set by the brightest 0.5% of LEDs, so a few noise spikes + // do not darken the whole picture. + const hist = S.hist.fill(0); + for (let i = 0; i < Q * Q; i++) if (shown[i] > 0) hist[min(255, floor((shown[i] / top) * 256))]++; + let b = 255; + for (let cnt = 0; b > 0 && (cnt += hist[b]) < Q * Q * 0.005; ) b--; + const peak = (top * (b + 1)) / 256; S.gain = S.gain > 0 ? S.gain + (1 / peak - S.gain) * 0.25 : 1 / peak; - const g = S.gain; + const g = S.gain, env = S.clear; // The newest projection flashes along its rays for a moment. - const fl = exp(-(S.t - S.lastT) / 0.25) * 0.5, cth = cos(S.lastTh), sth = sin(S.lastTh); + const fl = exp(-(S.t - S.lastT) / 0.25) * 0.5 * S.clear, cth = cos(S.lastTh), sth = sin(S.lastTh); const hr = 0.55 + S.hue, hn = 0.78 + S.hue; for (let y = 0; y < Q; y++) { const u0 = -C * cth + (C - y) * sth + MID; for (let x = 0; x < Q; x++) { const i = y * Q + x; - let u = shown[i] * g; + let u = shown[i] * g * env; if (fl > 0.01) { const s = u0 + x * cth, j = floor(s); if (j >= 0 && j < NB - 1) { @@ -260,37 +304,38 @@ export function draw(display, params) { u += fl * (A * pv + B * qv) * g; } } - if (S.fade < 1) u = S.fadeFrom[i] + (u - S.fadeFrom[i]) * S.fade; shown[i] = u; if (u >= 0) { - const v = min(1, u); - hsv(hr, 0.85 - 0.75 * v * v, 0.06 + 0.94 * v, c); + // A soft shoulder above 0.7 instead of a hard clip at white. + const v = u < 0.7 ? u : 1 - 0.3 * exp(-(u - 0.7) / 0.3); + hsv(hr - 0.05 + 0.1 * v, 0.85 - 0.75 * v * v, 0.06 + 0.94 * v ** 0.65, c); } else hsv(hn, 0.65, min(0.5, -u * 0.9), c); display.setPixel(x, y, 255 * c[0], 255 * c[1], 255 * c[2]); } } // The sinogram, angle 0 at the top to 180 degrees at the bottom. Rows the // beam has passed show the full sinogram dimly and the rows of the - // projections taken brightly; rows ahead still hold the last phantom's. - const swept = S.prog >= 1 ? Q : floor(S.prog * Q + 1e-6) + 1, hs = 0.72 + S.hue; - for (let r = 0; r < Q; r++) { - const tk = S.taken[r] >= 0 && r < swept; - const lv = tk ? (S.depth[r] ? 0.75 : 1) : r < swept ? 0.5 : 0.3; - const src = tk ? S.rows[r] : r < swept ? S.sino.subarray(r * Q, r * Q + Q) : S.prevSino.subarray(r * Q, r * Q + Q); - const pk = r < swept ? S.peak : S.prevPeak; + // projections measured brightly, brightest once they are smeared back; + // rows ahead are dark. A row flashes white as it is smeared back. + const swept = S.scan >= 1 ? Q : floor(S.scan * Q + 1e-6) + 1, hs = 0.72 + S.hue, pk = S.peak; + for (let r = 0; r < swept; r++) { + const tk = S.taken[r] >= 0; + const lv = (tk ? (S.taken[r] < S.done ? 1 : 0.7) * (S.depth[r] ? 0.8 : 1) : 0.55) * S.clear; + const src = tk ? S.rows[r] : S.sino.subarray(r * Q, r * Q + Q); const hot = tk && S.taken[r] === S.done - 1 ? exp(-(S.t - S.lastT) / 0.3) : 0; for (let x = 0; x < Q; x++) { - const v = cl(src[x] / pk, 0, 1); - hsv(hs, max(0, 0.8 - 0.55 * v * v - 0.6 * hot), lv * (0.05 + 0.95 * v) * (1 - hot) + hot * (0.3 + 0.7 * v), c); + // An empty bin reads 0 give or take its noise, on a faint floor. + const v = cl(0.12 + 0.88 * (src[x] / pk), 0, 1), e = v ** 0.65; + hsv(hs - 0.04 + 0.12 * v, max(0, 0.8 - 0.55 * v * v - 0.6 * hot), lv * e * (1 - hot) + hot * (0.3 + 0.7 * v), c); display.setPixel(x, Q + r, 255 * c[0], 255 * c[1], 255 * c[2]); } } - // The beam's place in the sweep, a tick at each side of the sinogram, and - // its direction across the reconstruction as a faint line. - if (S.prog < 1) { - const r = min(Q - 1, floor(S.prog * Q)); + // While the beam turns: its place, a tick at each side of the sinogram, + // and its direction across the top square as a faint line. + if (S.scan < 1) { + const r = min(Q - 1, floor(S.scan * Q)); for (const x of [0, 1, Q - 2, Q - 1]) display.setPixel(x, Q + r, 255, 255, 255); - const th = S.prog * PI, dx = -sin(th), dy = -cos(th); + const th = S.scan * PI, dx = -sin(th), dy = -cos(th); for (let s = -44; s <= 44; s += 0.5) { const x = round(C + s * dx), y = round(C + s * dy); if (x < 0 || x >= Q || y < 0 || y >= Q) continue; From 91f40c29c863d6da2df4f80bf779f9244351f527 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:31:38 +0000 Subject: [PATCH 15/19] incoming: Radon Transform dims to 15% between scans instead of black, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- incoming/pattern-study/patterns/radon-transform.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/incoming/pattern-study/patterns/radon-transform.js b/incoming/pattern-study/patterns/radon-transform.js index 0b98eff00..58b516d50 100644 --- a/incoming/pattern-study/patterns/radon-transform.js +++ b/incoming/pattern-study/patterns/radon-transform.js @@ -162,7 +162,7 @@ const TURNS = [4, 8, 16, 32, 64]; // At Speed 2 the beam's half turn takes SCAN seconds, reading the sinogram // back into the picture takes SWEEP, the result holds HOLD and the last // FADE of the hold clears both squares. -const SCAN = 1, SWEEP = 6, HOLD = 1.5, FADE = 0.25, P0 = 0.75; +const SCAN = 1, SWEEP = 6, HOLD = 1.5, FADE = 0.25, DIM = 0.15, P0 = 0.75; // A new scan starts on a cleared panel. function startCycle(S, E) { @@ -261,9 +261,9 @@ export function update(dt, input, params) { S.hold += dt * rate; if (S.hold >= HOLD) startCycle(S, nextPhantom(S.E)); } - // Near the end of the hold both squares fade out over FADE seconds and - // stay dark for the last 0.07 s. - S.clear = S.prog < 1 ? 1 : cl((HOLD - S.hold - 0.07) / FADE, 0, 1); + // Near the end of the hold both squares dim over FADE seconds to DIM of + // their light and stay there for the last 0.07 s. + S.clear = S.prog < 1 ? 1 : DIM + (1 - DIM) * cl((HOLD - S.hold - 0.07) / FADE, 0, 1); } export function draw(display, params) { From 9235c5c132d9c6990a1907938136754d30f5b5eb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:31:51 +0000 Subject: [PATCH 16/19] incoming: Radon Transform knob note says how the hold dims, pattern round 67 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- incoming/pattern-study/patterns/radon-transform.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incoming/pattern-study/patterns/radon-transform.js b/incoming/pattern-study/patterns/radon-transform.js index 58b516d50..7b2065c33 100644 --- a/incoming/pattern-study/patterns/radon-transform.js +++ b/incoming/pattern-study/patterns/radon-transform.js @@ -12,7 +12,7 @@ // @knobs Hue=0..1, Speed=0.2..3, Angles=0..64, Filter=0..1 // // Knob 1: Hue. Turns both squares round the wheel: at 0 the reconstruction runs from dark cyan to ice white and the sinogram from blue violet to purple. -// Knob 2: Speed. At 2 the beam's half turn takes 1 s, reading the projections back takes 6 s, and the picture holds 1.5 s, fading out over its last 0.3 s; at 0.2 everything takes ten times as long, at 3 two thirds as long. +// Knob 2: Speed. At 2 the beam's half turn takes 1 s, reading the projections back takes 6 s, and the picture holds 1.5 s, dimming to 15% over its last 0.3 s before the next scan starts on an empty sinogram; at 0.2 everything takes ten times as long, at 3 two thirds as long. // Knob 3: Angles. How many projections a half turn takes. 0 takes turns, doubling each cycle from 16: 16, 32, 64, then 4, 8 and round again. 1 to 64 hold that count; with few the shapes are crossed by the star of streaks, one per angle. The half turn has a fixed dose of photons shared by its projections: at 4 angles each bin gets 2,000 and the rows are clean, at 64 it gets 125 and they are grainy. // Knob 4: Filter. The filter each projection passes before it is smeared back: (1 - f) c + f |w|, with c the ramp's height at 1/400 of a cycle a sample. At 0 it is plain back-projection, every shape in a blurred 1/r halo; at 0.06 the edges already come through; at 1 it is the ramp alone, sharp, with dark lobes beside every streak. Plain back-projection smooths the photon noise away; the ramp turns it into fine grain. From 51a7e37f29f8e87bf94fd13624a104527d7e6fc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 13:33:24 +0000 Subject: [PATCH 17/19] =?UTF-8?q?incoming:=2067=ED=8C=90=20=ED=91=9C?= =?UTF-8?q?=EC=99=80=20=EC=A0=88,=20=EB=8B=A4=EC=9D=8C=20=ED=8C=90=20?= =?UTF-8?q?=EA=B0=90=EC=9D=84=20README=20=EC=97=90=20=EC=A0=81=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- incoming/pattern-study/README.md | 105 +++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/incoming/pattern-study/README.md b/incoming/pattern-study/README.md index 8612e5135..cdda6666f 100644 --- a/incoming/pattern-study/README.md +++ b/incoming/pattern-study/README.md @@ -1,6 +1,6 @@ # 패턴 연구 2026-10 -새 패턴 558개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [index.html](index.html) 을 브라우저로 열면 전부 한 장에서 돌아가요. 맨 위 지도에서 점을 누르면 그 패턴으로 가고, 카드의 슬라이더가 노브 넷이에요. 마음에 드는 것은 「코드 복사」로 가져가 Pattern Lab 의 CODE 칸에 붙여 넣으면 같은 그림이 나와요. 페이지는 파일 하나라서 바깥에서 받아 오는 것이 없어요. +새 패턴 568개를 쉰여섯 갈래로 나눠 만들었어요. 1판에서 열세 갈래로 23개를 만들고, 2판에서 1판에 없던 아홉 갈래로 16개를, 3판에서 하나를, 4판에서 새 갈래인 직물로 하나를 더했어요. 5판은 새 패턴 없이 1판의 Euclid Pulse 를 다시 짰고, 6판은 세운 패널의 128줄을 위아래로 다 쓰는 패턴 하나를 더했어요. 7판은 또렷한 모양이 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 8판은 새 갈래인 기계로 하나를 더했어요. 9판은 새 패턴 없이 1판의 Sprout 을 다시 짰어요. 10판은 세운 패널을 떨어지는 블록의 우물로 쓰는 패턴 하나를 더했어요. 11판은 여러 색이 꽤 멀리 흘러가는 지도 칸에 패턴 하나를 더했어요. 12판은 새 갈래인 천체로 하나를 더했어요. 13판은 새 패턴 없이 1판의 Glyph Rain 을 다시 짰어요. 14판은 절차가 바뀌기 전에 띄운 바퀴라서, 세운 패널의 높이를 다 떨어지는 폭포 하나를 더했어요. 15판은 바뀐 절차의 첫 판이라 열 갈래에 새 패턴을 하나씩, 모두 열 개를 더했어요. 도시와 음악 갈래는 그때 새로 열었어요. 16판도 열 갈래에 하나씩 열 개를 더했고, 바다와 곤충 갈래를 새로 열었어요. 17판도 열 갈래에 하나씩 열 개를 더했고, 전자 회로와 장난감 갈래를 새로 열었어요. 18판도 열 갈래에 하나씩 열 개를 더했고, 유리와 광물·결정, 놀이공원 갈래를 새로 열었어요. 19판도 열 갈래에 하나씩 열 개를 더했고, 부엌과 버섯·균사 갈래를 새로 열었어요. 20판도 열 갈래에 하나씩 열 개를 더했고, 새와 화학·실험 갈래를 새로 열었어요. 21판도 열 갈래에 하나씩 열 개를 더했고, 종이와 운동·경기 갈래를 새로 열었어요. 22판도 열 갈래에 하나씩 열 개를 더했고, 몸과 비행 갈래를 새로 열었어요. 23판도 열 갈래에 하나씩 열 개를 더했고, 무대·공연과 짐승 갈래를 새로 열었어요. 24판도 열 갈래에 하나씩 열 개를 더했고, 공사·건설과 도자기 갈래를 새로 열었어요. 25판도 열 갈래에 하나씩 열 개를 더했고, 농사·밭과 화산 갈래를 새로 열었어요. 26판도 열 갈래에 하나씩 열 개를 더했고, 얼음·극지와 미생물 갈래를 새로 열었어요. 27판도 열 갈래에 하나씩 열 개를 더했고, 철도와 필름·영화 갈래를 새로 열었어요. 28판은 주제 표가 수학 쪽으로 바뀐 첫 판이라 이름 있는 수학 대상 열 개를 열 갈래에 하나씩 더했고, 수론 갈래를 새로 열었어요. 29판은 주제 표가 수학 여섯과 화장품 팝업 넷으로 나뉜 첫 판이라, 이름 있는 수학 대상 여섯과 화장품 매장 벽에 틀 질감 넷을 더했고, 위상·매듭과 화장품 팝업 갈래를 새로 열었어요. 30판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩 더하고, 팝업 매장의 LED 벽에 틀 질감 넷을 화장품 팝업 갈래에 더했어요. 31판도 같은 표로 돌아서 이름 있는 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했고, 새 갈래는 열지 않았어요. 32판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽의 질감 넷을 화장품 팝업 갈래에 더했어요. 33판도 같은 표로 돌아서 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 34판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 35판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 36판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 37판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 38판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 39판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 40판도 수학 대상 여섯을 여섯 갈래에 하나씩, 매장 벽 질감 넷을 화장품 팝업 갈래에 더했어요. 41판은 주제 표가 수학·추상 다섯과 할로윈 다섯으로 바뀐 첫 판이라, 수학 대상 다섯을 다섯 갈래에 하나씩 더하고 할로윈 갈래를 새로 열어 10월 31일에 틀 패턴 다섯을 넣었어요. 42판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 41판이 연 할로윈 갈래에 더했어요. 43판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 44판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩 더하고, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 45판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 열 개 아래인 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 46판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 47판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 48판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 49판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 50판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 51판도 같은 표로 돌아서 수학 대상 다섯을 다섯 갈래에 하나씩, 할로윈 밤에 틀 그림 다섯을 할로윈 갈래에 더했어요. 52판도 같은 표로 돌아서 수학 대상 다섯을 패턴이 적은 다섯 갈래에 하나씩, 할로윈 그림 다섯을 할로윈 갈래에 더했어요. 53판은 할로윈 몫이 쉬게 된 첫 판이라, 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 54판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 55판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 56판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 57판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 58판도 수학 대상 열 개를 패턴이 적은 열 갈래에 하나씩 더했어요. 59판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 60판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 61판도 수학 대상 열 개를 패턴이 적은 갈래부터 열 갈래에 하나씩 더했어요. 62판도 수학 대상 열 개를 열 갈래에 하나씩 더했어요. 63판은 64판과 같이 주제 표에 「타이포」가 들어온 첫 판이라, 글자가 들어간 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 64판은 주제 표가 바뀌어 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 네 갈래에 더했어요. 66판도 같은 표로 돌아서 글자 패턴 넷을 글자 갈래에, 하나를 데이터 그림 갈래에 더하고, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. 67판도 같은 표로 돌아서 글자 패턴 다섯을 글자 갈래에, 수학 대상 다섯을 다섯 갈래에 하나씩 더했어요. [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판에서 세로로 다시 짰어요. @@ -885,6 +885,21 @@ | [Hodograph](patterns/hodograph.js) | 물리·스프링 | 해 하나를 이심률이 다른 천체 셋이 케플러의 박자로 돌며 같은 시간 부채를 쓸고, 아래에서는 한 원점에서 그은 속도 화살의 끝이 원점에서 반지름의 e 배만큼 비켜 선 원을 따라 돌아요. | | [Shapley-Folkman](patterns/shapley-folkman.js) | 기하·대칭 | 모양 하나를 1, 2, 4, 8 개 더해 그 수로 나눈 민코프스키 평균이 띠 넷으로 쌓이고, 오목한 만이 띠마다 절반씩 메워지며 흐린 볼록 껍질에 다가가요. | +### 67판 + +| 패턴 | 갈래 | 한 줄 | +| --- | --- | --- | +| [Shannon's Approximations](patterns/shannon-approximations.js) | 글자 | 커서가 『오만과 편견』 첫머리로 센 섀넌의 근사 영어를 16자 21줄 쪽에 덮어 치고, 책에 있는 낱말은 청록으로, 책에서 나란히 선 낱말 쌍은 금빛으로 켜져서 Order 를 올릴수록 흐린 잡음이 낱말로 바뀌어요. | +| [Silencio](patterns/silencio.js) | 글자 | 곰링어의 silencio 열네 낱말이 책등처럼 돌아누워 다섯 단 세 줄로 서고, 가운데 빈 자리가 옆 칸으로 옮겨 가면 그 칸의 낱말은 끝 글자부터 잦아들고 떠난 칸의 낱말은 첫 글자부터 다시 켜져요. | +| [Distance Field](patterns/distance-field.js) | 글자 | W, S, K, 3, 빛, ? 가 성긴 거리장 격자에서 쌍선형으로 되살아나 테두리와 바깥 빛을 두르고, 텍셀이 커질수록 모서리가 둥글어지고 가는 획이 사라지며 다음 글자로 녹아들어요. | +| [Braille Relief](patterns/braille-relief.js) | 글자 | 루이 브라유의 여섯 점 글자로 헬렌 켈러의 우물가 두 문장을 네 쪽에 새기고, 3~20도로 낮게 도는 램프가 점마다 밝은 쪽과 긴 그림자를 그리는 동안 손끝이 한 칸씩 읽으면 그 칸 밑에 활자가 작게 떠올랐다 사라져요. | +| [Hanging Line](patterns/hanging-line.js) | 글자 | 김소월 「진달래꽃」 94 음절이 안상수체를 본떠 직선과 원으로 그린 자모로 한 자모씩 쳐지며 윗줄에 매달려 아랫단이 들쭉날쭉하고, Frame 노브를 올리면 음절마다 같은 13×13 네모 칸에 늘려 담겨요. | +| [Radon Transform](patterns/radon-transform.js) | 알고리즘 | 빛줄기가 1초에 반 바퀴를 돌며 센 광자로 아래 칸에 거친 사이노그램을 채우고, 램프 필터로 거른 투영이 위 칸에 제 각도로 번져 쌓이면 별 모양 줄무늬 속에서 타원이 떠올랐다가 두 칸이 함께 어두워져요. | +| [Artin Braids](patterns/artin-braids.js) | 위상·매듭 | 위 띠의 점 셋에서 일곱이 생성원마다 이웃과 반 바퀴 돌아 자리를 바꾸고, 그 자취가 아래로 흘러 앞으로 지난 가닥은 이어지고 뒤로 지난 가닥은 끊긴 브레이드가 돼요. | +| [Walsh Functions](patterns/walsh-functions.js) | 물결·간섭 | 위에는 ±1 로만 된 2차원 Walsh 함수 64장의 표가 서고, 아래 추상 그림은 그 계수를 지그재그로 하나씩 더하며 8×8 블록에서 또렷해지는 동안 더해지는 타일이 위에서 빛나요. | +| [Blinking Vortex](patterns/blinking-vortex.js) | 시뮬레이션 | 아레프의 깜박이는 소용돌이 둘이 위아래로 60 LED 떨어져 반 주기씩 번갈아 돌고, 청록과 보라의 부드러운 띠가 늘고 접혀 LED 보다 가늘어지면 한 빛깔로 섞이다가 새 물감이 깔려요. | +| [Dupin Cyclide](patterns/dupin-cyclide.js) | 기하·대칭 | 위에서 도는 원환면을 아래의 구면 반전으로 뒤집은 뒤팽 사이클라이드가 같이 돌고, 원환면의 원 28 개가 상에서도 모두 원으로 남아 청록과 장미색 곡률선 두 무리를 이뤄요. | + ## 1판 갈래마다 노브 넷의 자리는 갈래가 달라도 거의 같아요. K1 은 색이고 K2 는 빠르기예요. K3 은 모양을 바꾸고, K4 는 그 갈래만의 손잡이예요. 그래서 처음 보는 패턴도 K1 과 K2 는 손이 먼저 알아요. @@ -3503,6 +3518,70 @@ Lab 에서 해 보기: Text 레이어에 「KS」를 Silkscreen 으로 카운터 주제 5 「수학 · 추상」으로, 볼록하지 않은 모양을 여러 번 더해 평균하면 볼록해진다는 것을 기하 갈래에 세웠어요. 섀플리-포크먼 보조정리는 로이드 섀플리와 존 포크먼이 증명했고, 로스 스타가 1969년 『Econometrica』 37권 1호에 낸 「Quasi-equilibria in markets with non-convex preferences」의 부록에 처음 실렸어요. 평면에서 n 개 집합의 합의 볼록 껍질에 든 점은 집합마다 점 하나씩 n 개의 합으로 쓸 수 있고, 그 가운데 집합 밖 껍질에서 빌려 오는 점은 많아야 둘이에요. 그래서 모양 S 를 n 번 더해 n 으로 나눈 평균 A(n) 은 껍질의 어느 점에서도 2D/n 안에 와요. D 는 n=1 일 때 껍질이 S 에서 가장 멀리 떨어진 거리예요. 패널은 위에서부터 32줄짜리 띠 넷에 A(1), A(2), A(4), A(8) 을 같은 크기로 그려요. 모양은 59×28 격자에서 LED 하나가 격자점 하나이고, 합은 격자점 집합을 비트맵으로 정확히 더해요. 8 번 더하면 465×217 격자가 돼요. S 를 A 의 모든 점으로 옮겨 OR 하는 대신 A 의 가장자리 점으로만 옮기고 S 의 조각마다 A 를 한 벌 더하는데, 모든 점으로 옮겨 OR 한 결과와 노브 자리 9 곳에서 40 프레임씩 2,520 번 맞춰 봐서 한 비트도 다르지 않았어요. LED 밝기는 그 칸에 든 A(n) 격자점의 몫이에요. 한 점에 닿는 길의 수로 밝히면 중심 극한 정리의 종 모양이 되니, 그건 이 보조정리와 다른 이야기라서 쓰지 않았어요. 첫 판은 띠마다 한 색이고 이번 띠에서 새로 찬 곳만 옅게 칠해서 띠 넷이 따로 노는 그림 넷으로 읽혔고, 한 프레임이 2.9 ms 였어요. A(1)⊂A(2)⊂A(4)⊂A(8) 로 포개지니 점마다 처음 나타난 띠의 색을 주자, 맨 아래 띠가 청록 달과 하늘색, 남색, 보라 테를 나이테처럼 다 품게 됐어요. S 의 행을 0~31 비트 옮긴 꼴로 미리 두고 껍질 변을 미리 셈해서 2.1 ms 가 됐어요. Hull 노브의 금빛 메움은 0.5 에서 갈색으로 보여서 빗금으로 바꿨어요. 달을 위상 12 곳에서 재면 껍질 가운데 덮이지 않은 몫이 n=1, 2, 4, 8 에서 42.6, 19.6, 8.6, 3.7% 이고, 하우스도르프 거리는 11.7, 5.7, 2.8, 1.37 LED 예요. n 을 곱하면 11.7, 11.5, 11.4, 10.9 로 거의 그대로라 거리는 1/n 으로 줄어요. 넓이는 n 을 곱한 값이 0.43 에서 0.30 으로 내려가 1/n 보다 조금 빨리 줄어요. 별은 n=1 부터 16 까지 n 을 곱한 거리가 5.78 로 똑같아요. 모양 다섯을 위상 12 곳에서 n=1 부터 16 까지 잰 960 경우가 모두 2D/n 안에 들었어요. 잰 값은 움직임 0.214, 촘촘함 0.051, 가장자리 0.502, 복잡도 0.359, 채움 0.429, 색 0.347, 모양 바뀜 0.163 이고, 노브는 0.18, 0.15, 0.20, 0.07 이에요. 새로 본 것은 1/n 이 흐려지는 모습이 아니라 홈의 수로 보인다는 거예요. 달의 현을 따라 A(n) 의 빈 자리가 n 개의 홈으로 갈라지고(위상 여섯 곳 가운데 넷에서 1, 2, 4, 8 개 그대로, 나머지 둘에서는 하나가 더 잡혔어요), 가장 깊은 홈이 1/n 로 얕아져요. +## 67판 + +67판은 65판(#328)과 같이 돌았어요. 앞 바퀴 PR 67개에는 댓글도 리뷰도 하나 없어서 따를 글이 없었어요. 65판이 PR 본문에 먼저 밝힌 열 개와 지도 칸 둘을 읽고 겹치지 않게 골랐어요. 타이포 다섯은 「타이포 실험」의 출발점을 하나씩 맡았어요. Shannon's Approximations 는 노브와 글, Silencio 는 글자로 짠 무늬, Distance Field 는 글자의 몸, Braille Relief 는 글자와 빛, Hanging Line 은 글자가 만들어지는 과정이에요. 글자 꼴도 다섯이 다 달라요. Shannon's Approximations 는 3×5 대문자 26자를 새로 그렸고, Silencio 는 책등처럼 돌려 세운 9줄 높이 소문자 열 자와 7×11 한글 음절 넷을 그렸어요. Distance Field 는 다각형 24개로 적은 여섯 자의 정확한 거리장을 쓰고, Braille Relief 는 여섯 점 칸과 그 밑에 뜨는 3×5 활자를, Hanging Line 은 7 LED 격자 위에 직선과 원으로 그은 자모 27 꼴을 써요. 66판이 「다음 판 감」에 적은 크리스 그린의 거리장과 안상수체를 여기서 해 봤어요. 수학 다섯은 558개에 없던 대상이고, 「낯선 수학 예」는 31판까지 다 써서 거기서는 고르지 않았어요. 알고리즘, 위상·매듭, 물결·간섭, 시뮬레이션, 기하·대칭에 하나씩 넣었고, Radon Transform 은 60판이, Blinking Vortex 는 61판이 「다음 판 감」에 적어 둔 것이에요. 주제 1 은 셋뿐인 칸 열하나 가운데 65판이 고른 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 빼고 복잡도 × 모양 바뀜 4,4 와 가장자리 × 움직임 1,4 를 겨눴어요. 주제 4 는 Radon Transform 과 Artin Braids 예요. + +### Shannon's Approximations + +주제 7 의 「노브와 글」로, Order 노브가 글의 통계 차수를 한 칸씩 올려요. 클로드 섀넌(Claude Shannon)은 「A Mathematical Theory of Communication」(1948) 3절에서 27 기호를 같은 확률로 늘어놓은 0차부터, 책을 아무 데나 펼쳐 앞 글자 뒤에 오는 글자를 옮겨 적는 식으로 차수를 올려 낱말 쌍을 따르는 5단계까지 영어의 근사를 보였어요. 코드는 제인 오스틴의 『오만과 편견』(1813) 첫 장 앞부분을 대문자와 빈칸만 남긴 2,812 기호로 싣고, 같은 방법으로 뽑아요. 패널은 직접 그린 3×5 대문자로 16자 21줄 쪽을 늘 채워 두고, 커서가 Speed 2 에서 1초에 16자씩 위에서부터 덮어 쳐요. 빈칸 사이 글자 묶음이 책의 낱말 236개 가운데 하나면 청록으로 켜지고, 책에서 나란히 선 두 낱말은 금빛이 돼요. 차수마다 2분씩 돌려 세니 낱말에 든 글자의 몫이 0차부터 0.1%, 1.6%, 6.3%, 27% 였고 4차와 5차는 98% 였어요. 맨 아래 한 줄이 그 몫을 막대로 그려요. 4차와 5차는 금빛의 몫으로만 갈려서 4차는 낱말 글자의 5%, 5차는 100% 가 금빛인데, 3차가 11% 로 4차보다 많은 것은 OF THE 같은 짧은 쌍이 세 글자 통계로도 저절로 나와서예요. 첫 판은 1초에 12자를 치고 새 글자가 0.5초 빛나서 Speed 반응이 0.020 이었는데, 16자와 1초로 바꾸자 0.030 이 됐어요. 잰 값은 움직임 0.034, 촘촘함 0.185, 가장자리 1.000, 복잡도 0.276, 채움 0.378, 색 0.099, 모양 바뀜 0.024 이고, 노브는 0.06, 0.03, 0.25, 0.09 예요. Blackout 을 1 로 올리면 낱말이 아닌 칸이 어두운 먹 막대로 덮여서, 3차 쪽은 JUST 와 GIRLS 같은 낱말만 떠 있는 지운 시가 돼요. + +Lab 에서 해 보기: Text 레이어를 mask 로 두고 Inter 로 IT 와 IS 를 두 줄로, 크기 52 에 sharp 로 쓰면 소설의 첫 두 낱말 안에서만 쪽이 보여서, Order 를 올릴수록 큰 글자의 속이 청회색 잡음에서 청록과 금빛 낱말로 차요. + +기능 감: 패턴 코드가 픽셀 글꼴을 비트맵으로 받아 쓰는 길이요. Text 레이어에는 Silkscreen 이 있고 펌웨어에는 Matrix Fonts 가 있는데 코드 레이어는 둘 다 못 써서, 대문자 26자를 15자리 문자열로 손수 그렸어요. + +### Silencio + +주제 7 「타이포」로, 글자 사이의 빈 자리가 주인공인 그림을 겨눴어요. 오이겐 곰링어(Eugen Gomringer)의 「silencio」(1953)는 silencio 를 한 줄에 셋씩 다섯 줄로 짜고 셋째 줄 가운데만 비운 구체시예요. 3×5 글꼴로도 silencio 는 31 LED 라 64 LED 폭에 셋이 서지 않아서, 라틴 낱말은 책등처럼 시계 방향으로 돌려 다섯 단 세 줄로 세웠고, 시의 첫 줄이 오른쪽 단이 되어 세로쓰기 한국어 책처럼 읽혀요. 라틴 글꼴은 세 낱말에 드는 소문자 열 자만 9줄 높이로 그렸고, 침묵과 고요는 7×11 음절을 손으로 그려 곰링어의 세 칸 다섯 줄 그대로 세웠어요. silencio 와 침묵은 마침 둘 다 한 낱말이 60 LED 예요. 빈 자리는 3.6초마다 옆 칸으로 옮겨 가는데, 들어선 칸의 낱말은 끝 글자부터 잦아들어 silen, si 를 거쳐 꺼지고 떠난 칸의 낱말은 첫 글자부터 다시 켜져요. 한글은 이 일을 자모 단위로 해서 침묵이 침ㅁ, 치를 거쳐 사라져요. 줄마다 읽는 빛이 낱말 하나를 1초에 지나며 1.1초 동안 잦아드는 잔광을 남기고, 빈 자리에서는 아무것도 켜지 않은 채 1.6초를 건너가요. Hush 를 올리면 침묵이 거리 고리마다 이웃으로 번져서 1 에서는 열다섯 칸 가운데 아홉이 비어요. 첫 판은 잔광을 글자 2.2 개 길이로 정해서 Speed 반응이 0.02 였는데, 잔광을 실제 시간 1.1초로 바꾸자 빨리 읽을수록 낱말 전체가 남아 0.033 이 됐어요. 잰 값은 움직임 0.035, 촘촘함 0.073, 가장자리 0.512, 복잡도 0.303, 채움 0.284, 색 0.095, 모양 바뀜 0.034 이고, thirds 는 22/29/32, 노브는 0.05, 0.03, 0.12, 0.05 예요. 새로 본 것은 빈 자리의 크기예요. 빠진 것은 7×26 LED 낱말 하나인데, 위아래 낱말과 양옆 단 사이 틈까지 합쳐 16×58 LED, 928개가 0 그대로 남아서 패널에서 가장 큰 어둠이 돼요. + +Lab 에서 해 보기: Word 를 3 에 두고 Text 레이어를 mask 로 바꿔 maskInvert 를 켠 뒤 Sans-serif 로 「침묵」을 size 28, sharp 로 가운데에 쓰면, 작은 침묵 마당에 큰 침묵 꼴의 빈 자리가 하나 더 뚫리고 옮겨 다니는 빈 자리가 거기 닿으면 두 침묵이 한 덩어리가 돼요. + +기능 감: Text 레이어의 글줄을 90° 돌리는 칸이요. silencio 는 3×5 글꼴로도 31 LED 라 셋이 서지 않아서 책등처럼 돌린 글자를 코드에 손으로 그렸는데, Text 레이어는 가로로만 써서 같은 판을 레이어로는 짤 수 없어요. + +### Distance Field + +주제 7 「타이포」의 「글자의 몸」으로, 글자 하나를 거리 값의 성긴 격자로만 들고 다니는 그림을 글자 갈래에 세웠어요. 크리스 그린(Chris Green)이 2007년 SIGGRAPH 에 낸 Valve 의 「Improved Alpha-Tested Magnification for Vector Textures and Special Effects」는 글자를 작은 텍스처에 부호 있는 거리로 담아 쌍선형으로 키운 뒤 0.5 에서 잘라 크기와 상관없이 날 선 가장자리를 얻었고, 같은 거리장을 다른 값에서 잘라 테두리와 바깥 빛도 냈어요. 패널에는 56×116 LED 쯤 되는 글자 하나가 서고, W, S, K, 3, 빛, ? 여섯이 곧은 꼴과 둥근 꼴을 번갈아 2초씩 선 뒤 두 거리장을 0.5초 동안 섞어 다음 글자로 녹아들어요. 글자 꼴은 겹쳐도 되는 다각형 24개로 적고, 변을 다른 다각형과 만나는 자리마다 잘라 한쪽만 글자 안인 조각만 남겨서 획이 만나는 자리 안쪽까지 정확한 거리를 셈해요. 다각형마다 잰 거리의 최솟값을 쓰면 W 안에서 6.8 LED 까지 틀렸고, 정확한 값은 1/8 LED 격자로 따로 잰 거리와 다섯 글자에서 0.13 LED 안으로 맞았어요(W 는 그 격자가 놓친 좁은 카운터 끝에서 0.73). 이 값을 텍셀 중심에서만 뽑아 두고, LED 마다 쌍선형으로 되살려 잘라요. 처음 여는 6 LED 텍셀은 12×23 = 276 개 수로 글자를 되살려 원래 꼴과 겹치는 몫(IoU)이 0.92~0.98 이에요. 21 LED 까지 키우면 32 개 수만 남아 0.05~0.32 로 떨어지고, 빛의 ㅊ 꼭지는 16 LED 에서, ? 의 점은 21 LED 에서 사라져요. 텍셀 중심은 흐린 점으로 찍혀서 글자 안에서는 어두운 구멍으로 보여요. 첫 판은 둥근 글자 하나의 거리장을 2 LED 텍셀에서 셈하는 데 37 ms 가 걸려 글자가 바뀌는 프레임이 57 ms 까지 늘었는데, 다각형을 평평한 배열로 펴고 외접 상자로 거르자 4.4 ms 와 14 ms 가 됐어요. 잰 값은 움직임 0.469 에 채움 0.689 이고 thirds 는 71/64/71 이며, 노브 반응은 Magnify 가 0.21 로 가장 약하고 Weight 가 0.41 로 가장 세요. + +Lab 에서 해 보기: Text 레이어에 Inter 로 ? 한 자를 size 128, sharp, 흰색으로 가운데에 두고 blend 를 add, opacity 를 0.3 으로 얹어요. sharp 는 덮임 128/255 에서 LED 를 켜는 알파 테스트라 이 패턴의 문턱과 같은 규칙인데, Lab 은 제 크기로 래스터화해서 패턴의 ? 가 설 때 Magnify 를 1.8 까지 올리면 패턴의 점만 사라지고 Lab 의 점은 남아요. + +기능 감: Text 레이어 효과에 거리장으로 내는 테두리와 바깥 빛이 있으면 좋겠어요. 지금 효과에는 글자 둘레를 다루는 것이 없어서 글자 꼴을 코드에 다시 적어야 했고, 여섯 자에 다각형 24개를 손으로 적었어요. Text 레이어는 글이 바뀔 때만 다시 그리니, 그때 덮임 비트맵에서 거리장을 한 번 셈해 두면 돼요. + +### Braille Relief + +주제 7 의 「글자와 빛」으로, 빛이 글자를 그리는 패턴이에요. 루이 브라유(Louis Braille)가 1829년에 펴낸 여섯 점 글자는 a 부터 j 까지 1, 2, 4, 5번 점만 쓰고, k 부터 t 는 거기에 3번을, u, v, x, y, z 는 3번과 6번을 더하고, w 만 2-4-5-6 으로 따로 서요. 코드가 이 규칙으로 지은 26자를 유니코드 점자 블록의 코드와 맞춰 보니 다 같았어요. 글은 헬렌 켈러의 『The Story of My Life』(1903) 우물가 대목 두 문장이고, 대문자 표(6번 점)와 문장 부호는 빼고 1종 점자로 적었어요. 29 낱말 119자가 한 줄 여섯 칸, 한 쪽 일곱 줄로 하이픈(3-6번) 넷과 함께 네 쪽을 채우고, 솟은 점은 354개예요. 점은 4 LED 간격에 반지름 1.75 LED 의 포물면 돔이고, 그림자는 닫힌 식으로 풀려서 램프가 3도일 때 높이의 19배까지 늘어나요. 손끝은 Speed 2 에서 1초에 2.5칸을 읽고, 닿은 칸 밑 7줄 틈에 3×5 활자가 떠올랐다 3초에 걸쳐 사라져요. 첫 판은 종이 결이 지금의 세 배라 낮은 빛에 종이가 점보다 반짝였고, 어두운 크림색이 갈색으로 읽혀 Hue 반응이 0.02 였어요. 결을 3분의 1로 줄이고 어두운 곳은 차가운 회색에서 밝을수록 램프 색으로 넘어가게 하자 노브가 0.05, 0.06, 0.06, 0.05 가 됐어요. 잰 값은 움직임 0.070, 촘촘함 0.056, 가장자리 0.305, 복잡도 1.541, 채움 0.871, 색 0.076, 모양 바뀜 0.071 이고 thirds 는 81/89/89 예요. 새로 본 것은 램프의 높이가 점의 높이보다 크게 읽힌다는 거예요. Relief 0.4 에서도 램프가 4도면 밝기 상위 2% 가 223 인데, 20도로 올리면 103 에 그쳐요. + +Lab 에서 해 보기: Text 레이어에 Newsreader 로 소문자 a 를 크기 120 쯤 가운데에 쓰고 role 을 mask 로 바꾸면 점자 쪽이 큰 활자 a 안에서만 보여요. Light 노브를 0 으로 내리면 긴 그림자가 a 의 테두리에서 잘려서 활자의 윤곽이 빛으로 한 번 더 서요. + +기능 감: 코드 레이어가 위 Text 레이어의 덮임(알파)을 높이 지도로 읽는 길이요. 그러면 이 패턴의 돔과 그림자 식이 사람이 친 낱말을 돋을새김으로 띄우고 같은 램프로 비춰요. 지금은 켈러의 두 문장과 3×5 활자 27자를 다 코드에 적었어요. + +### Hanging Line + +주제 7 의 「글자가 만들어지는 과정」으로, 김소월의 「진달래꽃」(1922)이 탈네모꼴 한글로 한 자모씩 쳐지는 패턴이에요. 안상수체(안상수, 1985)는 자모 67 꼴로 11,172 음절을 모두 짜는 세벌식 글꼴이고, 자모마다 꼴이 하나라 음절이 네모 칸에 맞춰 줄지 않고 윗줄에 매달려 아랫단이 들쭉날쭉해요. 패널의 글꼴은 그걸 본떠 직선과 원으로 새로 그렸고, 시에 드는 닿자 16 꼴과 홀자 11 꼴만 있어요. 닿자 칸은 7×7 LED 이고, 세로 홀자의 기둥은 받침이 없어도 받침 칸 한가운데까지 12 LED 를 내려와요. 그래서 94 음절의 높이가 9 에서 19 LED 까지 일곱 가지로 갈리고, 너비는 7 에서 13 LED 까지 네 가지예요. 자모는 세벌식 차례대로 Speed 2 에서 1초에 4.4 걸음씩 들어와 시 한 벌이 1분에 끝나요. Frame 0 에서는 먼저 친 자모가 끝까지 제자리에 있고, 1 로 올리면 음절마다 같은 13×13 칸에 늘려 담겨 치는 중인 음절의 첫닿자가 자모가 올 때마다 줄어들어요. 처음 그린 ㅎ 은 반지름 1.5 LED 의 원이 속까지 메워진 얼룩으로 보여서, 그 자리 LED 들이 만드는 팔각 고리로 바꿨어요. 잰 값은 움직임 0.688, 촘촘함 0.099, 가장자리 0.951, 복잡도 0.128, 채움 0.131, 색 0.136, 모양 바뀜 0.096 이고, thirds 는 13/16/9, 노브는 0.04, 0.10, 0.12, 0.14 예요. 새로 본 것은 두 꼴이 자리를 아끼는 쪽이 다르다는 거예요. 94 음절의 줄 길이가 탈네모꼴로는 890 LED, 네모 칸으로는 1,222 LED 라서 패널 줄이 26 에서 37 로 늘지만, 줄 간격이 23 에서 17 LED 로 줄어 시 한 벌의 높이는 639 와 660 LED 로 3% 차이예요. + +Lab 에서 해 보기: Text 레이어를 패턴 아래에 두고 sans-serif 로 「꽃」 한 자를 size 100, 색 #2a0a20 쯤의 어두운 자주로 깔면, 브라우저 글꼴의 네모꼴 「꽃」이 배경에 서고 그 위로 탈네모꼴 시가 매달려 지나가요. Frame 노브를 1 로 올리면 앞뒤가 같은 네모꼴이 돼요. + +기능 감: Text 레이어의 타자 효과요. 지금 효과는 다 써 둔 글 한 장을 흘리거나 반짝이게 해서 글이 한꺼번에 서는데, 글자를 하나씩, 한글은 자모 차례로 내보내는 효과가 있으면 어느 글꼴로든 글을 쳐 나가는 화면을 만들 수 있어요. 그 길이 없어 이 패턴은 시 한 편과 그 시에 드는 자모 27 꼴만 코드에 그렸어요. + +### Radon Transform + +주제 5 「수학 · 추상」과 주제 4 를 함께 맡아 위 64×64 칸에 재구성을, 아래 64×64 칸에 사이노그램을 세웠고, 주제 1 의 복잡도 × 모양 바뀜 4,4 칸도 겨눴어요. 요한 라돈(Johann Radon)은 1917년에 평면 위 함수가 모든 직선을 따라 잰 적분으로 다시 정해진다는 것을 보였고, G. N. 라마찬드란(G. N. Ramachandran)과 A. V. 락슈미나라야난(A. V. Lakshminarayanan)은 1971년에 투영을 |ω| 램프 필터로 거른 뒤 되비추는 길을 냈어요. 팬텀은 밀도가 다른 타원 여섯 개라 직선 적분이 닫힌 꼴로 나오고, 수치 적분과 견주니 어긋남이 0.003 아래였어요. Speed 2 에서 빛줄기가 1초에 반 바퀴를 돌며 통과한 광자를 세어 사이노그램을 채우고, 그 뒤 6초 동안 투영을 하나씩 걸러 위 칸에 제 각도로 번지게 해요. 결과를 1.5초 보여 주다가 마지막 0.3초에 두 칸이 빛의 15% 까지 어두워지고, 26도에서 43도 돌고 안쪽 타원 하나가 바뀐 팬텀이 빈 사이노그램에서 다시 시작해요. 한 바퀴의 광자는 검출 칸마다 8,000개라 투영 4개면 하나에 2,000개씩 받아 줄이 깨끗하고, 64개면 125개씩이라 거칠어요. 잡음이 없을 때 반지름 30 안의 평균 오차는 투영 4개에 0.21, 8개에 0.12, 16개에 0.065, 64개에 0.027 로 줄었는데, 같은 광자를 나눠 쓰니 16개부터는 0.19 근처에서 더 내려가지 않아요. 첫 판은 복잡도 1.866, 모양 바뀜 0.167 로 칸에 못 미쳤어요. 빛줄기의 반 바퀴를 6초에서 1초로 줄여 사이노그램이 금방 차게 하고 광자 잡음을 넣자 복잡도 2.317 에 모양 바뀜 0.488 이 됐는데, 0.488 은 모양 바뀜 축의 끝인 Cloth 의 0.413 을 넘어 모든 칸의 경계를 옮겼어요. 지우는 자리를 검정 대신 빛의 15% 로 남기자 모양 바뀜 0.375, 복잡도 2.333 으로 축 안에 들어왔어요. 잰 값은 움직임 0.508, 촘촘함 0.081, 가장자리 0.235, 채움 0.924, 색 0.251 이고, 노브는 0.24, 0.21, 0.09, 0.12 이며, 가장 어두운 아래 3분의 1 도 87% 가 켜져요. 이 패널에서 새로 본 것은 Filter 0 의 단순 역투영이 광자 잡음을 뭉개 매끈한 덩어리를 남기고, 1 의 램프는 같은 잡음을 고운 낟알로 바꿔 저선량 CT 사진처럼 보인다는 것이에요. + +### Artin Braids + +주제 5 「수학 · 추상」과 주제 4 를 같이 겨눴어요. 에밀 아르틴(Emil Artin)은 1925년 「Theorie der Zöpfe」에서 브레이드 군을 생성원 σ1…σn−1 과 두 가지 관계로 적었고, 랠프 폭스(Ralph Fox)와 리 노이워스(Lee Neuwirth)는 1962년에 이 군이 평면 위 서로 다른 n 점이 놓이는 배치 공간의 기본군이라는 것을 보였어요. 패턴은 그 두 번째 뜻을 그대로 그려요. 위 30줄 띠에 점 3~7개가 위에서 본 대로 서고, σi 는 x 순서로 i 번째와 i+1 번째 점을 그 가운데를 축으로 시계 반대 방향 반 바퀴, 역원은 시계 방향 반 바퀴 돌려요. 아래 98줄은 점들의 x 를 시간을 따라 내려 그린 시공간 그림이라, 새 줄이 띠 바로 밑에 생기고 지난 줄은 아래로 흘러 나가요. 띠에서 아래쪽 점을 앞으로 정해서, σi 에서는 오른쪽으로 가는 가닥이 위로 지나고 뒤로 지난 가닥은 끊겨요. Word 노브는 무작위 낱말 말고도 온 꼬임 (σ1…σn−1)ⁿ, 보로메오 브레이드 (σ1σ2⁻¹)³, 브레이드 관계를 고리로 쓴 σ1σ2σ1σ2⁻¹σ1⁻¹σ2⁻¹, 양의 브레이드를 고르고, 앞의 셋은 다 돌면 색이 제자리로 와서 띠 테두리가 한 번 밝아져요. 온 꼬임은 세 가닥에서 6 교차에 7.5초, 일곱 가닥에서 42 교차에 52.5초가 걸려요. 첫 판은 반 바퀴를 smoothstep 으로 18줄에 몰아 교차 가운데 기울기가 한 줄에 3.3 LED 라 계단처럼 보였는데, 회전을 거의 고른 빠르기로 바꾸고 생성원 하나에 간격의 1.15 배 줄을 주자 1.7 LED 로 누워 S 자가 됐고 움직임도 0.868 에서 0.604 로 내려왔어요. 잰 값은 움직임 0.604, 촘촘함 0.047, 가장자리 0.418, 복잡도 0.883, 채움 0.321, 색 0.270, 모양 바뀜 0.145 이고, thirds 는 24/36/35, 띠 테두리 두 줄을 빼면 20/36/35, 노브는 0.11, 0.13, 0.18, 0.11 이에요. 새로 본 것은 보로메오 브레이드가 15판 Plait 의 세 가닥 땋기와 같은 그림이라는 거예요. 위아래가 번갈아 드는 땋기가 (σ1σ2⁻¹)ᵏ 이고, 세 번 돌린 것을 닫으면 고리 하나만 빼도 나머지 둘이 풀리는 보로메오 고리가 돼요. + +### Walsh Functions + +주제 5 「수학 · 추상」으로, 사인 대신 +1 과 −1 만 쓰는 직교 함수계를 물결·간섭 갈래에 세웠어요. 조지프 월시(Joseph L. Walsh)는 1923년 「A closed set of normal orthogonal functions」에서 이 함수들을 부호가 바뀌는 횟수(sequency)대로 늘어놓았고, 같은 함수들은 제임스 조지프 실베스터(James Joseph Sylvester)가 1867년에 [H, H; H, −H] 를 거듭해 짠 ±1 행렬의 행이기도 해요. 레이먼드 페일리(Raymond Paley)는 1932년에 라데마허 사각파의 곱으로 같은 집합을 다른 차례(dyadic)로 적었어요. 패널 위 64×64 는 8×8 LED 타일 64장으로 된 2차원 Walsh 기저 표예요. 타일 사이에 틈을 둘 자리가 없어서 처음에는 이웃 타일이 한 무늬로 이어졌는데, 함수마다 두 sequency 의 합이 짝수면 파랑, 홀수면 분홍으로 칠하자 sequency 차례에서 바둑판으로 갈렸어요. 아래 64×64 는 돌아가는 네모와 원이 흐르는 추상 그림을 8×8 블록 64개로 나눠, 블록마다 정수만 쓰는 빠른 Walsh-Hadamard 변환의 계수를 지그재그 차례로 하나씩 더해 되살려요. 더해지는 계수의 타일은 위에서 하얗게 빛나고, 아래에는 그 기저가 블록마다 계수만큼 하얀 도장으로 잠깐 찍혀요. Order 를 돌리면 타일 64장이 0.9초 동안 새 자리로 미끄러지고 셈이 처음부터 다시 서요. sequency 차례의 k 번째 행이 부호를 꼭 k 번 바꾸는지 setup 에서 세어 보고, 변환은 무작위 블록 200개에서 정의대로 셈한 값과 0 차이로 맞았어요. 계수 16개만 남기면 RMS 오차가 255 가운데 sequency 차례 9.6, natural 차례 14.2 였고, dyadic 은 9.2 로 sequency 와 거의 같았어요. 두 차례는 표의 왼쪽 위 1×1, 2×2, 4×4 정사각형에 같은 함수를 차례만 바꿔 담아서 지그재그의 대각선 안에서만 갈라져요. 잰 값은 움직임 0.080 에 채움 0.772 이고 thirds 는 55/76/100 이며, 노브 반응은 Speed 가 0.07 로 가장 약하고 Hue 가 0.19 로 가장 세요. + +### Blinking Vortex + +주제 5 「수학 · 추상」으로 하산 아레프(Hassan Aref)가 「Stirring by chaotic advection」(J. Fluid Mech. 143, 1984)에서 다룬 깜박이는 소용돌이를 그렸고, 주제 1 의 가장자리 × 움직임 1,4 칸도 같이 겨눴어요. 60 LED 떨어진 점 소용돌이 둘이 패널 가운데 세로로 서서 반 주기씩 번갈아 켜지고, 켜진 소용돌이는 거리 r 의 물을 μa²/(2r²) 라디안만큼 돌려요. 아레프의 원형 벽은 빼고 무한 평면으로 두어서 반 주기 하나가 r 에만 기대는 정확한 회전이고, LED 마다 지난 회전을 하나씩 거꾸로 돌려 처음 물감 자리를 찾으니 물감을 격자에 옮겨 담는 번짐이 없어요. 회전의 야코비안도 함께 끌고 와서(행렬식이 1 이라 넓이를 지켜요) LED 한 칸이 처음 물감에서 덮는 자리를 재고, 여러 띠를 덮는 LED 는 그 평균을 보여 줘요. 첫 판은 하늘색과 호박색 띠 사이에 검은 물 띠를 둔 딱딱한 배치라 가장자리가 0.674, 움직임이 0.655 로 칸에서 멀었어요. 그래서 색은 청록과 보라가 날카롭게 바뀌되 둘 다 파랑이 가장 밝은 채널이라 밝기 계단이 생기지 않게 했어요. 밝기는 띠 가운데가 밝고 색이 바뀌는 곳이 어두운 코사인이라, 눌려 가늘어져도 이웃 LED 사이 계단이 40 을 넘기 전에 평균으로 바래요. 그것만으로는 가장자리 0.027, 움직임 0.263 이 되어 이번엔 너무 고요했어요. 소용돌이 간격을 48 에서 60 LED 로, Speed 2 의 반 주기를 1초에서 0.45초로, 여는 세기 μ 를 1 에서 2 로 올리고 물감이 12/μ 반 주기마다 새로 깔리게 하자 움직임 1.952, 가장자리 0.129 로 칸 안에 들었어요. 나머지 잰 값은 촘촘함 0.054, 복잡도 2.356, 채움 1.000, 색 0.387, 모양 바뀜 0.091 이고, thirds 는 99/100/99, 노브는 0.33, 0.21, 0.21, 0.25 예요. 새로 본 것은 움직임이 덜 섞인 물감에서만 나온다는 거예요. 다 섞인 곳은 LED 마다 같은 평균색이라 아무리 빨리 돌아도 멈춰 보여서, 반 주기가 0.4초일 때 물감 수명을 48/μ 에서 12/μ 로 줄인 것만으로 움직임이 1.793 에서 2.205 로 올랐어요. + +### Dupin Cyclide + +주제 5 「수학 · 추상」으로 뒤팽 사이클라이드를 그렸어요. 샤를 뒤팽(Charles Dupin)이 1822년에 다룬 이 곡면은 곡률선 두 무리가 모두 원이에요. 원환면을 구면 반전으로 뒤집으면 이 곡면이 되고, 원기둥이나 원뿔을 뒤집어도 돼요. 패널 위에서는 원환면이 서서 돌고, 아래에서는 그 원환면을 밑에 둔 반전 중심으로 뒤집은 상이 같은 각으로 돌아요. 원환면의 자오선 18 개와 위선 10 개가 반전 뒤에도 원이라 코드는 상의 세 점으로 원을 정하는데, 노브 35 자리에서 351,000 점을 직접 반전해 맞대 보니 그 원에서 가장 멀리 벗어난 점도 4.6×10⁻¹⁴ 였어요. Shape 노브가 관의 반지름을 중심원 반지름의 0.5 배에서 1.6 배까지 키우면 1 배에서 구멍이 한 점으로 닫혀 뿔 사이클라이드가 되고, 그 뒤로는 곡면이 제 몸을 뚫는 방추 사이클라이드가 돼요. Distance 노브는 원환면과 반전 중심 사이를 바깥 반지름의 1.2 배에서 29 배까지 벌려요. 처음 자리 0.06 에서는 상의 관이 아래쪽에서 위쪽의 3.1 배로 굵고, 29 배에서는 1.09 배라 원환면과 거의 같아요. 첫 판은 자오선을 원환면에 고르게 놓아서 상의 가는 쪽과 굵은 쪽의 원 간격이 2.93 배 벌어졌고, 반전이 늘리는 배율의 제곱근을 따라 놓자 두 곡면 다 1.72 배가 됐어요. 잰 값은 움직임 0.541, 촘촘함 0.052, 가장자리 0.284, 복잡도 1.122, 채움 0.305, 색 0.095, 모양 바뀜 0.076 이고, thirds 는 32/29/30, 노브는 0.04, 0.08, 0.07, 0.05 예요. 새로 본 것은 고리 사이클라이드가 이 패널에 홀로 서지 못한다는 거예요. 고리를 마주 보면 너비와 높이가 같아서 64 LED 너비에 맞추면 128 줄의 절반만 차고, 그래서 뒤집기 전 원환면을 위에 같이 세웠어요. + ## 지도 지도의 자리는 손으로 매긴 점수가 아니라 잰 값이에요. 패턴마다 처음 노브 값으로 Lab 엔진에서 8초를 돌리고, 2초부터 6초 동안의 화면을 재요. LED 하나의 밝기는 세 채널 가운데 가장 밝은 값으로 봐요. @@ -3633,6 +3712,8 @@ ESP32-S3 로 옮길 때 걸릴 것도 있어요. Reaction Diffusion 은 Node 에 66판은 64판까지 든 548개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 열셋 가운데 같이 돈 65판(#328)이 고른 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 빼고 촘촘함 × 모양 바뀜 4,4 와 복잡도 × 채움 4,1 을 겨눴어요. 촘촘함 × 모양 바뀜 4,4 는 이웃 차이 평균이 0.297 을 넘으면서 모양 바뀜이 0.328 을 넘는 칸이에요. 고운 결은 16×8 블록 안에서 평균되어 지워지니, 결은 둔 채로 블록 평균이 통째로 옮겨 가야 해요. Barrier Grid 는 낱말과 음화를 한 칸씩 엇갈려 짠 인쇄 위로 막대 격자가 미끄러져, 금빛 글자가 파란 바탕의 검은 글자로 뒤집혀요. 첫 판은 촘촘함 0.250, 모양 바뀜 0.228 이었고, 막대에 바둑판 구멍을 내고 「빛」의 세로획을 4 LED 의 배수에 세우자 0.316 과 0.381 이 됐어요. 복잡도 × 채움 4,1 은 복잡도가 2.172 를 넘으면서 채움이 0.302 아래인 칸이에요. Wilkinson's Polynomial 은 의사근 집합 안의 알갱이마다 켤지와 밝기를 따로 뽑아 deflate 가 줄이지 못하게 하고, 집합 밖 알갱이는 24 아래에 두어 복잡도 2.203, 채움 0.283 이에요. 성운을 24 아래 먼지로만 둔 첫 판은 1.259 와 0.060 이었어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 66판까지 든 558개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 열셋에서 열하나로 줄었어요. 겨눈 두 칸은 둘 다 넷이 됐어요. +67판은 66판까지 든 558개에 빈 칸도 둘뿐인 칸도 없어서, 셋뿐인 칸 열하나 가운데 같이 돈 65판(#328)이 고른 촘촘함 × 색 4,4 와 색 × 모양 바뀜 4,4 를 빼고 복잡도 × 모양 바뀜 4,4 와 가장자리 × 움직임 1,4 를 겨눴어요. 복잡도 × 모양 바뀜 4,4 는 복잡도가 2.171 을 넘으면서 모양 바뀜이 0.328 을 넘는 칸이에요. 모양 바뀜 축의 끝은 Cloth 의 0.413 이라 이 칸은 0.328 에서 0.413 사이의 좁은 띠예요. Radon Transform 은 광자를 세는 잡음으로 사이노그램과 재구성을 deflate 가 줄이지 못하는 낟알로 채우고, 한 바퀴가 끝날 때 두 칸을 함께 어둡게 해서 블록 평균을 통째로 옮겨요. 첫 판은 복잡도 1.866, 모양 바뀜 0.167 이었고, 검정까지 지운 판은 0.488 로 축 끝을 넘어 모든 칸의 경계를 옮겼어요. 빛의 15% 에서 멈추자 복잡도 2.333, 모양 바뀜 0.375 가 됐어요. 가장자리 × 움직임 1,4 는 40 을 넘는 계단의 몫이 0.210 아래이면서 움직임이 1.529 를 넘는 칸이에요. 가장자리는 가장 밝은 채널로만 재서, Blinking Vortex 는 물감 두 색을 파랑이 가장 밝은 청록과 보라로 골라 색이 바뀌는 자리에 밝기 계단을 두지 않고, 밝기는 띠마다 코사인으로 오르내리게 했어요. 그것만으로는 가장자리 0.027, 움직임 0.263 으로 너무 고요해서, 반 주기를 0.45초로 줄이고 물감을 12/μ 반 주기마다 새로 깔자 가장자리 0.129, 움직임 1.952 가 됐어요. 다 섞인 물은 LED 마다 같은 평균색이라 아무리 빨리 돌아도 움직임에 들지 않았어요. 열 개 다 일곱 축의 끝 안에 있어서 칸 경계는 60판 그대로예요. 66판까지 든 558개에 67판 열 개를 더한 568개에서 지도 21장은 다 `none` 이고, 셋뿐인 칸은 열하나에서 아홉으로 줄었어요. 겨눈 두 칸은 둘 다 넷이 됐어요. + ### 아직 없는 갈래 `families.json` 의 쉰여섯 갈래에 없는 것 가운데 세운 패널에 맞을 것을 적어 둬요. 29판이 연 위상·매듭 갈래는 Hopf Fibration 과 31판의 Boy's Surface, 33판의 Borromean Rings, 34판의 Lakes of Wada, 36판의 Heawood Torus, 38판의 Sperner's Lemma, 39판의 Borsuk-Ulam, 44판의 Ham Sandwich, 46판의 Reeb Graph, 48판의 Möbius Energy, 49판의 Square Peg, 51판의 Persistence Barcode, 53판의 Belt Trick, 55판의 Universal Cover 예요. 28판이 열려다 접은 갈래라 28판의 Hairy Ball 은 노이즈·흐름에 그대로 있어요. 세잎 매듭은 48판의 Möbius Energy 가 처음 세웠어요. 29판은 화장품 팝업 갈래도 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Cream Swirl, Serum Gel, Glitter Fall, Loose Powder 넷이 다 들어 있어요. 30판의 Swatch Row, Pearl Sheen, Bi-Phase, Satin Fold 가 그 갈래에 넷을 더했고, 31판의 Lip Gloss, Foam Lather, Setting Mist, Sponge Blend 가 넷을 더 더해 열둘이에요. 32판의 Cushion Mesh, Clay Mask, Sugar Scrub, Chrome Powder 가 넷을 더해 열여섯이고, 33판의 Cleansing Balm, Gold Leaf, Essence Drop, Peel-Off 가 넷을 더해 스물이에요. 34판의 Cat Eye, Velvet Nap, Mood Tint, Hair Gloss 가 넷을 더해 스물넷이고, 35판의 Bath Fizz, Toner Pad, Sheet Mask, Gilded Cream 이 넷을 더해 스물여덟이고, 36판의 Lotion Ribbon, Shimmer Oil, Blooming Gel, Whipped Soap 이 넷을 더해 서른둘이에요. 38판의 Milk Ring, Couette Shimmer, Photochromic, Serum Fingers 가 넷을 더해 서른여섯이에요. 39판의 Soap Film, Blotting Paper, Serum Drip, Crackle Polish 가 넷을 더해, 37판의 넷까지 마흔넷이에요. 41판은 할로윈 갈래를 열었어요. 주제 6 의 몫이라 주제 2 로 세지 않았고, Jack-o'-Lantern, Sheet Ghosts, Spider Drop, Cauldron, Eyes in the Dark 다섯이 들어 있어요. 28판이 연 수론 갈래는 Ford Circles 와 30판의 Three Gaps, 31판의 Gaussian Moat, 32판의 Gilbreath Table, 33판의 Goldbach Comet, 35판의 Kolakoski, 36판의 Elliptic Chords, 39판의 Explicit Formula, 44판의 Bulgarian Solitaire, 48판의 Chebyshev's Bias, 49판의 Curlicue, 51판의 Sato-Tate, 53판의 Gaussian Periods, 55판의 Question Mark Function 이에요. 27판이 연 철도 갈래는 Funicular 하나이고, 필름·영화 갈래는 Horse in Motion 하나예요. 두 갈래는 주제 표가 수학 쪽으로 바뀌기 전에 열었어요. 그 뒤로 ROUND.md 는 사물로 묶는 갈래를 더 열지 않아요. 26판이 연 얼음·극지 갈래는 Iceberg 와 27판의 Sled Dogs 이고, 미생물 갈래는 Dark Field 와 27판의 Mitosis 예요. 25판이 연 농사·밭 갈래는 Rice Terraces 와 26판의 Rice Planting, 27판의 Center Pivot 이고, 화산 갈래는 Eruption 과 26판의 Geyser, 27판의 Lava Lake 예요. 24판이 연 공사·건설 갈래는 Tower Crane 과 25판의 Excavation, 26판의 Implosion 이에요. 24판이 연 도자기 갈래는 Potter's Wheel 과 25판의 Kintsugi, 26판의 Noborigama 예요. 23판이 연 무대·공연 갈래는 Proscenium 과 24판의 Flying Trapeze, 25판의 Marionette, 26판의 Shadow Play 예요. 23판이 연 짐승 갈래는 Giraffe 와 24판의 Emergence, 25판의 Snow Monkeys 예요. 22판이 연 몸 갈래는 Circulation 과 23판의 Axon, 24판의 Breath, 27판의 Fluoroscope 예요. 22판이 연 비행 갈래는 Liftoff 와 23판의 Final Approach, 24판의 Thermal, 25판의 Skydiver 예요. 20판이 연 새 갈래는 Kingfisher 와 21판의 Woodpecker, 22판의 Weaver, 23판의 Skein 이에요. 20판이 연 화학·실험 갈래는 Chromatography 와 21판의 Chemical Garden, 22판의 Flame Test, 23판의 Elephant Toothpaste 예요. 21판이 연 종이 갈래는 Pop-up Book 과 22판의 Senbazuru, 23판의 Hanging Scroll, 26판의 Origami 예요. 21판이 연 운동·경기 갈래는 Lap Pool 과 22판의 Bowling Lane, 23판의 High Dive, 26판의 Pole Vault, 27판의 Climbing Wall 이에요. 위아래로 움직이는 경기는 High Dive 와 Pole Vault 뒤로 27판에 실내 암벽이 섰어요. 19판이 연 부엌 갈래는 Popcorn 과 20판의 Kettle, 21판의 Pancakes 이고, 버섯·균사 갈래는 Foxfire 와 20판의 Veiled Lady, 21판의 Bread Mould 예요. 18판이 연 유리 갈래는 Stained Glass 와 19판의 Smalti, 20판의 Millefiori, 27판의 Glassblower 예요. 광물·결정 갈래는 Bismuth 와 19판의 Dripstone, 20판의 Agate 이고, 놀이공원 갈래는 Drop Tower 와 19판의 Helter Skelter, 20판의 High Striker, 25판의 Ferris Wheel 이에요. 말이 오르내리는 회전목마는 아직 없어요. 17판이 연 전자 회로 갈래는 Circuit Board 와 18판의 Server Rack, 21판의 Nixie 이고, 장난감 갈래는 Marble Run 과 18판의 Peg Light, 21판의 Slinky, 22판의 Pin Art 예요. 16판이 연 바다 갈래는 Abyss 와 17판의 Shoreline, 20판의 Sunglint, 23판의 Bait Ball 이고, 곤충 갈래는 Fireflies 와 17판의 Ant Farm, 20판의 Moth Lamp, 24판의 Waggle Dance 예요. 15판이 연 도시 갈래는 Metro Map 과 16판의 Neon Alley, 17판의 Crossroads, 22판의 Night Flight 예요. 15판이 연 음악 갈래는 Player Piano 와 16판의 Harp, 18판의 Turntables, 24판의 Pipe Organ 이에요. 4판이 연 직물 갈래는 Jacquard Loom 과 15판의 Plait, 19판의 Needlepoint, 23판의 Sequins 이고, 8판이 연 기계 갈래는 Gear Train 과 15판의 Cam Wave, 19판의 Paternoster 예요. 12판이 연 천체 갈래는 Star Trails 와 15판의 Analemma, 16판의 Lunation, 22판의 Galilean Moons 예요. 위성의 궤도는 Galilean Moons 가 그렸고, 해를 도는 행성은 아직 없어요. 리듬·펄스 갈래는 1판의 Euclid Pulse 와 Pulse Bars 뒤로 20판의 Change Ringing, 32판의 Arnold Tongues, 35판의 Chimera State, 38판의 Infinity Series, 41판의 Siteswap, 44판의 Kac Ring, 46판의 Stochastic Resonance 가 섰어요. @@ -3774,9 +3855,11 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 66판에서는 Golomb Ruler 와 Zermelo Navigation 이 128줄을 써요. Golomb Ruler 는 눈금 14개인 최적 자의 길이가 127 이라 눈금 0 이 맨 위 줄에, 127 이 맨 아래 줄에 서고, 91개 호가 그 사이를 덮어 thirds 가 55/68/48 이에요. 왼쪽 자와 오른쪽 길이 자를 빼고 재도 49/63/44 예요. Zermelo Navigation 은 121번 줄 항구에서 0.5초마다 떠난 앞선이 1초에 18 LED 씩 올라가 패널 끝에 닿아서 66/64/39 예요. Etaoin Shrdlu 도 위에 글줄이, 아래에 막대가 서서 26/17/49 이고, 늘 켜 둔 라이노타이프 줄을 빼면 27/17/44 예요. 가운데 3분의 1 은 떨어지는 글자만 지나가서 가장 비어요. 나머지 가운데 Wilkinson's Polynomial 은 근 1~7 이 거의 움직이지 않는 점으로 남아 아래 3분의 1 이 4 예요. +67판에서는 Radon Transform 과 Artin Braids 가 128줄을 써요. Radon Transform 은 위 64×64 칸에 재구성이, 아래 64×64 칸에 사이노그램이 서고 광자 잡음이 빈 바탕까지 낟알로 덮어서 thirds 가 95/94/87 이에요. Artin Braids 는 위 30줄 띠 아래 98줄이 점들의 자취라, setup 에서 101 줄을 미리 그어 첫 프레임부터 가닥이 바닥까지 내려와서 24/36/35 이고, 띠 테두리 두 줄을 빼면 20/36/35 예요. 글자 다섯 가운데 Distance Field 는 56×116 LED 쯤 되는 글자 하나가 서서 71/64/71 이고, Hanging Line 은 탈네모꼴 글줄 사이가 비어 13/16/9 로 가장 엷어요. + ### 낯선 수학 -주제 5 로 쓴 수학과 다음에 해 볼 수학이에요. 28판이 처음 열 개를 썼어요. 슈람-뢰브너 진화(Loewner Trace), 포드 원(Ford Circles), 털 난 공 정리(Hairy Ball), 아즈텍 다이아몬드와 북극권(Aztec Diamond), 부니모비치 경기장(Stadium Billiard), 라우지 프랙탈(Rauzy Fractal), 고셋의 4₂₁ 다면체와 E8(Gosset Polytope), 콘웨이의 보고 말하기 수열(Look and Say), 위토프 게임의 그런디 값(Wythoff Nim), 쌍곡 평면의 띠 모형(Hyperbolic Band)이에요. 29판은 여섯을 더했어요. 클라인의 j 불변량(J-Invariant), 호프 올뭉치(Hopf Fibration), 다이슨 브라운 운동(Dyson Gas), 가케야 바늘 문제의 페론 나무(Perron Tree), 임계선 위의 리만 제타 함수(Zeta Loops), 길버트-섀넌-리즈 리플과 베이어-디아코니스의 일곱 번(Seven Shuffles)이에요. 30판도 여섯을 더했어요. 세 간격 정리(Three Gaps), 호프스태터 나비와 TKNN 정수(Hofstadter Butterfly), 노이만의 외부 당구(Outer Billiards), 볼드의 탄도 퇴적과 KPZ 앞면(Ballistic Deposition), 아르놀트의 미앤더(Meander), 브로드벤트와 해머슬리의 스미기(Percolation)예요. 31판도 여섯을 더했어요. 마스킷 조각의 클라인 군 극한 집합(Indra's Pearls), 리틀우드 다항식의 근(Littlewood Roots), 고든의 가우스 해자(Gaussian Moat), 아르놀트의 고양이 사상(Cat Map), 자부스키와 크루스칼의 KdV 솔리톤(KdV Solitons), 보이 곡면(Boy's Surface)이에요. ROUND.md 의 「낯선 수학 예」 여덟 가운데 쓴 것은 슈람-뢰브너 진화와 아즈텍 다이아몬드, 모듈러 형식, 호프 올뭉치, 세 간격 정리, 외부 당구 여섯이었고, 31판이 리틀우드 다항식의 근과 클라인 군의 극한 집합을 써서 여덟을 다 썼어요. 32판도 여섯을 더했어요. 탤벗 효과와 양자 카펫(Talbot Carpet), 길브레스 추측의 표(Gilbreath Table), 원 사상의 아르놀트 혀(Arnold Tongues), 콘웨이와 라딘의 핀휠 타일링(Pinwheel Tiling), 윌킨슨과 윌렘센의 침입 스미기(Invasion Percolation), 울람의 문제와 해머슬리 과정(Hammersley Lines)이에요. 33판도 여섯을 더했어요. 에인절, 홀로이드, 로믹, 비라그의 무작위 정렬 네트워크(Sorting Network), 베르트랑의 역설(Bertrand Paradox), 펜로즈의 비출 수 없는 방(Penrose Room), 당들랭 구(Dandelin Spheres), 테이트의 보로메오 고리(Borromean Rings), 골드바흐 혜성(Goldbach Comet)이에요. 34판도 여섯을 더했어요. 톰슨 문제와 오일러가 남기는 다섯 이웃 열둘(Thomson Problem), 켈빈의 배 물결(Kelvin Wake), 요네야마가 낸 와다의 호수(Lakes of Wada), 샤이데거의 강 그물(Scheidegger Rivers), 모저의 소파 문제와 해머슬리의 소파(Moving Sofa), 크누스의 춤추는 링크로 푼 펜토미노 덮기(Pentomino Cover)예요. 35판도 여섯을 더했어요. 뢸로의 폭이 일정한 곡선(Constant Width), 렌니의 주차 문제(Rényi Parking), 카리의 비주기 왕 타일(Kari-Culik Tiles), 콜라코스키 수열(Kolakoski), 쿠라모토와 바톡토흐의 키메라 상태(Chimera State), 주콥스키 날개(Joukowsky Airfoil)예요. 36판도 여섯을 더했어요. 그레이슨의 정리가 말하는 곡선 단축 흐름(Curve Shortening), 헤우드의 일곱 색 토러스 지도(Heawood Torus), 슈바르츠 랜턴(Schwarz Lantern), 윌브러햄과 깁스의 현상(Gibbs Phenomenon), 타원 곡선의 현과 접선(Elliptic Chords), 지먼의 파국 기계(Catastrophe Machine)예요. 37판도 여섯을 더했어요. 포천의 쓸기 선 알고리즘(Fortune's Sweep), 앙투안의 목걸이(Antoine's Necklace), 카르만 소용돌이 길의 안정성(Kármán Street), 단단한 원판의 헥사틱 상(Hexatic Phase), 존슨-멜 쪽매와 아브라미 법칙(Johnson-Mehl), 쿠라모토-시바신스키 방정식(Kuramoto-Sivashinsky)이에요. 38판도 여섯을 더했어요. 앤더슨 국소화(Anderson Localization), 무어가 찾고 Chenciner 와 Montgomery 가 증명한 세 몸 문제의 8자 해(Figure-Eight Orbit), 예센과 빈트너, 에르되시의 베르누이 합성곱(Bernoulli Convolutions), 브라우어르의 고정점 정리를 증명하는 슈페르너 보조정리(Sperner's Lemma), 에겐베르거와 폴리아의 항아리(Pólya Urn), 뇌르고르의 무한 수열(Infinity Series)이에요. 39판도 여섯을 더했어요. 고든, 웹, 월퍼트의 등스펙트럼 북(Isospectral Drums), 폰 망골트의 꼴로 쓴 리만의 명시 공식(Explicit Formula), 요한 베르누이의 최단 강하선(Brachistochrone), 슈타이너의 포리즘(Steiner Chain), 모런 집단으로 그린 킹먼의 합류(Kingman Coalescent), 보르수크-울람 정리(Borsuk-Ulam)예요. 40판도 여섯을 더했어요. 앨더스의 브라운 연속 랜덤 나무(Continuum Random Tree), 슈뢰딩거의 수소 원자 고유 함수(Hydrogen Orbital), 젤도비치 근사의 팬케이크(Zeldovich Pancakes), 망델브로가 내다보고 롤러, 슈람, 베르너가 증명한 브라운 운동의 바깥 경계(Brownian Frontier), 복소 긴즈부르크-란다우 방정식의 결함 난류(Defect Turbulence), 루퍼트 왕자의 정육면체(Prince Rupert's Cube)예요. 41판은 다섯을 더했어요. 홀과 왜건의 현수선 길과 정다각형 바퀴(Square Wheels), 퐁슬레의 포리즘(Poncelet Porism), 슐레플리의 4차원 정다포체를 자른 단면(Polytope Slices), 클림켁과 티먼, 마그누손의 사이트스왑(Siteswap), Dudeney 의 잡화상 퍼즐과 월리스-보여이-게르비엔 정리(Hinged Dissection)예요. 42판은 다섯을 더했어요. 바른스도르프 규칙으로 도는 기사 여행(Knight's Tour), 페르미와 파스타, 울람, 칭구의 되돌아옴(FPUT Recurrence), 포슬리에-립킨 반전기(Peaucellier Linkage), 몰리의 삼등분선 정리(Morley Triangle), 랄과 마드라스, 소칼의 피벗 알고리즘으로 돌린 자기 회피 걸음(Pivot Walk)이에요. 43판은 다섯을 더했어요. 월리스 워커의 칼레이도사이클(Kaleidocycle), 쌍곡형에서 록소드롬형으로 감기는 뫼비우스 변환의 흐름(Möbius Loxodrome), 터트의 용수철 정리(Tutte Embedding), 디펀트와 크래비츠의 고리 정리를 품은 히토메자시 사시코(Hitomezashi), 룽게 현상과 체비쇼프 점(Runge Phenomenon)이에요. 44판은 다섯을 더했어요. 마크 카츠의 고리 모형(Kac Ring), 스톤과 튜키의 햄 샌드위치 정리(Ham Sandwich), 브란트의 불가리아 솔리테어(Bulgarian Solitaire), 엘스켄스와 프리시의 탄도 소멸(Ballistic Annihilation), 페레그린의 솔리톤(Peregrine Soliton)이에요. 45판은 다섯을 더했어요. 로건-셰프와 베르시크-케로프의 극한 곡선으로 다가가는 영 도형의 플랑셰렐 성장(Plancherel Growth), 도브루신 경계 조건을 건 임계 이징 모형의 SLE(3) 경계(Ising Interface), 프리예제프와 동료들의 오일러 걷개로 쌓는 로터 라우터 응집과 레빈-페레스의 스매시 합(Rotor-Router), 폴리아의 과수원 문제(Pólya's Orchard), 넬슨의 평면 색칠 문제와 아이즈벨의 일곱 색(Hadwiger-Nelson)이에요. 46판은 다섯을 더했어요. 브릴루앙의 구역(Brillouin Zones), 모스 이론과 레브의 그래프(Reeb Graph), 로렌츠 기체와 시나이의 당구(Lorentz Gas), 리언 글래스의 점무늬(Glass Patterns), 벤치와 수테라, 불피아니의 확률 공명(Stochastic Resonance)이에요. 47판은 다섯을 더했어요. 오키프와 홍, 스트로가츠의 스워멀레이터(Swarmalators), 빌스의 외로운 달리기 선수 추측(Lonely Runner), 샤츠의 올로이드와 그 전개도(Oloid), 쾨베의 원 채우기 정리와 콜린스-스티븐슨의 반복(Koebe Packing), 콘웨이의 병사와 황금비 무게(Conway's Soldiers)예요. 48판은 다섯을 더했어요. 오하라의 매듭 뫼비우스 에너지(Möbius Energy), 체비쇼프의 편향과 루빈스타인-사르낙의 밀도(Chebyshev's Bias), 섀든과 레키엔, 마스든의 이중 소용돌이 능선(Double Gyre), 아자리와 프로스트의 깜빡이는 래칫(Flashing Ratchet), 망델브로의 프랙탈 스미기(Fractal Percolation)예요. 49판은 다섯을 더했어요. 뒤러의 전개 문제와 셰퍼드의 추측(Dürer's Unfolding), 베리와 골드버그가 재규격화한 세타 합의 꼬부랑 곡선(Curlicue), 오일러 방정식과 푸앵소의 폴호드로 본 테니스 라켓 정리(Tennis Racket), 켐프의 사슬과 네 색 정리(Kempe Chains), 퇴플리츠의 내접 정사각형 문제와 그린-로브의 직사각형(Square Peg)이에요. 50판은 다섯을 더했어요. 크레이치넌과 배철러의 2차원 난류가 거꾸로 가는 캐스케이드와 맥윌리엄스의 소용돌이(Inverse Cascade), 사포발과 로소, 구예의 확산 앞줄(Diffusion Front), 필리와 마르케티, 케이츠와 타이외르의 운동성 상분리(Motility Clusters), 박과 스네펜의 진화 모형(Bak-Sneppen), 리세의 영원한 가속(Risset Rhythm)이에요. 51판은 다섯을 더했어요. 스미스와 마이어스, 캐플런, 굿먼-스트라우스의 모자 타일과 H/T/P/F 메타타일 치환(Hat Monotile), 반도와 동료들의 최적 속도 모형과 스기야마의 원형 트랙 실험(Phantom Jam), 사토와 테이트의 법칙과 타원곡선의 a_p(Sato-Tate), 칼라비와 야우의 공간에서 핸슨이 잘라 그린 5차 페르마 곡선(Calabi-Yau Quintic), 에델스브루너와 레처, 조모로디언의 지속 호몰로지와 바코드(Persistence Barcode)예요. 52판은 다섯을 더했어요. 슈람과 셰필드가 등고선을 SLE(4) 로 보인 2차원 가우스 자유장(Gaussian Free Field), 당튀와 마즈무다르, 베링거의 광탄성 힘 사슬(Force Chains), 앤스컴의 사중주와 마테이카, 피츠모리스의 같은 통계 담금질(Anscombe's Quartet), 드 브라윈 수열과 BEST 정리(de Bruijn Sequence), 라인스의 베타 평면 띠 제트(Zonal Jets)예요. 53판은 열을 더했어요. 듀크, 가르시아, 루츠가 그린 가우스 주기(Gaussian Periods), 부르가 보인 현수면과 나선면의 휨(Catenoid-Helicoid), 디랙의 벨트 묘기(Belt Trick), 헬름홀츠와 러브의 연기 고리 개구리뜀(Leapfrogging Vortices), 브룩스, 스미스, 스톤, 텃의 정사각형 분할(Squared Rectangle), 포드와 풀커슨의 최대 흐름 최소 절단(Max-Flow Min-Cut), 코스타스 배열(Costas Array), 바라니, 비어든, 칸의 무게중심 분할(Barycentric Subdivision), 무코파디아야와 크네저의 네 꼭짓점 정리(Four-Vertex Theorem), 오일러의 엘라스티카(Euler's Elastica)예요. 54판은 열을 더했어요. 베리의 무작위 파동과 보고몰니, 슈미트가 스미기로 센 마디 영역(Nodal Domains), 피어험버트가 사인 흐름에서 찾은 이상 고유 모드(Strange Eigenmode), 피어시 적분과 베리, 업스틸의 첨점 회절 파국(Pearcey Cusp), 나겔과 바이스의 STIT 모자이크(STIT Tessellation), 레비의 아크사인 법칙(Arcsine Law), 스퇴르메르가 다룬 쌍극자 장 속 대전 입자(Størmer Trap), 게일과 섀플리의 지연 수락(Stable Marriage), 스메일의 말굽(Smale Horseshoe), 울람 수열과 슈타이너베르거가 찾은 숨은 진동수(Ulam Sequence), 위어와 펠런의 거품과 켈빈의 깎은 팔면체(Weaire-Phelan)예요. 55판은 열을 더했어요. 밀러와 셰필드의 허수 기하(Imaginary Geometry), 티소의 지시 타원과 메르카토르 투영(Tissot's Indicatrix), 민코프스키의 물음표 함수(Question Mark Function), 버로스와 휠러의 변환(Burrows-Wheeler), 뷔퐁의 바늘과 바르비에의 국수(Buffon's Noodle), 부자의 리듬 타일링 캐넌(Vuza Canon), 하인과 내시의 헥스와 게일의 헥스 정리(Hex Theorem), 8자 고리의 범피복과 자유군의 케일리 그래프(Universal Cover), 오브친니코프와 젤도비치, 투생과 윌첵의 확산 제한 소멸(Toussaint-Wilczek), 헬름홀츠와 켈빈의 와류 시트와 크라스니의 와동 덩이(Vortex Sheet)예요. 56판은 열을 더했어요. 마테롱의 죽은 잎과 켄들, 퇴네스가 아래에서 본 완전 표본(Dead Leaves), 골롬의 트로미노 깔기(Golomb Trominoes), 윌슨의 고리 지운 걸음과 롤러, 슈람, 베르너의 균등 생성나무 페아노 곡선(Tree Peano Curve), 블로흐와 제너의 진동과 워니어의 사다리(Bloch Oscillations), 앤드루스의 곡선과 아시모프의 그랜드 투어(Andrews Curves), 지니브르와 기르코의 원 법칙과 좀머스, 크리산티, 솜폴린스키, 스타인의 타원 법칙(Elliptic Law), 에르되시의 불일치 문제와 보웨인, 최, 쿤스의 3진 수열(Erdős Discrepancy), 망델브로의 곱셈 폭포와 카안, 페이리에르의 문턱(Multiplicative Cascade), 코르테, 체이킨, 골럽, 파인의 무작위 조직화(Random Organization), 칼루가레아누, 화이트, 풀러의 꼬임과 뒤틀림(Twist and Writhe)이에요. 57판도 열을 더했어요. 블룸의 중심축과 들불 변환(Medial Axis), 스메일과 윌리엄스의 솔레노이드(Solenoid), 솔로몬의 무작위 환경 속 걸음과 시나이의 걸음(Sinai's Walk), 테렐과 펜로즈의 회전(Terrell Rotation), 갈페린의 π 당구(Galperin Billiard), 페르마의 두 제곱수 정리와 자기에의 한 문장 증명(Zagier's Windmills), 스토크스 표류와 게르스트너의 트로코이드 파도(Stokes Drift), 라인하르트부터 맨, 매클라우드맨, 폰 데라우까지의 볼록 오각형 타일링(Pentagon Tilings), 페로와 하위헌스의 트랙트릭스와 자전거 바퀴 자국(Bicycle Tracks), 오스트발트 숙성과 LSW 이론(Ostwald Ripening)이에요. 58판도 열을 더했어요. 야블란과 게르데스의 거울 곡선(Mirror Curves), 가운트와 피셔, 백스터의 딱딱한 정사각형 격자 기체(Hard Squares), 다니엘 베르누이의 매달린 사슬과 베셀 함수 J0(Hanging Chain), 레일리와 테일러의 불안정성(Rayleigh-Taylor), 터키의 반평면 깊이와 라도의 중심점 정리(Tukey Depth), 롤러와 베르너의 브라운 고리 수프(Loop Soup), 카돔체프-페트비아시빌리 방정식과 코다마의 선 솔리톤 웹(KP Web), 크누스가 이름 붙인 베케트-그레이 코드(Beckett-Gray), 파스칼의 신비 육각형과 슈타이너, 커크먼의 점(Hexagrammum Mysticum), 드 스밋과 렌스트라의 판화 화랑 사상(Print Gallery)이에요. 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)예요. +주제 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)예요. 67판은 다섯을 더했어요. 라돈의 변환과 라마찬드란, 락슈미나라야난의 램프 필터(Radon Transform), 아르틴의 브레이드 군과 폭스, 노이워스의 배치 공간(Artin Braids), 월시의 함수계와 실베스터의 행렬, 페일리의 차례(Walsh Functions), 아레프의 깜박이는 소용돌이(Blinking Vortex), 뒤팽의 사이클라이드(Dupin Cyclide)예요. 목록 밖에서 다음에 해 볼 것이에요. 수론 갈래는 32판의 Gilbreath Table 까지 넷이에요. 길브레스 표는 소수 말고 다른 수열로도 세울 수 있어요. 오들리코(1993)에 따르면 2 다음에 간격이 작고 고르게 흩어진 홀수가 오는 수열에서는 1 의 열이 서고, 간격이 크면 깨져요. 간격의 크기를 노브로 걸면 1 의 열이 무너지는 자리가 보여요. 해머슬리 과정의 점 대신 격자 칸마다 지수 분포 무게를 두면 마지막 통과 스미기(로스트, 1981)가 되고, 극한 모양이 쌍곡선에서 (√x+√y)² 의 경계로 바뀌어요. @@ -3856,6 +3939,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 66판에서 본 것이에요. 정리가 수를 못 박는 대상은 이번에도 패턴의 셈으로 먼저 맞춰 봤어요. Wilkinson's Polynomial 은 곱 꼴로 셈한 동시 반복이 윌킨슨의 1959년 표를 소수 아홉째 자리까지 그대로 내서, 가장 넓은 쌍이 16.730737466 ± 2.812624894i 였어요. Hodograph 의 속도 끝 720개는 이심률 0 부터 0.95 까지 원에서 반지름의 3.3×10⁻¹⁵ 안에 있었고, Golomb Ruler 의 14 눈금 자는 91개 거리가 다 달랐어요. 극한을 말하는 정리는 패널에서 그 빠르기를 쟀어요. Shapley-Folkman 은 달 모양의 하우스도르프 거리에 n 을 곱한 값이 n = 1, 2, 4, 8 에서 11.7, 11.5, 11.4, 10.9 로 거의 그대로였고, 덮이지 않은 넓이는 1/n 보다 조금 빨리 줄었어요. 짐작과 셈이 어긋난 자리는 Zermelo Navigation 이었어요. 물살이 배보다 빠르면 소용돌이 뒤에 그림자가 질 줄 알았는데, 물이 생기지도 사라지지도 않는 물살 안에서는 돌아가면 어디든 닿아서, 그림자 1,662개가 모두 패널 가장자리에 닿아 있었어요. Wilkinson's Polynomial 도 짐작과 달랐어요. 윌킨슨의 섭동을 건 다항식은 근이 정수에 있을 때보다 덜 예민해서, 같은 δ 에서 근 16 의 구름이 1.63 에서 0.01 로 줄었어요. +67판에서 본 것이에요. 셈이 정확한 대상은 이번에도 패턴 안에서 먼저 맞춰 봤어요. Dupin Cyclide 는 원환면의 원을 반전한 세 점으로 정해 그리는데, 노브 35 자리에서 351,000 점을 직접 반전해 보니 그 원에서 가장 먼 점이 4.6×10⁻¹⁴ 였어요. Walsh Functions 는 sequency 차례의 k 번째 행이 부호를 꼭 k 번 바꾸지 않으면 setup 에서 멈추게 했고, 정수만 쓰는 변환은 무작위 블록 200개에서 정의대로 셈한 값과 하나도 다르지 않았어요. Radon Transform 은 타원의 닫힌 꼴 투영이 수치 적분과 0.003 안에서 맞았고, 잡음 없이 투영 수를 1 에서 64 까지 늘리면 반지름 30 안의 평균 오차가 0.50 에서 0.027 로 줄었어요. 같은 광자 8,000개를 투영끼리 나눠 쓰게 하자 오차는 투영 16개부터 0.19 언저리에서 멈췄어요. 수학이 그린 그림이 다른 패턴과 겹치기도 했어요. Artin Braids 의 보로메오 브레이드 (σ1σ2⁻¹)³ 은 15판 Plait 의 세 가닥 머리 땋기와 같은 그림이었어요. 셈하는 법이 그림을 바꾼 곳도 있었어요. Blinking Vortex 는 LED 마다 두 점을 찍어 물감을 고르면 섞인 가운데가 반짝이 잡음이 돼서, 회전의 야코비안을 끌고 와 LED 한 칸이 처음 물감에서 덮는 자리를 평균했어요. 점을 찍던 첫 판은 움직임이 1.366 으로, 평균한 판의 두 배 넘게 반짝임을 움직임으로 셌어요. 3차원 곡면은 이번에도 패널의 비율이 그림을 정했어요. 고리 사이클라이드를 마주 보면 너비와 높이가 같아서 64 LED 너비에 맞추면 128줄의 절반만 차고, 그래서 Dupin Cyclide 는 뒤집기 전 원환면을 위에 같이 세웠어요. + 목록 밖에서 31판 뒤에 해 볼 것이에요. 페르미-파스타-울람-칭구의 되돌아옴(1955)은 KdV Solitons 와 같은 뿌리라 같은 판에 두지 않아요. 목록 밖에서 33판 뒤에 해 볼 것이에요. 토카르스키의 방(1995)은 거울로 된 다각형 방에서 한 점의 빛이 다른 한 점에만 닿지 못하는 방이라 Penrose Room 의 광선 추적을 그대로 써요. 프리드먼과 스코라(1987)는 둥근 원 셋으로는 보로메오 고리를 만들 수 없다는 것을 보였는데, 원을 밀어 넣을 때 어디서 부딪히는지가 그림이 돼요. 정렬 네트워크의 시간 t 의 순열을 점으로 찍으면 대각선에서 원판을 지나 반대 대각선으로 도는 점 구름이 돼요. 그 원판이 2007년 논문이 내다본 아르키메데스 법칙이에요. 간격이 2k 인 소수 쌍의 수는 Goldbach Comet 과 같은 FFT 로 세고, k 마다 하디-리틀우드 인수로 갈리는 줄기 그림이 돼요. @@ -3910,9 +3995,9 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 59판 뒤에 해 볼 것이에요. 고트홀트 아이젠슈타인(1844)은 p/2 × q/2 직사각형의 대각선 아래와 위의 격자점을 세어 상호 법칙을 증명해서, 두 수의 합 (p−1)(q−1)/4 의 홀짝이 부호가 되는 것을 패널에서 셀 수 있어요. 대니얼 고테스만, 알렉세이 키타예프, 존 프레스킬(2001)의 GKP 격자 상태는 위그너 함수가 양과 음의 점으로 바둑판처럼 번갈아 서서, Wigner Cat 의 변환으로 그릴 만해요. 아서 케일리(1869)의 마디 3차 곡면 쪽으로 클렙시 곡면을 바꾸면 직선 27개가 마디 넷으로 모이며 9개로 줄어요. 하인리히 헤슈(1968)의 둘레 수 문제는 한 타일을 같은 타일로 몇 겹까지 감쌀 수 있는지 묻는 것이라, 겹마다 색을 바꿔 쌓아 볼 만해요. 존 밀너(1968)의 올뭉치는 8_19 같은 올뭉치 매듭에서 자이페르트 곡면이 매듭 둘레를 한 바퀴 돌며 여집합을 다 쓸고 지나가는 그림이 돼요. 오가타 요시히코(1988)의 ETAS 모형은 사건에 크기를 붙여 큰 사건이 자식을 많이 낳게 해서, 본진 뒤 여진이 오모리 후사키치(1894)의 법칙대로 잦아드는 것을 보여요. 코호넨 사슬을 고리로 닫아 도시 수십 개에 걸면 리처드 더빈과 데이비드 윌쇼(1987)의 탄성망이 외판원 순회를 찾아가요. 세게 흔든 알갱이 층에서 차가운 덩어리가 뜨거운 기체 위에 뜨는 알갱이 라이덴프로스트 효과(페터 에쉬위스와 세 사람, 2005)도 세운 패널에 맞아요. 루빈스타인, 샤미르, 아비단(2008)의 그래프 컷 심 카빙은 동영상을 줄일 때 시간축을 따라 이음매 면을 잘라 내요. -목록 밖에서 60판 뒤에 해 볼 것이에요. 10차 직교 라틴 방진은 둘까지 알려졌고 셋이 서로 직교하는 묶음은 아직 아무도 찾지 못했어요. Euler's Officers 의 정확 덮개 탐색을 셋째 방진 쪽으로 돌리면, 두 방진의 가로지르기가 몇 개까지 겹치는지가 그림이 될 만해요. 마틴 크루스칼(1960)과 죄르지 세케레시(1960)의 좌표로 슈바르츠실트 블랙홀을 끝까지 펴면, 펜로즈 도형이 지평선 둘이 엇갈려 나눈 네 구역과 위아래의 특이점 선으로 바뀌어요. Penrose Diagram 의 아크탄젠트 셈을 그대로 쓸 수 있어요. 요한 라돈(1917)의 변환은 허프 변환의 연속판이라, Hough Transform 의 누산기를 사인 곡선 대신 투영의 합으로 채우고 여과 역투영으로 위 정사각형을 되살리는 CT 그림이 될 만해요. 아르투어 쇤플리스(1906)는 조르당 곡선의 안이 원판과 같은 꼴이라는 것을 보였어요. Jordan Curve 의 안을 원판으로 펴는 등각 사상은 곡선이 구불거릴수록 가지 끝마다 원 둘레의 아주 좁은 호로 몰려요. 에르되시-레니 그래프에서 둘째로 큰 성분은 문턱 n/2 에서 가장 커졌다가 거대 성분에 먹혀 줄어들어서, Giant Component 의 그래프에 둘째 곡선을 겹치면 문턱이 봉우리로 보여요. 로버트 코베유와 로버트 맥퍼슨(1967)의 스펙트럼 검사로 선형 합동 생성기의 평면 간격을 2차원부터 6차원까지 셈하면, RANDU 가 3차원에서 갑자기 무너지는 것이 막대 하나로 보여요. +목록 밖에서 60판 뒤에 해 볼 것이에요. 10차 직교 라틴 방진은 둘까지 알려졌고 셋이 서로 직교하는 묶음은 아직 아무도 찾지 못했어요. Euler's Officers 의 정확 덮개 탐색을 셋째 방진 쪽으로 돌리면, 두 방진의 가로지르기가 몇 개까지 겹치는지가 그림이 될 만해요. 마틴 크루스칼(1960)과 죄르지 세케레시(1960)의 좌표로 슈바르츠실트 블랙홀을 끝까지 펴면, 펜로즈 도형이 지평선 둘이 엇갈려 나눈 네 구역과 위아래의 특이점 선으로 바뀌어요. Penrose Diagram 의 아크탄젠트 셈을 그대로 쓸 수 있어요. 아르투어 쇤플리스(1906)는 조르당 곡선의 안이 원판과 같은 꼴이라는 것을 보였어요. Jordan Curve 의 안을 원판으로 펴는 등각 사상은 곡선이 구불거릴수록 가지 끝마다 원 둘레의 아주 좁은 호로 몰려요. 에르되시-레니 그래프에서 둘째로 큰 성분은 문턱 n/2 에서 가장 커졌다가 거대 성분에 먹혀 줄어들어서, Giant Component 의 그래프에 둘째 곡선을 겹치면 문턱이 봉우리로 보여요. 로버트 코베유와 로버트 맥퍼슨(1967)의 스펙트럼 검사로 선형 합동 생성기의 평면 간격을 2차원부터 6차원까지 셈하면, RANDU 가 3차원에서 갑자기 무너지는 것이 막대 하나로 보여요. -목록 밖에서 61판 뒤에 해 볼 것이에요. 같은 재료로 가장 높이 서는 기둥은 위로 갈수록 가늘어지는 꼴이라(조지프 켈러와 프리티오프 니오르드손, 1966), Greenhill Column 의 셈에 단면을 하나 더 얹으면 돼요. 모든 모서리가 직각인 갤러리는 경비원 ⌊n/4⌋ 명이면 되고(제프 칸, 마리아 클라웨, 대니얼 클라이트먼, 1983), Art Gallery 의 증명 그림을 사각형 나누기로 바꿔 볼 만해요. 너비가 w 로 일정한 곡선은 둘레가 꼭 πw 라는 바르비에의 정리(조제프에밀 바르비에, 1860)는 Holditch Ring 처럼 꼴이 바뀌어도 같은 양이 남는 그림이 돼요. 원을 도는 전하의 역선은 꺾임이 나선으로 감기는 싱크로트론 그림이 되고(로저 첸, 1972), Thomson Kink 의 셈을 그대로 써요. 점 질량 하나에 바깥 전단을 더한 창-레프스달 렌즈(1979)는 Binary Lens 의 광선 추적으로 네 뾰족점 커스틱을 따로 세울 수 있어요. 아레프의 깜박이는 소용돌이(하산 아레프, 1984)는 Taylor Unmixing 과 달리 되돌려도 섞임이 풀리지 않는 혼돈 섞기예요. 접힌 마디 곁의 카나르는 큰 스파이크 사이에 작은 고리를 끼우는 혼합 모드 진동이 돼서(마르틴 베흐젤베르거, 2005), Canard Explosion 의 아래 자취가 그 리듬을 칠 수 있어요. 점 무리를 연속 분포로 옮기는 반이산 수송은 파워 다이어그램의 라게르 칸으로 풀려서(프란츠 아우렌하머, 1987; 캉탱 메리고, 2011), Monge Transport 의 도형을 칸으로 나눠 보일 수 있어요. +목록 밖에서 61판 뒤에 해 볼 것이에요. 같은 재료로 가장 높이 서는 기둥은 위로 갈수록 가늘어지는 꼴이라(조지프 켈러와 프리티오프 니오르드손, 1966), Greenhill Column 의 셈에 단면을 하나 더 얹으면 돼요. 모든 모서리가 직각인 갤러리는 경비원 ⌊n/4⌋ 명이면 되고(제프 칸, 마리아 클라웨, 대니얼 클라이트먼, 1983), Art Gallery 의 증명 그림을 사각형 나누기로 바꿔 볼 만해요. 너비가 w 로 일정한 곡선은 둘레가 꼭 πw 라는 바르비에의 정리(조제프에밀 바르비에, 1860)는 Holditch Ring 처럼 꼴이 바뀌어도 같은 양이 남는 그림이 돼요. 원을 도는 전하의 역선은 꺾임이 나선으로 감기는 싱크로트론 그림이 되고(로저 첸, 1972), Thomson Kink 의 셈을 그대로 써요. 점 질량 하나에 바깥 전단을 더한 창-레프스달 렌즈(1979)는 Binary Lens 의 광선 추적으로 네 뾰족점 커스틱을 따로 세울 수 있어요. 접힌 마디 곁의 카나르는 큰 스파이크 사이에 작은 고리를 끼우는 혼합 모드 진동이 돼서(마르틴 베흐젤베르거, 2005), Canard Explosion 의 아래 자취가 그 리듬을 칠 수 있어요. 점 무리를 연속 분포로 옮기는 반이산 수송은 파워 다이어그램의 라게르 칸으로 풀려서(프란츠 아우렌하머, 1987; 캉탱 메리고, 2011), Monge Transport 의 도형을 칸으로 나눠 보일 수 있어요. 목록 밖에서 62판 뒤에 해 볼 것이에요. 격자가 단단한 판이 아니라 탄성체라서 생기는 트카첸코 물결(Vito Tkachenko, 1966)을 소용돌이 자리를 흔들어 그려 볼 만한데, I. 코딩턴, P. 엥겔스, V. 슈바이크하르트, E. 코넬(2003)이 도는 응축체에서 찍었어요. 고른 회색 하나를 오차 확산(로버트 플로이드와 루이스 스타인버그, 1976)으로 찍을 때 회색 값마다 서는 벌레 무늬를, 같은 몫의 빈자리와 뭉치 문턱과 나란히 볼 만해요. 같은 격자 위 길을 i 없이 크기만으로 더하면 시드니 골드스타인(1951)과 마크 카츠(1974)의 전신 방정식 걸음이 되니, 두 합을 나란히 그리면 위상이 확산을 빛 원뿔 쪽으로 미는 것이 보여요. 에밀 포스트(1921)의 태그 시스템은 낱말 앞에서 세 글자를 지우고 지운 첫 글자가 0 이면 00 을, 1 이면 1101 을 뒤에 붙이는데, 모든 낱말이 끝나는지 아직 아무도 몰라서 낱말을 한 줄씩 시간 아래로 쌓으면 같은 시공간 그림이 될 만해요. 커티스 맥멀런(2003)과 캐리언 칼타(2004)가 찾은 종수 2 의 L자 탁자는 정다각형이 아닌데도 변의 비가 2차 무리수이면 비치의 이분법을 따라서, 같은 닫힘과 채움을 L 모양 하나로 그릴 수 있어요. 스테판 자파르(1996)가 셈한 리만 함수의 다중프랙탈 스펙트럼, 곧 점마다 다른 횔더 지수 α 가 1/2 에서 3/4 사이에 퍼지고 지수가 α 인 점들의 차원이 4α − 2 라는 것을 t 축 위의 색으로 그려 볼 만해요. 평면을 덮지 못하는 도형을 몇 겹까지 둘러쌀 수 있는지 묻는 헤슈 수(하인리히 헤슈, 1968)를, 다섯 겹까지 둘러싸이는 케이시 맨(2004)의 도형으로 그려 볼 만해요. 장필리프 부쇼와 마르크 메자르(2000)의 부의 응축 모형은 곱으로 불어나는 재산에 이웃과 나누는 몫을 더해서, 나누는 몫이 작아지면 꼬리가 파레토 법칙에서 한 사람 몫의 응축으로 넘어가요. 안드레이 마르코프(1880)의 마르코프 세 수 x² + y² + z² = 3xyz 도 (1, 1, 1) 에서 z 를 3xy − z 로 바꾸는 비에타 뛰기로 가지를 치는 나무라서, 같은 꼴의 다리 그림에 그 나무를 올려 볼 만해요. 밀턴 배빗이 1950년대에 쓰고 데이비드 루인(1959)이 증명한 육음 정리를 리듬에 옮기면 12걸음의 여섯 걸음 리듬과 그 여집합은 사이 간격의 목록이 같아서, 둘의 걸음을 나란히 세워 볼 수 있어요. @@ -3922,6 +4007,8 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 목록 밖에서 66판 뒤에 해 볼 것이에요. 제임스 싱어(James Singer, 1938)의 완전 차집합은 91 을 법으로 {0, 1, 3, 9, 27, 49, 56, 61, 77, 81} 열 점이 0 아닌 나머지를 꼭 한 번씩 재서, Golomb Ruler 의 호 그림을 91줄 원통에 감으면 빈칸도 겹침도 없는 길이 자가 돼요. 아이작 뉴턴의 회전 궤도 정리(『프린키피아』 1권 9절, 1687)는 Hodograph 의 케플러 풀이에서 해에서 본 각도만 k 배로 늘리면 되고, 역세제곱 힘을 더한 궤도의 호도그래프가 원에서 벗어나는 모습을 나란히 볼 수 있어요. Zermelo Navigation 의 32방향 데이크스트라에 물살 대신 언덕 높이를 넣으면, 한 점에서 퍼지는 측지선 앞선이 만나 접히는 절단 궤적(앙리 푸앵카레, 1905)이 그려져요. Shapley-Folkman 의 격자 민코프스키 합으로 두 모양을 섞은 (1−t)A + tB 의 넓이를 띠마다 재면, 넓이의 제곱근이 t 에 대해 오목하다는 브룬-민코프스키 부등식(헤르만 브룬, 1887; 헤르만 민코프스키, 1896)이 보여요. 같은 스무 근을 번스타인 기저로 적으면 의사근 구름이 얼마나 줄어드는지(리다 파루키와 V. T. 라잔, 1987)를 Wilkinson's Polynomial 의 동시 반복과 의사근 지도로 패널 위아래에 나란히 그릴 수 있어요. +목록 밖에서 67판 뒤에 해 볼 것이에요. 스테판 카치마르츠(Stefan Kaczmarz)의 반복법(1937)은 사이노그램을 한 줄씩 풀어 가는 재구성이라, Radon Transform 의 걸러 되비추기 옆에 세우면 투영이 적을 때 별 자국 대신 어떤 얼룩이 남는지 견줄 수 있어요. 같은 브레이드를 다르게 적은 두 낱말은 F. A. 가사이드(F. A. Garside, 1969)의 정규형에서 한 줄로 모여서, Artin Braids 의 시공간 그림 둘을 나란히 두고 모이는 것을 볼 만해요. 알프레드 하르(Alfréd Haar, 1910)의 함수계는 Walsh 함수와 같은 ±1 값이지만 받침이 좁아서, 같은 그림을 되살리면 계수가 가장자리 둘레에 모여요. Blinking Vortex 가 LED 마다 끌고 온 야코비안의 큰 특이값은 유한 시간 랴푸노프 지수라, 조지 할러(George Haller)와 위안궈청(Guocheng Yuan)의 라그랑주 응집 구조(2000)를 그 위에 바로 그릴 수 있어요. 원환면의 비스듬한 빌라르소 원(이봉 빌라르소, 1848)도 반전 뒤에 원으로 남으니, Dupin Cyclide 의 한 점을 지나는 원 넷을 같이 그려 볼 만해요. + ### 타이포 주제 7 로 해 본 글자 실험과 다음에 해 볼 것이에요. 63판과 64판이 같이 돌며 다섯씩 처음 만들었어요. 63판의 다섯은 한글 자모가 날아와 모아쓰기로 잠기는 조립(Jamo Assembly), 놀턴과 하먼의 기호 모자이크(Type Mosaic), 호프스태터의 트립렛 그림자(Trip-let), 카프리카의 계산 종이(Kaprekar's Routine), 캐럴의 낱말 사다리(Word Ladder)예요. @@ -3942,7 +4029,15 @@ Flock, Harmonograph, Big Type 은 처음 화면이 빨강이에요. 기능 감은 이번에도 Text 레이어의 글을 코드가 받는 길로 모였고, 받는 꼴이 패턴마다 달랐어요. Etaoin Shrdlu 는 글을 받아 세려는데 Text 레이어가 200자까지라(`TEXT_MAX_CHARS`) 앨리스 첫 두 문장의 464글자가 157글자로 줄어요. Barrier Grid 는 걸음마다 다른 낱말이 필요해서 낱말 목록과 낱말마다의 덮임 비트맵을 바랐고, Subpixel 은 글꼴의 글자 윤곽을 2차 베지에 점 목록으로 받기를 바랐어요. Syllable Odometer 는 패턴끼리 글꼴을 나눠 쓰는 길을 바랐어요. Lab 의 사이트 글꼴 넷은 latin subset 만 실어서 한글 픽셀 글꼴이 아직 없어요. Gridfonts 는 다른 것을 바랐어요. Style 노브의 일곱 자리에 이름을 달 수 있게 `@knobs` 가 칸 이름을 받으면, 지금 어느 스타일인지 주석을 읽지 않아도 알 수 있어요. -66판 뒤에 해 볼 것이에요. 데이비드 허프먼의 부호(1952)는 Etaoin Shrdlu 가 세고 줄 세운 열여섯 칸 위에서 지을 수 있어요. 가장 낮은 두 막대가 하나로 합쳐 올라가기를 되풀이하면 패널 위로 자라는 나무가 되고, 잦은 글자일수록 뿌리까지 가지가 짧아요. 게럴드 오스터와 니시지마 야스노리(Scientific American, 1963)가 보인 대로 주기 구조 둘을 겹치면 모아레가 생기니, Barrier Grid 의 인쇄와 막대를 동심원 주기로 짜면 낱말이 원 띠마다 빛과 그늘로 갈려요. 크리스 그린이 Valve 에서 SIGGRAPH 2007 에 낸 거리장 글자 확대는 Subpixel 의 윤곽을 거친 격자의 거리장으로 바꿔 키운 뒤 문턱으로 자르면 되고, 모서리가 둥글게 녹는 것이 픽셀 단위로 보여요. 안상수체(안상수, 1985)는 자모마다 꼴이 한 벌뿐인 탈네모꼴이라, Syllable Odometer 의 바퀴 셋이 고른 자모를 제자리에 겹쳐 놓기만 해도 11,172 음절이 다 서요. +66판 뒤에 해 볼 것이에요. 데이비드 허프먼의 부호(1952)는 Etaoin Shrdlu 가 세고 줄 세운 열여섯 칸 위에서 지을 수 있어요. 가장 낮은 두 막대가 하나로 합쳐 올라가기를 되풀이하면 패널 위로 자라는 나무가 되고, 잦은 글자일수록 뿌리까지 가지가 짧아요. 게럴드 오스터와 니시지마 야스노리(Scientific American, 1963)가 보인 대로 주기 구조 둘을 겹치면 모아레가 생기니, Barrier Grid 의 인쇄와 막대를 동심원 주기로 짜면 낱말이 원 띠마다 빛과 그늘로 갈려요. + +67판의 다섯은 섀넌이 1948년에 보인 근사 영어를 노브로 차수를 올리며 치는 쪽(Shannon's Approximations), 곰링어의 구체시 「silencio」(1953)의 빈 자리가 옮겨 다니는 낱말 마당(Silencio), 크리스 그린의 거리장으로 키운 큰 글자 하나(Distance Field), 낮게 도는 램프가 비추는 브라유 점자(Braille Relief), 안상수체를 본뜬 탈네모꼴로 한 자모씩 쳐지는 「진달래꽃」(Hanging Line)이에요. + +67판에서 본 것이에요. 이번에도 다섯이 다 글자 꼴을 코드에 적었고, 옮겨 쓴 글꼴은 없어요. 큰 글자는 꼴을 적는 법이 그림을 정했어요. Distance Field 는 글자를 겹친 다각형 24개로 적었는데, 다각형마다 잰 거리의 최솟값을 쓰면 획이 만나는 안쪽에서 W 가 6.8 LED 까지 틀려서, 변을 만나는 자리마다 잘라 한쪽만 글자 안인 조각으로 셌어요. 작은 글자는 줄 길이가 판을 정했어요. 3×5 글꼴로도 silencio 는 31 LED 라 셋이 나란히 서지 않아서, Silencio 는 라틴 낱말을 책등처럼 돌려 세웠어요. Hanging Line 의 탈네모꼴은 「진달래꽃」 94 음절의 줄 길이가 네모 칸보다 27% 짧지만 줄 간격이 23 LED 로 6 LED 넓어서, 시 한 벌의 높이는 639 와 660 LED 로 3% 만 갈렸어요. 천천히 쳐지는 글은 Speed 반응이 약했어요. Shannon's Approximations 는 새 글자의 빛을 0.5초에서 1초로, Silencio 는 읽는 빛의 잔광을 글자 2.2 개 길이에서 실제 시간 1.1초로 바꾸고서야 0.03 을 넘었어요. 빛으로 그린 글자는 램프가 글자를 정했어요. Braille Relief 는 점의 높이보다 램프의 높이가 크게 읽혀서, 같은 돋음에서 램프가 4도면 밝기 상위 2% 가 223, 20도면 103 이었어요. 빈 자리도 셀 수 있었어요. Silencio 에서 빠진 것은 7×26 LED 낱말 하나인데, 이웃 낱말 사이 틈까지 합쳐 16×58 LED 가 0 그대로 남아요. + +기능 감은 이번에도 글을 코드가 받는 길로 모였는데, 받으려는 것이 다섯 다 달랐어요. Shannon's Approximations 는 Text 레이어의 Silkscreen 이나 펌웨어의 Matrix Fonts 를 비트맵으로 받아 쓰기를 바랐어요. 대문자 26자를 15자리 문자열로 손수 그렸기 때문이에요. Silencio 는 Text 레이어가 글줄을 90° 돌려 쓰기를 바랐고, Distance Field 는 Text 레이어 효과에 거리장으로 내는 테두리와 바깥 빛이 들기를 바랐어요. Braille Relief 는 코드 레이어가 위 Text 레이어의 덮임을 높이 지도로 읽기를 바랐어요. 그러면 사람이 친 낱말이 같은 램프 아래 돋을새김으로 떠요. Hanging Line 은 글자를 하나씩, 한글은 자모 차례로 내보내는 타자 효과를 바랐어요. + +67판 뒤에 해 볼 것이에요. 섀넌의 「Prediction and Entropy of Printed English」(1951)처럼 패널이 다음 글자를 가리고 사람이 노브로 맞히게 하면, 맞히기까지 고른 횟수가 영어 한 글자의 엔트로피 어림이 돼요. 라인하르트 될(Reinhard Döhl)의 「Apfel」(1965)은 Apfel 로 채운 사과 꼴에 Wurm 하나를 숨긴 구체시라, Silencio 처럼 노브가 그 낱말의 자리를 옮기게 해 볼 만해요. 빅토르 흘룸스키(Viktor Chlumský)의 다채널 거리장(2015)은 거리장 셋의 가운데 값으로 모서리를 지켜서, 같은 텍셀 수로 Distance Field 의 둥글어진 W 와 견줄 수 있어요. 윌리엄 문(William Moon)이 1845년에 만든 문 활자는 로마자를 단순한 돋은 선으로 줄인 글자라, Braille Relief 의 램프 아래에서 점 대신 선의 그림자가 글자를 그려요. 공병우의 세벌식 타자기(1949)가 찍던 탈네모꼴 글줄을 글쇠 차례대로 다시 찍으면, 글꼴이 아니라 기계가 만든 탈네모꼴이 Hanging Line 과 나란히 서요. ### 화장품 팝업 From 7af462e8da96b0d2bf337cd6cb0a69af06f38e6d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:06:47 +0000 Subject: [PATCH 18/19] =?UTF-8?q?incoming:=20index.html=20=EC=9D=84=20568?= =?UTF-8?q?=EA=B0=9C=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20=EC=A7=93=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gdqi6QfgbteLgPSf6papxo --- incoming/pattern-study/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/incoming/pattern-study/index.html b/incoming/pattern-study/index.html index 32227b9c0..2cbfb6fbc 100644 --- a/incoming/pattern-study/index.html +++ b/incoming/pattern-study/index.html @@ -210,8 +210,8 @@

패턴 연구

- - + + - + +