Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bbfe1a2
시작 — 패턴 연구 50판
claude Oct 25, 2026
4b8bfcf
패턴: Bak-Sneppen 를 짓는다
claude Oct 25, 2026
5cc8a94
패턴: Old Well 를 짓는다
claude Oct 25, 2026
b586441
패턴: Murmuration 를 짓는다
claude Oct 25, 2026
14f5866
패턴: Skull Swap 를 짓는다
claude Oct 25, 2026
53c062b
패턴: Skull Swap 의 던지는 높이를 판 안으로 줄이고 노브 설명을 잰 시각에 맞춘다
claude Oct 25, 2026
4f68861
일꾼 셋째 갈래를 합친다: Bak-Sneppen, Murmuration
claude Oct 25, 2026
a79800a
패턴: In the Flash 를 짓는다
claude Oct 25, 2026
c61083a
패턴: Paper Lantern Ghost 를 짓는다
claude Oct 25, 2026
0c882a5
패턴: In the Flash 의 형체가 번쩍임 사이 어둠에 남지 않게 하고 두 패턴의 노브 설명을 잰 값에 맞춘다
claude Oct 25, 2026
b2831df
일꾼 다섯째 갈래를 합친다: Skull Swap, Paper Lantern Ghost
claude Oct 25, 2026
2581dd6
패턴: Old Well 의 물 장면을 줄이고 두레박을 빨리 감아 2~8초 안에 두레박이 46줄까지 오르게 한다
claude Oct 25, 2026
49c91c0
패턴: Old Well 의 빛 셈을 표로 바꾸고 픽셀마다 만들던 배열을 없앤다
claude Oct 25, 2026
1b1ed6f
패턴: Old Well 의 머리 글을 줄인다
claude Oct 25, 2026
beb048e
일꾼 넷째 갈래를 합친다: Old Well, In the Flash
claude Oct 25, 2026
feb6d0e
패턴: Motility Clusters 를 짓는다
claude Oct 25, 2026
9c0326e
패턴: Inverse Cascade 를 짓는다
claude Oct 25, 2026
4193192
패턴: Risset Rhythm 을 짓는다
claude Oct 25, 2026
ecae7c9
패턴: Diffusion Front 를 짓는다
claude Oct 25, 2026
e621dd5
패턴: Diffusion Front 의 쓰지 않는 값을 덜어 낸다
claude Oct 25, 2026
aa820e9
패턴: Risset Rhythm 의 처음 종 폭 값을 노브와 맞춘다
claude Oct 25, 2026
76813df
패턴: Inverse Cascade 의 스펙트럼 고리를 2/3 안 모드로 줄인다
claude Oct 25, 2026
8ab3a53
패턴: Risset Rhythm 의 Hue 설명을 실제 색에 맞춘다
claude Oct 25, 2026
f0b13fd
패턴: Motility Clusters 의 가장자리 색 설명을 고친다
claude Oct 25, 2026
4a05dd8
패턴: Inverse Cascade 의 노브 설명을 잰 값으로 고친다
claude Oct 25, 2026
b787fd6
패턴: Motility Clusters 의 주석 한 줄을 고친다
claude Oct 25, 2026
6abc12e
일꾼 둘째 갈래를 합친다: Motility Clusters, Risset Rhythm
claude Oct 25, 2026
736790e
일꾼 첫째 갈래를 합친다: Inverse Cascade, Diffusion Front
claude Oct 25, 2026
5f75ed7
연구 노트에 50판 열 개를 적는다
claude Oct 25, 2026
01cdb4a
dev 를 합친다: 49판이 먼저 들어가서 README 의 50판 줄을 49판 뒤에 둔다
claude Oct 25, 2026
0461b80
연구 페이지를 50판까지 다시 짓는다
claude Oct 25, 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
83 changes: 79 additions & 4 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.

