Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f80c281
시작 — 패턴 연구 54판
claude Oct 26, 2026
e59260a
54판: stormer-trap 짓기
claude Oct 26, 2026
0c9c1bd
54판: nodal-domains 짓기
claude Oct 26, 2026
39e3f8f
54판: pearcey-cusp 짓기
claude Oct 26, 2026
011b983
54판: weaire-phelan 짓기
claude Oct 26, 2026
2bbede1
54판: arcsine-law 짓기
claude Oct 26, 2026
8da1f45
54판: smale-horseshoe 짓기
claude Oct 26, 2026
ded9be6
54판: smale-horseshoe 다듬기
claude Oct 26, 2026
0b574e2
54판: stable-marriage 짓기
claude Oct 26, 2026
765a759
54판: arcsine-law 주석 다듬기
claude Oct 26, 2026
a99863b
54판: strange-eigenmode 짓기
claude Oct 26, 2026
c501f96
Merge branch 'claude/lsh-pfpat-round52-mdkcif-a' into claude/lsh-pfpa…
claude Oct 26, 2026
49d56d6
Merge branch 'claude/lsh-pfpat-round52-mdkcif-c' into claude/lsh-pfpa…
claude Oct 26, 2026
6fc4c49
54판: stit-tessellation 짓기
claude Oct 26, 2026
1ecdb4a
Merge branch 'claude/lsh-pfpat-round52-mdkcif-d' into claude/lsh-pfpa…
claude Oct 26, 2026
22da0f7
54판: ulam-sequence 짓기
claude Oct 26, 2026
13ddef7
54판: strange-eigenmode 노브 글 다듬기
claude Oct 26, 2026
eedf32b
Merge branch 'claude/lsh-pfpat-round52-mdkcif-b' into claude/lsh-pfpa…
claude Oct 26, 2026
748662d
54판: arcsine-law 동전 결을 밝기로 그려 촘촘함 × 모양 바뀜 4,3 에 세우기
claude Oct 26, 2026
71cd313
Merge branch 'claude/lsh-pfpat-round52-mdkcif-a' into claude/lsh-pfpa…
claude Oct 26, 2026
010716b
54판: README 연구 노트, Ulam Sequence 의 Idea 줄 셋 나열 고치기
claude Oct 26, 2026
55aa6af
54판: index.html 을 438개로 다시 짓고 노브 문단에 네 자리 값
claude Oct 26, 2026
516c56a
54판: 모양 바뀜 칸 경계 값을 0.214 로 바로잡기
claude Oct 26, 2026
179b188
dev 를 merge: 53판(#264)이 먼저 머지돼 README 는 dev 판에 54판 줄을 다시 얹음
claude Oct 26, 2026
8e56200
54판: index.html 을 53판까지 든 448개로 다시 짓기
claude Oct 26, 2026
3f0cf5c
dev 를 merge: 55판(#266)이 먼저 머지돼 README 의 54판 줄을 53판과 55판 사이에 다시 얹음
claude Oct 26, 2026
04312e4
54판: index.html 을 55판까지 든 458개로 다시 짓기
claude Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 72 additions & 3 deletions incoming/pattern-study/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions incoming/pattern-study/index.html

Large diffs are not rendered by default.

311 changes: 311 additions & 0 deletions incoming/pattern-study/patterns/arcsine-law.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
// ===== Patternflow pattern =====
// Title: Arcsine Law
// Family: data
// Idea: 48 fair random walks come down the panel a row a step, each LED one toss of the coin, bright for a step up and dark for a step down, coloured warm while its walk is above zero and cool below, so the tosses look like noise while most columns run in long stretches of one colour; after 126 steps the warm cells rise to the top of each column and the columns sort themselves by how many there are, so the boundary between the colours draws the arcsine quantile curve, while a histogram beside them gathers every walk so far into Lévy's U-shaped law, its exact values marked by white dots.
// Author: patternflow-lab
// Date: 2026-10-26
// SPDX-License-Identifier: CC-BY-SA-4.0
// Lineage: arcsine laws (Paul Lévy, 1939)
// Round: 54
// ===============================
// @matrix 64x128
// @knobs Hue=0..1, Speed=0.2..3, Statistic=0..2, Drift=-1..1
//
// Knob 1: Hue. Turns every colour around the wheel together. At 0 a walk above zero is amber and below zero azure, pale near zero and deeper the further out it is, a step down drawn at a third of the brightness of a step up, with lavender histogram bars; at 0.25 green above and magenta below, at 0.5 the two sides trade colours.
// Knob 2: Speed. 20 rows a second per unit: 40 at the opening 2, so a batch of 126 steps comes down in 3.2 s, settles and sorts in 1.4 s and holds for 1.4 s; at 0.2 a batch takes 32 s to come down, at 3 it takes 2.1 s. Walks that are not drawn feed the histogram at 120 a second per unit.
// Knob 3: Statistic. Which of Lévy's three the columns sort by and the histogram counts. At 0 the time above zero: the warm cells rise to the top of their column. At 1 the last zero: the cells before each walk's last return to zero are dimmed, leaving a bright tail of one colour below it. At 2 the first time of the maximum: the cells after each walk's highest point so far are dimmed. All three histograms settle on the same U.
// Knob 4: Drift. A step goes up with chance (1 + mu) / 2. mu is 0 within 0.08 of the middle, so the opening 0.06 is the fair walk, then grows as the square to 0.014 at 0.3, 0.052 at 0.5 and 0.25 at either end, where nearly every column floods one colour. Any drift breaks the law: at 0.5 the histogram of time above zero lies 0.19 from it in Kolmogorov distance, while the last zero, which only asks whether the walk comes back, lies 0.051 from it.

const W = 64, H = 128;
// 48 walks of 126 steps, one a column, step t drawn in row t - 1; then a
// dark column and the histogram, 15 columns wide, its bins two rows tall on
// the same time axis as the walks.
const NW = 48, N = 126, HX = 49, HL = 15, NB = 64;
const { floor, round, min, max, abs, sqrt, sin, PI } = Math;
const cl = (v, a, b) => (v < a ? a : v > b ? b : v);
const ease = (u) => (u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u));

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) * 255;
o[1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255;
o[2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255;
return o;
}

// The exact laws for N = 126 steps, bin k of the histogram. The time spent
// above zero (counting a step as above when either end of it is) and the
// last zero both take the value 2k with probability u(2k) u(N - 2k), where
// u(2m) = C(2m, m) / 4^m is the chance of being at zero at time 2m: the
// discrete arcsine law of Feller's textbook (1950). The first time of the
// maximum is k with probability a(k) b(N - k): a(k) the chance a walk stays
// above zero for k steps, b(m) the chance it stays at or below zero for m,
// both by following the walk step by step. Lévy's arcsine law is the limit
// of all three.
const U = new Float64Array(N + 1);
U[0] = 1;
for (let m = 1; 2 * m <= N; m++) U[2 * m] = (U[2 * m - 2] * (2 * m - 1)) / (2 * m);
function stay(strict) {
// out[k]: chance that S_1..S_k all stay above zero (strict) or at or
// below zero, by the distribution of the walk killed when it leaves.
const out = new Float64Array(N + 1), off = N + 1;
let p = new Float64Array(2 * N + 3), q = new Float64Array(2 * N + 3);
p[off] = 1;
out[0] = 1;
for (let k = 1; k <= N; k++) {
q.fill(0);
let tot = 0;
for (let x = 1; x < 2 * N + 2; x++) {
const v = 0.5 * (p[x - 1] + p[x + 1]);
const y = x - off;
if (strict ? y > 0 : y <= 0) (q[x] = v), (tot += v);
}
[p, q] = [q, p];
out[k] = tot;
}
return out;
}
const PB = [new Float64Array(NB), new Float64Array(NB), new Float64Array(NB)];
{
const a = stay(true), b = stay(false);
for (let k = 0; k < NB; k++) PB[0][k] = PB[1][k] = U[2 * k] * U[N - 2 * k];
for (let t = 0; t <= N; t++) PB[2][t >> 1] += a[t] * b[N - t];
}
// The histogram's scale puts the third bin from either end at full length,
// so the bins at the very ends run off the band.
const PFULL = PB[0][2];

export function setup(params) {
params.s = {
hue: 0, speed: 2, stat: 0, mu: 0, init: false,
// The batch on show: positions S[i * (N + 1) + t].
S: new Int16Array(NW * (N + 1)), row: 0, phase: 0, clock: 0,
pos: new Int16Array(NW), lz: new Int16Array(NW), tm: new Int16Array(NW), top: new Int16Array(NW),
// Sorting: each walk's target column and each cell's target row.
X: new Int16Array(NW), R: new Int16Array(NW * N), sortStat: -1,
// The last sorted picture, shown on every other LED below the new
// batch's front.
prev: new Float32Array(NW * N * 3),
hist: [new Float64Array(NB), new Float64Array(NB), new Float64Array(NB)], walks: 0, histMu: 0,
fb: new Float32Array(W * H * 3), c: [0, 0, 0],
};
}

// One walk's three statistics from its positions w[0..N].
function stats(w, o, out) {
let pos = 0, lz = 0, tm = 0, top = 0;
for (let t = 1; t <= N; t++) {
const a = w[o + t - 1], b = w[o + t];
if (a + b > 0) pos++;
if (b === 0) lz = t;
if (b > top) (top = b), (tm = t);
}
out[0] = pos;
out[1] = lz;
out[2] = tm;
}

const tmpW = new Int16Array(N + 1), tmpS = [0, 0, 0];
function hiddenWalk(S) {
const p = (1 + S.mu) / 2;
tmpW[0] = 0;
for (let t = 1; t <= N; t++) tmpW[t] = tmpW[t - 1] + (Math.random() < p ? 1 : -1);
stats(tmpW, 0, tmpS);
addStats(S, tmpS);
}
function addStats(S, st) {
S.hist[0][st[0] >> 1]++;
S.hist[1][st[1] >> 1]++;
S.hist[2][st[2] >> 1]++;
S.walks++;
}

function stepRow(S) {
const t = S.row + 1, p = (1 + S.mu) / 2, A = S.S;
for (let i = 0; i < NW; i++) {
const o = i * (N + 1), b = (A[o + t] = A[o + t - 1] + (Math.random() < p ? 1 : -1)), a = A[o + t - 1];
if (a + b > 0) S.pos[i]++;
if (b === 0) S.lz[i] = t;
if (b > S.top[i]) (S.top[i] = b), (S.tm[i] = t);
}
S.row = t;
}

function newBatch(S) {
S.S.fill(0);
S.pos.fill(0);
S.lz.fill(0);
S.tm.fill(0);
S.top.fill(0);
S.row = 0;
S.phase = 0;
S.clock = 0;
}

// The statistic each walk is sorted by, and where each cell goes: for the
// time above zero the cells above zero rise to the top of their column in
// their order and the rest sink below them; for the other two the cells
// stay in their rows.
function plan(S) {
const st = S.stat === 0 ? S.pos : S.stat === 1 ? S.lz : S.tm, A = S.S;
const idx = [...Array(NW).keys()].sort((a, b) => st[a] - st[b] || A[a * (N + 1) + N] - A[b * (N + 1) + N] || a - b);
idx.forEach((w, r) => (S.X[w] = r));
for (let i = 0; i < NW; i++) {
const o = i * (N + 1);
let up = 0, down = S.pos[i];
for (let t = 1; t <= N; t++) {
const above = A[o + t - 1] + A[o + t] > 0;
S.R[i * N + t - 1] = S.stat === 0 ? (above ? up++ : down++) : t - 1;
}
}
S.sortStat = S.stat;
}

// A cell is one toss of the coin: bright when the step went up, dark when
// it went down, so the tosses read as noise. Its hue is the sign of the
// running total, warm above zero and cool below, paler near zero and
// deeper the further the walk is out in units of sqrt(t), and that hue
// runs in long stretches. Cells are dimmed before the last zero or after
// the maximum when those are the statistic.
function shade(S, i, t, out) {
const o = i * (N + 1), a = S.S[o + t - 1], b = S.S[o + t], m = min(1, abs(a + b) / (2.2 * sqrt(t))), up = b > a;
if (a + b > 0) hsv(0.11 + S.hue, 0.3 + 0.7 * m, up ? 1 : 0.3, out);
else hsv(0.6 + S.hue, 0.3 + 0.65 * m, up ? 0.85 : 0.25, out);
let dim = false;
if (S.stat === 1) dim = t <= S.lz[i];
else if (S.stat === 2) dim = t > S.tm[i];
if (dim) {
const g = (out[0] + out[1] + out[2]) / 3;
for (let c = 0; c < 3; c++) out[c] = 0.2 * (0.4 * out[c] + 0.6 * g);
}
return out;
}

export function update(dt, input, params) {
if (!params.s) setup(params);
const S = params.s, kv = input.knobValues || [0, 2, 0, 0.06], d = cl(dt || 0, 0, 0.1);
S.hue = kv[0];
S.speed = cl(kv[1], 0.2, 3);
S.stat = cl(round(kv[2]), 0, 2);
// Drift: none within 0.08 of the middle, then growing as the square to
// 0.25 at either end; the chance of a step up is (1 + mu) / 2.
const k4 = cl(kv[3], -1, 1), e = max(0, (abs(k4) - 0.08) / 0.92);
S.mu = (k4 < 0 ? -1 : 1) * 0.25 * e * e;
if (S.mu !== S.histMu) {
for (const h of S.hist) h.fill(0);
S.walks = 0;
S.histMu = S.mu;
}
if (!S.init) {
// The first batch comes down a dark panel; each later one comes down
// over the sorted picture of the one before.
S.init = true;
newBatch(S);
}
// Walks nobody sees feed the histogram, 8 a frame at Speed 2.
S.hidden = (S.hidden || 0) + (d * 30 * 8 * S.speed) / 2;
while (S.hidden >= 1) hiddenWalk(S), S.hidden--;
const rate = 20 * S.speed, slow = sqrt(2 / S.speed);
S.clock += d;
if (S.phase === 0) {
const want = min(N, floor(S.clock * rate));
while (S.row < want) stepRow(S);
if (S.row >= N) {
for (let i = 0; i < NW; i++) addStats(S, [S.pos[i], S.lz[i], S.tm[i]]);
plan(S);
S.phase = 1;
S.clock = 0;
}
} else {
if (S.stat !== S.sortStat) plan(S), (S.clock = 0), (S.phase = 1);
if (S.phase === 1 && S.clock > 1.4 * slow) (S.phase = 2), (S.clock = 0);
if (S.phase === 2 && S.clock > 1.4 * slow) keep(S), newBatch(S);
}
}

// Store the sorted picture of the batch as the backdrop.
function keep(S) {
const P = S.prev, c = S.c;
P.fill(0);
for (let i = 0; i < NW; i++)
for (let t = 1; t <= N; t++) {
shade(S, i, t, c);
const q = (S.R[i * N + t - 1] * NW + S.X[i]) * 3;
P[q] = c[0];
P[q + 1] = c[1];
P[q + 2] = c[2];
}
}

export function draw(display, params) {
const S = params.s;
if (!S || !S.init) return;
const fb = S.fb, c = S.c, P = S.prev;
fb.fill(0);
if (S.phase === 0) {
// The old picture below the front on every other LED, the new walks
// above it.
for (let r = S.row; r < N; r++)
for (let x = (r + 1) & 1; x < NW; x += 2) {
const q = (r * W + x) * 3, s = (r * NW + x) * 3;
fb[q] = P[s];
fb[q + 1] = P[s + 1];
fb[q + 2] = P[s + 2];
}
for (let i = 0; i < NW; i++)
for (let t = 1; t <= S.row; t++) {
shade(S, i, t, c);
const q = ((t - 1) * W + i) * 3;
fb[q] = c[0];
fb[q + 1] = c[1];
fb[q + 2] = c[2];
}
} else {
// Settle first, then slide the columns into their order.
const u = S.phase === 2 ? 1 : S.clock / (1.4 * sqrt(2 / S.speed));
const ey = ease(u * 2), ex = ease(u * 2 - 1);
for (let i = 0; i < NW; i++) {
const x = round(i + (S.X[i] - i) * ex);
for (let t = 1; t <= N; t++) {
const y = round(t - 1 + (S.R[i * N + t - 1] - (t - 1)) * ey);
shade(S, i, t, c);
const q = (y * W + x) * 3;
fb[q] = c[0];
fb[q + 1] = c[1];
fb[q + 2] = c[2];
}
}
}
// Once the columns stand in order, a dot in each marks where Lévy's law
// puts that rank: column r of 48 at N sin^2(pi (r + 1/2) / 96), the
// arcsine quantile. The boundary of the batch should hug it.
if (S.phase === 2) {
const v = 0.85 * min(1, S.clock / 0.4);
for (let r = 0; r < NW; r++) {
const sn = sin((PI * (r + 0.5)) / (2 * NW)), y = min(N - 1, floor(N * sn * sn)), q = (y * W + r) * 3;
for (let j = 0; j < 3; j++) fb[q + j] += (255 - fb[q + j]) * v;
}
}
// The histogram of the chosen statistic over every walk so far, and the
// exact law as a dot at the length each bar should reach.
const h = S.hist[S.stat], pb = PB[S.stat], n = max(1, S.walks), sc = HL / PFULL;
for (let k = 0; k < NB; k++) {
const len = (h[k] / n) * sc, ex = pb[k] * sc;
for (let j = 0; j < HL; j++) {
const f = cl(len - j, 0, 1), y0 = 2 * k;
let r = 0, g = 0, b = 0;
// The bar, the shortfall from the law dimmer, and the law's dot.
const sf = j >= len && j < ex - 0.5 ? 0.3 : 0;
if (f > 0 || sf) hsv(0.8 + S.hue, 0.35, f > 0 ? 0.3 + 0.6 * f : sf, c), (r = c[0]), (g = c[1]), (b = c[2]);
if (abs(j + 0.5 - ex) < 0.5) (r = 255), (g = 255), (b = 255);
for (let y = y0; y < y0 + 2; y++) {
const q = (y * W + HX + j) * 3;
fb[q] = r;
fb[q + 1] = g;
fb[q + 2] = b;
}
}
}
for (let p = 0; p < W * H; p++) display.setPixel(p % W, (p / W) | 0, fb[p * 3] | 0, fb[p * 3 + 1] | 0, fb[p * 3 + 2] | 0);
}
Loading
Loading