const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const rgb = (cs.getPropertyValue('--accent-3').trim() || '#18c29c').match(/[0-9a-f]{2}/gi).map((x) => parseInt(x, 16));
const GW = 110, GH = 66;
const small = document.createElement('canvas');
small.width = GW; small.height = GH;
const sctx = small.getContext('2d');
const img = sctx.createImageData(GW, GH);
let U = new Float32Array(GW * GH).fill(1);
let V = new Float32Array(GW * GH).fill(0);
for (let k = 0; k < 6; k++) {
const cx = (0.2 + Math.random() * 0.6) * GW, cy = (0.2 + Math.random() * 0.6) * GH;
for (let y = 0; y < GH; y++) for (let x = 0; x < GW; x++) {
if ((x - cx) ** 2 + (y - cy) ** 2 < 16) { U[y * GW + x] = 0.5; V[y * GW + x] = 0.25; }
}
}
const FEED = 0.055, KILL = 0.062, DU = 1, DV = 0.5;
function idx(x, y) { return ((y + GH) % GH) * GW + ((x + GW) % GW); }
// Karl Sims 튜토리얼의 가중 3x3 라플라시안 — 4점 라플라시안보다 완만해서 dt=1 에서도 안정적이다
function lap(F, x, y) {
return F[idx(x - 1, y - 1)] * 0.05 + F[idx(x, y - 1)] * 0.2 + F[idx(x + 1, y - 1)] * 0.05
+ F[idx(x - 1, y)] * 0.2 - F[idx(x, y)] + F[idx(x + 1, y)] * 0.2
+ F[idx(x - 1, y + 1)] * 0.05 + F[idx(x, y + 1)] * 0.2 + F[idx(x + 1, y + 1)] * 0.05;
}
function step() {
const U2 = new Float32Array(U), V2 = new Float32Array(V);
for (let y = 0; y < GH; y++) for (let x = 0; x < GW; x++) {
const i = y * GW + x;
const u = U[i], v = V[i], uvv = u * v * v;
U2[i] = u + (DU * lap(U, x, y) - uvv + FEED * (1 - u));
V2[i] = v + (DV * lap(V, x, y) + uvv - (FEED + KILL) * v);
}
U = U2; V = V2;
}
function render() {
for (let i = 0; i < GW * GH; i++) {
const n = Math.max(0, Math.min(1, V[i] * 2.4));
const j = i * 4;
img.data[j] = 13 + n * (rgb[0] - 13); img.data[j + 1] = 13 + n * (rgb[1] - 13); img.data[j + 2] = 18 + n * (rgb[2] - 18); img.data[j + 3] = 255;
}
sctx.putImageData(img, 0, 0);
ctx.imageSmoothingEnabled = true;
ctx.drawImage(small, 0, 0, c.width, c.height);
}
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
c.width = innerWidth * dpr; c.height = innerHeight * dpr;
}
addEventListener('resize', resize);
resize();
for (let i = 0; i < 3000; i++) step(); // 스크린샷 시점에 이미 무늬가 자라 있도록 미리 계산
render();
(function loop() {
for (let i = 0; i < 6; i++) step();
render();
requestAnimationFrame(loop);
})();