157 changes: 157 additions & 0 deletions incoming/pattern-study/patterns/bak-sneppen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// ===== Patternflow pattern =====
// Title: Bak-Sneppen
// Family: data
// Idea: Sixty-four species stand on a ring as bars of fitness, and every step the least fit one and its two neighbours are redrawn at random, so the floor of the bars climbs by itself to a threshold near two thirds while fresh colours break out in avalanches around the hopping minimum.
// Author: patternflow-lab
// Date: 2026-10-25
// SPDX-License-Identifier: CC-BY-SA-4.0
// Lineage: Bak-Sneppen model of evolution (Per Bak and Kim Sneppen, 1993), with its random-neighbour version (Henrik Flyvbjerg, Kim Sneppen and Per Bak, 1993), pattern round 50
// Round: 50
// ===============================
// @matrix 64x128
// @knobs Hue=0..1, Speed=0.2..3, Random=0..1, Memory=0..1
//
// Knob 1: Hue. Turns the colours of age round the wheel together. At 0 a bar just redrawn is pale gold, then orange, rose and violet as it waits, and a bar left alone for long is deep blue; the gap line and the marked minimum are aqua. At 0.25 fresh bars are lime and old ones crimson; at 0.5 fresh is aqua and old is gold; at 0.75 fresh is pink and old is green.
// Knob 2: Speed. Steps of the model a second, 25 times the knob, so 50 at 2. Each fresh start first runs a climb: 6,000 steps in 1.8 s, slow at first and faster and faster, so the gap line rises evenly from the bottom to 0.645 on average; then the model walks at the knob's pace for 40 s and the bars are thrown again. At 0.2 everything is ten times slower, at 3 half as fast again.
// Knob 3: Random. The chance that each of the two neighbours redrawn with the minimum is a species picked anywhere on the ring instead of the one beside it. At 0 the classic ring: the floor settles at 0.62 for 64 species, just under the 0.667 tick of the infinite ring, and avalanches stay local. At 0.25 the floor drops to 0.42, at 0.5 to 0.37, and at 1, the random-neighbour model, to 0.35, just over the 1/3 tick, and fresh bars flicker all over the ring.
// Knob 4: Memory. How many steps a bar takes to turn from fresh to old, on a log scale of its age: 150 steps at 0, 184 at the opening 0.06, 820 at 0.5 and 4,500 at 1. Short memory shows only the avalanche of the last second or two as a bright patch on a blue block; long memory paints each bar by how long ago it was touched, so old avalanches stay as bands of rose and violet.
// Buttons: K1 tap throws all 64 bars again from a uniform start and the climb runs anew.

const W = 64, H = 128, N = 64;
const { floor, round, min, max, abs, log, pow } = Math;
const cl = (v, a, b) => (v < a ? a : v > b ? b : v);

// The climb: CMAX steps in TC seconds at Speed 2, the step count growing as
// CMAX^u, so the gap, which rises about 0.05 for every doubling of the steps,
// rises evenly. Then RATE * Speed steps a second for CYCLE seconds at Speed 2.
const CMAX = 6000, TC = 1.8, RATE = 25, CYCLE = 40;
// The threshold of the infinite ring (Paczuski, Maslov and Bak, 1996) and of
// the random-neighbour model (Flyvbjerg, Sneppen and Bak, 1993).
const FC = 0.66702, FMF = 1 / 3;

function hsv(h, s, v, out, o) {
h = (h - floor(h)) * 6;
const i = floor(h) % 6, f = h - floor(h), p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s);
out[o] = (i === 0 || i === 5 ? v : i === 1 ? q : i === 4 ? t : p) * 255;
out[o + 1] = (i === 1 || i === 2 ? v : i === 0 ? t : i === 3 ? q : p) * 255;
out[o + 2] = (i === 3 || i === 4 ? v : i === 2 ? t : i === 5 ? q : p) * 255;
}

export function setup(params) {
params.s = {
hue: 0, speed: 2, rnd: 0, mem: 0.06,
f: new Float32Array(N), born: new Float64Array(N), step: 0, m: 0, G: 0,
ph: "climb", pt: 0, acc: 0,
// 64 colours of age, plus the gap line, its dim twin, the ticks and the
// marked minimum.
pal: new Float32Array(70 * 3), palKey: "",
};
throwBars(params.s);
}

// A uniform start: every species gets a fitness drawn at random and counts
// as old, so the bars the climb redraws stand out from the first step.
function throwBars(S) {
for (let i = 0; i < N; i++) (S.f[i] = Math.random()), (S.born[i] = -1e9);
S.step = 0;
S.G = 0;
S.ph = "climb";
S.pt = 0;
S.acc = 0;
S.m = argmin(S.f);
}

function argmin(f) {
let m = 0;
for (let i = 1; i < N; i++) if (f[i] < f[m]) m = i;
return m;
}

// One step of the model: the least fit species and two others get new
// fitnesses, and the gap, the largest minimum seen since the start, may rise.
function step(S) {
const f = S.f, m = S.m;
if (f[m] > S.G) S.G = f[m];
S.step++;
const a = Math.random() < S.rnd ? floor(Math.random() * N) : (m + N - 1) % N;
const b = Math.random() < S.rnd ? floor(Math.random() * N) : (m + 1) % N;
for (const i of [m, a, b]) (f[i] = Math.random()), (S.born[i] = S.step);
S.m = argmin(f);
}

export function update(dt, input, params) {
if (!params.s) setup(params);
const S = params.s, k = input.knobValues || [0, 2, 0, 0.06];
S.hue = cl(k[0], 0, 1);
S.speed = cl(k[1], 0.2, 3);
S.rnd = cl(k[2], 0, 1);
S.mem = cl(k[3], 0, 1);
const d = cl(dt || 0, 0, 0.1), v = S.speed / 2;
if (input.btnPressed && input.btnPressed[0]) throwBars(S);
S.pt += d * v;
if (S.ph === "climb") {
const u = min(1, S.pt / TC), target = round(pow(CMAX, u)) - 1;
while (S.step < target) step(S);
if (u >= 1) (S.ph = "walk"), (S.pt = 0);
} else {
S.acc += d * RATE * S.speed;
while (S.acc >= 1) step(S), (S.acc -= 1);
if (S.pt > CYCLE) throwBars(S);
}
}

// Colours of age: index 0 is a bar redrawn this step, 63 a bar older than
// the memory. Then 64 the gap line, 65 the line's dim twin over the dark,
// 66 the threshold ticks, 67 the marked minimum, 68 the pointer above it.
function palette(S) {
const key = S.hue + ":" + S.mem;
if (key === S.palKey) return;
S.palKey = key;
const P = S.pal, h = S.hue + 0.14;
for (let i = 0; i < 64; i++) {
const u = i / 63;
// Warm white, gold, orange, rose and violet to deep blue; the value
// stays above 0.55, and old ages near orange and yellow stay brighter
// still, so no age turns brown or olive whatever the hue.
const hh = h - 0.48 * pow(u, 0.8), dw = abs(hh - floor(hh) - 0.11), warm = max(0, 1 - min(dw, 1 - dw) / 0.14);
hsv(hh, 0.4 + 0.48 * min(1, u * 3), min(1, (1 - 0.45 * u) * (1 + 0.55 * warm * u)), P, i * 3);
}
hsv(h + 0.38, 0.3, 1, P, 64 * 3);
hsv(h + 0.38, 0.45, 0.6, P, 65 * 3);
hsv(h, 0.05, 1, P, 66 * 3);
hsv(h + 0.38, 0.55, 1, P, 67 * 3);
hsv(h + 0.38, 0.6, 0.45, P, 68 * 3);
}

const row = (f) => cl(round(H - f * H), 0, H - 1);

export function draw(display, params) {
const S = params.s;
if (!S) return;
palette(S);
const P = S.pal, f = S.f, born = S.born, step = S.step;
// Ages on a log scale up to the memory, 150 to 4,500 steps.
const M = 150 * pow(30, S.mem), lm = log(1 + M);
const yG = S.G > 0 ? row(S.G) : -1, y1 = row(FC), y2 = row(FMF);
for (let x = 0; x < W; x++) {
const top = row(f[x]), mark = x === S.m;
const c = min(63, round((63 * log(1 + step - born[x])) / lm)) * 3;
// Every other bar a shade darker, so 64 bars read as 64 and not a block.
const sh = x & 1 ? 0.86 : 1;
for (let y = 0; y < H; y++) {
let o = -1, s = 1;
if (y >= top) {
o = mark ? 67 * 3 : c;
s = y === top ? 1.12 : sh;
} else if (mark) o = 68 * 3;
if (y === yG) o = y >= top ? 64 * 3 : 65 * 3;
if ((y === y1 || y === y2) && (x < 3 || x > W - 4)) o = 66 * 3;
if (o < 0) {
display.setPixel(x, y, 0, 0, 0);
continue;
}
// Channels in steps of 4 of 255: too fine to see, and a frame packs smaller.
display.setPixel(x, y, min(255, round((P[o] * s) / 4) * 4), min(255, round((P[o + 1] * s) / 4) * 4), min(255, round((P[o + 2] * s) / 4) * 4));
}
}
}
181 changes: 181 additions & 0 deletions incoming/pattern-study/patterns/diffusion-front.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// ===== Patternflow pattern =====
// Title: Diffusion Front
// Family: fractal
// Idea: Every LED is a lattice site with a fixed random number and is occupied while that number is below the concentration of a dye diffusing up from the bottom row, erfc(h / 2 sqrt(Dt)); the occupied sites joined to the source glow pale cyan to deep blue with the concentration, a site that has just joined flashes white, islands not joined glow violet, and the cluster's outer boundary, the diffusion front, is drawn gold: a fractal hull that climbs as the square root of time and sits where the concentration is 0.5927, the percolation threshold, until the source shuts, the cluster is cut from it, the dye drains out and a fresh lattice starts.
// Author: patternflow-lab
// Date: 2026-10-25
// SPDX-License-Identifier: CC-BY-SA-4.0
// Lineage: diffusion fronts and gradient percolation (Bernard Sapoval, Michel Rosso and Jean-François Gouyet, 1985), pattern round 50
// Round: 50
// ===============================
// @matrix 64x128
// @knobs Hue=0..1, Speed=0.2..3, Hop=0..1, Islands=0..1
//
// Knob 1: Hue. Turns every colour round the wheel together. At 0 the cluster runs from pale cyan at the front to deep blue at the source, the front is gold and the islands violet; at 0.25 the cluster runs from grey violet to magenta, the front is mint and the islands brick red; at 0.5 the cluster runs from brown to amber, the front is pale blue and the islands olive; at 0.75 the cluster is green, the front pink and the islands teal.
// Knob 2: Speed. At 2 the front is 28 rows up after a quarter of a second, 55 after one second, 78 after two and 121 after 4.8, as the square root of time; it holds half a second, the source shuts and the dye drains for a second, so a run lasts 6.3 seconds. At 0.2 everything is ten times slower, at 3 half as fast again.
// Knob 3: Hop. How often each site draws a new random number. At 0 the lattice is frozen, the gradient percolation of one fixed lattice, and the front only climbs; at 0.5 each site draws again once a second on average and at 1 twice, so the occupied sites flicker like hopping particles, islands join the cluster and fall away, and the front shivers as it climbs.
// Knob 4: Islands. How bright the occupied sites that are not joined to the source glow. At 0 they are dark and only the cluster joined to the source shows; at the opening 0.06 they glow violet at 113 of 255, at 0.25 at 196 and at 1 at 237, nearly as bright as the front.
// Buttons: K1 tap starts a fresh lattice.

const W = 64, H = 128, N = W * H;
const { floor, exp, sqrt, abs, pow } = Math;
const cl = (v, a, b) => (v < a ? a : v > b ? b : v);

// erfc(x) to 1.2e-7 (Numerical Recipes' Chebyshev fit).
function erfc(x) {
const z = abs(x), t = 1 / (1 + 0.5 * z);
const r = t * exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.37409196 + t * (0.09678418 + t * (-0.18628806 + t * (0.27886807 + t * (-1.13520398 + t * (1.48851587 + t * (-0.82215223 + t * 0.17087277)))))))));
return x >= 0 ? r : 2 - r;
}

// The front sits where p = 0.5927, at h = 0.3778 L with L = 2 sqrt(D t).
// A run grows L from L0 to L1 in GROW seconds (at Speed 2) with L^2 growing
// evenly in time, holds, shuts the source and drains for DRAIN seconds, then
// starts again on a fresh lattice.
const L1 = 320, L0 = 10, GROW = 4.8, HOLD = 0.5, DRAIN = 1.0, PC = 0.5927;

export function setup(params) {
params.s = {
hue: 0, speed: 2, hop: 0, isl: 0.06, c: 0, L: L0, runs: 0,
u: new Float32Array(N), occ: new Uint8Array(N), A: new Uint8Array(N), B: new Uint8Array(N), hull: new Uint8Array(N),
join: new Float32Array(N), q: new Int32Array(N), p: new Float64Array(H), glow: new Float32Array(N * 3), ease: 1,
// Front sites and their mean height in rows, kept for checking the
// front against the threshold.
nHull: 0, hf: 0,
};
fresh(params.s);
}

function fresh(S) {
for (let i = 0; i < N; i++) S.u[i] = Math.random();
S.join.fill(-1);
S.c = 0;
S.runs++;
}

// Occupancy from p(h), the cluster joined to the source row (4-neighbour),
// the empty sites joined to the open top (8-neighbour), and the hull: the
// cluster's sites that touch that open emptiness.
function percolate(S) {
const { u, occ, A, B, hull, q, p } = S;
for (let y = 0; y < H; y++) {
const py = p[y], o = y * W;
for (let x = 0; x < W; x++) occ[o + x] = u[o + x] < py ? 1 : 0;
}
A.fill(0);
let t = 0;
for (let x = 0, o = (H - 1) * W; x < W; x++) if (occ[o + x]) (A[o + x] = 1), (q[t++] = o + x);
for (let k = 0; k < t; k++) {
const i = q[k], x = i % W;
if (x > 0 && occ[i - 1] && !A[i - 1]) (A[i - 1] = 1), (q[t++] = i - 1);
if (x < W - 1 && occ[i + 1] && !A[i + 1]) (A[i + 1] = 1), (q[t++] = i + 1);
if (i >= W && occ[i - W] && !A[i - W]) (A[i - W] = 1), (q[t++] = i - W);
if (i + W < N && occ[i + W] && !A[i + W]) (A[i + W] = 1), (q[t++] = i + W);
}
B.fill(0);
t = 0;
for (let x = 0; x < W; x++) if (!occ[x]) (B[x] = 1), (q[t++] = x);
for (let k = 0; k < t; k++) {
const i = q[k], x = i % W, y = (i - x) / W;
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 j = yy * W + xx;
if (!occ[j] && !B[j]) (B[j] = 1), (q[t++] = j);
}
}
}
let nh = 0, sh = 0;
for (let i = 0; i < N; i++) {
let f = 0;
if (A[i]) {
const x = i % W;
f = i < W || (x > 0 && B[i - 1]) || (x < W - 1 && B[i + 1]) || B[i - W] || (i + W < N && B[i + W]) ? 1 : 0;
if (f) (nh++), (sh += H - 1 - (i - x) / W);
}
hull[i] = f;
}
S.nHull = nh;
S.hf = nh ? sh / nh : 0;
}

export function update(dt, input, params) {
if (!params.s) setup(params);
const S = params.s, k = input.knobValues || [0, 2, 0, 0.06];
S.hue = cl(k[0], 0, 1);
S.speed = cl(k[1], 0.2, 3);
S.hop = cl(k[2], 0, 1);
S.isl = cl(k[3], 0, 1);
const d = (cl(dt || 0, 0, 0.1) * S.speed) / 2;
S.ease = 1 - exp(-d / 0.08);
if (input.btnPressed && input.btnPressed[0]) fresh(S);
S.c += d;
if (S.c > GROW + HOLD + DRAIN) fresh(S);
// L^2 = 4 D t grows evenly; once the source shuts at GROW + HOLD, the
// profile is the same erfc less one started at the moment of shutting
// (the source row held at zero from then on), so the source row empties
// at once and the rest drains out through it.
const K = (L1 * L1 - L0 * L0) / GROW, ts = GROW + HOLD;
S.L = sqrt(L0 * L0 + K * S.c);
const Ls = S.c > ts ? sqrt(K * (S.c - ts)) : 0;
for (let y = 0; y < H; y++) {
const h = H - 1 - y;
S.p[y] = erfc(h / S.L) - (Ls > 0 ? erfc(h / Ls) : 0);
}
// Hop: each site draws a new random number at this rate a second.
if (S.hop > 0) {
const r = 1 - exp(-2 * S.hop * d);
for (let i = 0; i < N; i++) if (Math.random() < r) S.u[i] = Math.random();
}
percolate(S);
const { A, join } = S;
for (let i = 0; i < N; i++) {
if (A[i]) {
if (join[i] < 0) join[i] = S.c;
} else join[i] = -1;
}
}

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 ? [v, t, p] : i === 1 ? [q, v, p] : i === 2 ? [p, v, t] : i === 3 ? [p, q, v] : i === 4 ? [t, p, v] : [v, p, q];
}

export function draw(display, params) {
const S = params.s;
if (!S) return;
const { occ, A, hull, join, p, glow } = S;
const hu = S.hue;
const front = hsv(hu + 0.12, 0.35, 1), flash = hsv(hu + 0.5, 0.05, 1), isl = hsv(hu + 0.78, 0.7, 1);
const il = (255 * S.isl) / (S.isl + 0.075);
for (let y = 0; y < H; y++) {
// The cluster is lit by the concentration where it stands: dim at the
// front, where p is near the threshold, bright near the source.
const sy = pow(cl((p[y] - PC) / (1 - PC), 0, 1), 0.8), lv = 255 * (0.45 + 0.55 * sy);
const c = hsv(hu + 0.5 + 0.12 * sy, 0.4 + 0.5 * sy, 1);
for (let x = 0; x < W; x++) {
const i = y * W + x;
let r = 0, g = 0, b = 0;
if (hull[i]) (r = 255 * front[0]), (g = 255 * front[1]), (b = 255 * front[2]);
else if (A[i]) {
// A site that has just joined flashes pale and settles in half a second.
const a = cl((S.c - join[i]) / 0.5, 0, 1), v = 255 * (1 - a), w = lv * a;
r = flash[0] * v + c[0] * w;
g = flash[1] * v + c[1] * w;
b = flash[2] * v + c[2] * w;
} else if (occ[i]) (r = il * isl[0]), (g = il * isl[1]), (b = il * isl[2]);
// Each LED eases towards its colour in about a tenth of a second, so a
// cluster cut from the source sinks to island colour instead of
// switching.
const o = i * 3, e = S.ease;
glow[o] += (r - glow[o]) * e;
glow[o + 1] += (g - glow[o + 1]) * e;
glow[o + 2] += (b - glow[o + 2]) * e;
display.setPixel(x, y, glow[o], glow[o + 1], glow[o + 2]);
}
}
}
Loading
Loading