GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

폴링 샌드(falling sand simulator) 9종 — 모래·물·불 셀 오토마타

라이브러리·WebGL 없이 칸 하나의 규칙 네 개(아래·좌아래·우아래·옆)로 모래가 쌓이고 물이 흐르고 불이 번집니다 — 이 폴링 샌드(falling sand simulator) 9종은 그 계산을 캔버스와 바닐라 자바스크립트만으로 보여줍니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

낙하 규칙 하나가 아홉 재료로 갈라지는 이유

순서는 격자에 얹는 재료 종류의 개수로 갈립니다. 01 기본 낙하는 아래칸이 비면 내려가는 규칙 하나로 도는 최소 단위이고, 02 사면 미끄짐은 거기에 좌우 대각선 시도를 더해 안식각을 만듭니다. sand game 장르가 다 여기서 갈라지는데, 03 물 흐름은 밀도가 다른 재료 둘(모래·물)을 한 격자에 같이 두는 첫 판이고 04 불 번짐은 재료가 다른 재료를 소모해 없애는 반응을 처음 넣습니다. 05 식물 성장은 그 반대로 재료가 재료를 만들어내는 생성 규칙이고, 06 포인터 브러시는 규칙이 아니라 입력(커서)이 재료를 흘려보내는 스포너 판입니다. 07 산 부식은 지형 자체를 파내는 부식 반응, 08 모래시계는 고정 벽으로 낙하를 가두는 판, 09 생태계는 이 여덟 규칙을 한 격자에서 동시에 돌리는 통합입니다. 격자형(ImageData/putImageData, 03·04·07·09)은 재료가 서로 반응하는 판이고 블록형(fillRect, 01·02·05·06·08)은 낙하·입력 위주의 가벼운 판이라는 구분도 이 순서를 따라갑니다.

모래는 왜 셀 오토마타로 떨어지나? — 4방향 규칙과 확률 퍼짐의 셈

4방향 규칙이 정하는 순서 — 아래 → 대각선 → 좌우

모든 항목의 낙하 함수는 같은 순서를 지킵니다. 아래가 비었는지 먼저 보고, 막혔으면 대각선 둘(좌하·우하)을 시도하고, 그마저 막힌 재료(물)만 좌우로 퍼집니다. 이 순서를 바꿔 대각선을 먼저 보면 모래가 바닥에 닿기도 전에 옆으로 새어 나가는 그림이 나옵니다.

확률 퍼짐이 만드는 질감

SLIDE_P·GROW_P·POUR_P·IGNITE_P·EAT_P 다섯 상수는 전부 0~1 확률이고, 낮출수록 규칙이 성공하는 빈도가 줄어 화면이 더디게 바뀝니다. falling sand 부류가 값 하나로 완전히 다른 질감을 내는 이유가 여기에 있는데, 같은 낙하 함수라도 확률만 바꾸면 폭포가 되거나 봉긋한 무더기가 됩니다.

스캔 방향을 프레임마다 바꾸는 이유

04·05의 ltr = (tick & 1) === 0처럼 짝수 프레임은 왼쪽부터, 홀수 프레임은 오른쪽부터 훑습니다. 한 방향으로만 훑으면 이미 갱신된 칸을 그 프레임 안에서 다시 건드리는 순서 편향이 생겨 무더기가 한쪽으로 쏠립니다.

ImageData 일괄 처리가 fillRect 보다 빠른 경계

03·04·07·09처럼 재료가 서로 반응해 화면 전체가 자주 바뀌는 항목은 칸마다 fillRect를 부르는 대신 ImageData 버퍼를 한 번에 채워 putImageData로 올립니다. 01·02·05·06·08처럼 바뀌는 칸이 상대적으로 적은 항목은 셀 단위 fillRect로도 프레임이 유지됩니다.

항목별 코드

01기본 낙하 — 아래칸이 비었으면 내려가는 규칙 하나

칸마다 재료 번호를 담는 격자를 만들고 매 프레임 아래칸이 비어 있으면 스왑하는 규칙 하나만 돌리면, 위에서 쏟아진 모래가 바닥에 무더기로 쌓이는 첫 장면이 완성됩니다 — 실측 누적면적 33.3% · 23/23 프레임이 움직였습니다.

기본 낙하 — 아래칸이 비었으면 내려가는 규칙 하나
새 창에서 열기

01 · 기본 낙하 — 아래칸이 비었으면 내려가는 규칙 하나 — 측정 누적면적 33.333% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectrequestAnimationFrame

아래로 한 칸씩 스왑하는 낙하 루프

function update() {
  for (var y = rows - 2; y >= 0; y--) {
    for (var x = 0; x < cols; x++) {
      var i = idx(x, y);
      if (grid[i] !== 1) continue;
      var destY = y;
      for (var s = 1; s <= FALL; s++) {
        var yy = y + s;
        if (yy >= rows) break;
        if (grid[idx(x, yy)] === 0) destY = yy; else break;
      }
      if (destY !== y) {
        var di = idx(x, destY);
        grid[di] = 1; shade[di] = shade[i];
        grid[i] = 0;
      }
    }
  }
}

아래 행부터 위 행 순서로 스캔해야 한 프레임 안에서 이미 내려온 칸을 다시 내리는 이중 이동이 안 생기고, FALL을 1보다 키우면 한 프레임에 여러 칸씩 쏟아지는 폭포가 됩니다.

02사면 미끄짐 — 좌우 대각선 규칙으로 자연스러운 안식각

아래가 막힌 칸은 왼쪽-아래·오른쪽-아래 대각선을 차례로 시도해 비어 있으면 그리로 옮기면, 모래가 한 줄로 곧게 쌓이지 않고 실제처럼 옆으로 퍼지며 안식각을 이룹니다 — 실측 누적면적 33.9% · 23/23 프레임이 움직였습니다.

사면 미끄짐 — 좌우 대각선 규칙으로 자연스러운 안식각
새 창에서 열기

02 · 사면 미끄짐 — 좌우 대각선 규칙으로 자연스러운 안식각 — 측정 누적면적 33.878% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectrequestAnimationFrame

막히면 좌우 대각선을 순서대로 시도한다

// 2) 대각선 — 좌하 → 우하 순서, SLIDE_P 확률
if (rand() < SLIDE_P) {
  var dir = rand() < 0.5 ? -1 : 1;
  var try1 = (x + dir >= 0 && x + dir < cols && grid[idx(x + dir, y + 1)] === 0 && grid[idx(x + dir, y)] === 0);
  var try2 = false;
  if (!try1) {
    var d2 = -dir;
    try2 = (x + d2 >= 0 && x + d2 < cols && grid[idx(x + d2, y + 1)] === 0 && grid[idx(x + d2, y)] === 0);
  }
  if (try1 || try2) {
    var dx = try1 ? dir : -dir;
    var di = idx(x + dx, y + 1);
    grid[di] = 1; shade[di] = shade[i]; grid[i] = 0;
  }
}

SLIDE_P는 1.0이 기본값이라 막힌 칸이 항상 미끄러지지만, 낮은 확률로 두면 마찰이 큰 것처럼 가파르게 쌓입니다.

03물 흐름 — 아래가 막히면 좌우로 퍼지는 액체 규칙

물 칸은 아래→대각선→왼쪽·오른쪽 순서로 시도하고 좌우 이동은 매 프레임 방향을 바꿔 주면, 모래는 쌓이고 물은 낮은 곳을 찾아 흐르는 두 성질이 같은 격자 안에서 공존합니다 — 실측 누적면적 33.6% · 23/23 프레임이 움직였습니다.

물 흐름 — 아래가 막히면 좌우로 퍼지는 액체 규칙
새 창에서 열기

03 · 물 흐름 — 아래가 막히면 좌우로 퍼지는 액체 규칙 — 측정 누적면적 33.589% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

물 위로는 모래가, 모래 위로는 물이 가라앉지 않는다

function update() {
  flowDir = -flowDir;   // 물 좌우 방향 매 틱 교대
  var x, y, i, j, nx;
  for (y = rows - 2; y >= 0; y--) {
    for (x = 0; x < cols; x++) {
      i = idx(x, y);
      var c = grid[i];
      if (c !== SAND && c !== WATER) continue;
      // 아래로 — 물은 모래·돌 위로 못 감. 모래가 물 위로 가라앉음(밀도)
      var bi = idx(x, y + 1);
      if (grid[bi] === EMPTY || (c === SAND && grid[bi] === WATER)) {
        swap(i, bi);
        continue;
      }

같은 스왑 조건 안에 c === SAND를 끼워 넣은 한 줄이 밀도를 흉내 내는 전부라서, 모래 칸만 물 칸과 자리를 바꿀 수 있고 물은 물끼리 자리를 바꿔도 그림이 안 변해 조건에서 빠집니다.

04불 번짐 — 이웃 연료를 태우고 줄어드는 소모 규칙

불 칸은 매 프레임 확률로 이웃 나무·기름 칸을 불로 바꾸고 자신은 수명을 한 칸 줄여 재가 되게 하면, 타고 넘어가며 결국 꺼지는 불줄기가 재료 사이를 이동합니다 — 실측 누적면적 49.4% · 23/23 프레임이 움직였습니다.

불 번짐 — 이웃 연료를 태우고 줄어드는 소모 규칙
새 창에서 열기

04 · 불 번짐 — 이웃 연료를 태우고 줄어드는 소모 규칙 — 측정 누적면적 49.402% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

점화 확률과 수명이 불줄기의 속도를 정한다

if (m === FIRE) {
  // 이웃 점화 — 4방향의 나무·기름을 확률로 불로
  var nb = [[1,0],[-1,0],[0,1],[0,-1]];
  for (d = 0; d < 4; d++) {
    var nx = x + nb[d][0], ny = y + nb[d][1];
    if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
    var j = idx(nx, ny);
    if ((grid[j] === WOOD || grid[j] === OIL) && rand() < IGNITE_P) {
      grid[j] = FIRE; life[j] = LIFE; moved[j] = 1;
    }
  }
  // 수명 감소 — 다하면 재, 위 빈칸에 연기 시도
  if (life[i] <= 1) {
    grid[i] = ASH;
    if (y > 0 && grid[idx(x, y - 1)] === EMPTY && rand() < 0.5) {
      grid[idx(x, y - 1)] = SMOKE;
      if (y > 1 && grid[idx(x, y - 2)] === EMPTY && rand() < 0.5) grid[idx(x, y - 2)] = SMOKE;
    }
  } else {
    life[i]--;
  }

IGNITE_P 0.06과 LIFE 14틱의 조합이 이 편의 화면을 채우는 값이고, 이미 이번 틱에 옮긴 칸을 moved 버퍼로 걸러야 불이 한 틱에 화면 끝까지 번지는 연쇄를 막습니다.

05식물 성장 — 물을 만나면 위로 자라는 생성 규칙

씨앗 칸이 아래에서 물이 스쳐 지나가면 확률로 위 칸을 새 줄기로 바꾸고 물을 없애 규칙 하나로 생태가 만들어집니다 — 물이 흐르는 길을 따라 풀이 자라나며, 실측 누적면적 30.4% · 23/23 프레임이 움직였습니다.

식물 성장 — 물을 만나면 위로 자라는 생성 규칙
새 창에서 열기

05 · 식물 성장 — 물을 만나면 위로 자라는 생성 규칙 — 측정 누적면적 30.37% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectrequestAnimationFrame

물이 스치는 세 방향을 검사해 줄기를 올린다

} else if (m === SEED || m === PLANT) {
  // 성장 — 아래·옆에 물이 닿으면 확률로 위 빈칸을 줄기로, 물 소모
  var touch = [
    [0, 1, y < rows - 1 ? grid[idx(x, y + 1)] : STONE],
    [1, 0, x < cols - 1 ? grid[idx(x + 1, y)] : STONE],
    [-1, 0, x > 0 ? grid[idx(x - 1, y)] : STONE]
  ];
  for (var t2 = 0; t2 < 3; t2++) {
    if (touch[t2][2] !== WATER) continue;
    var wx = x + touch[t2][0], wy = y + touch[t2][1];
    if (y > 0 && grid[idx(x, y - 1)] === EMPTY && rand() < GROW_P) {
      grid[idx(x, y - 1)] = PLANT;      // 새 줄기 — plant/leaf 교대 색은 렌더에서 줄 번호로
      grid[idx(wx, wy)] = EMPTY;         // 물 소모
      moved[idx(x, y - 1)] = 1;
    }
    break; // 물 한 방울은 한 번만 쓴다
  }
}

GROW_P 0.08은 물 한 방울이 줄기 하나로 이어지는 확률이고, 세 방향 중 하나만 맞아도 곧바로 break 로 빠져나가 같은 물 한 방울이 두 번 소모되지 않습니다.

06포인터 브러시 — 커서에서 재료를 붓처럼 쏟는 스포너

포인터 좌표 주변 반경 안의 빈 칸에 매 프레임 선택 재료를 확률로 채우면, 커서가 지나간 자리마다 모래·물·씨앗이 붓놀림처럼 쏟아져 내립니다 — 실측 누적면적 35.5% · 23/23 프레임이 움직였습니다.

포인터 브러시 — 커서에서 재료를 붓처럼 쏟는 스포너
새 창에서 열기

06 · 포인터 브러시 — 커서에서 재료를 붓처럼 쏟는 스포너 — 측정 누적면적 35.529% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermovefillRectrequestAnimationFrame

원형 반경 안의 빈 칸만 확률로 채운다

function pour(gx, gy, mat) {
  var r2 = BRUSH_R * BRUSH_R;
  for (var dy = -BRUSH_R; dy <= BRUSH_R; dy++) {
    for (var dx = -BRUSH_R; dx <= BRUSH_R; dx++) {
      if (dx * dx + dy * dy > r2) continue;
      var x = gx + dx, y = gy + dy;
      if (x < 0 || x >= cols || y < 0 || y >= rows) continue;
      var i = idx(x, y);
      if (grid[i] === EMPTY && rand() < POUR_P) grid[i] = mat;
    }
  }
}

dx*dx+dy*dy > r2 한 줄이 사각 반복을 원형 반경으로 잘라내고, BRUSH_R 6칸과 POUR_P 0.5가 붓의 굵기와 농도를 각각 정합니다.

07산 부식 — 만나는 재료를 지우는 반응 규칙

산 칸이 아래·옆 이웃이 흙·돌이면 그 칸을 비우고 자신도 확률로 소모되게 하면, 바닥을 뚫고 지하로 스며들며 통로를 파는 부식이 생깁니다 — 실측 누적면적 30.3% · 23/23 프레임이 움직였습니다.

산 부식 — 만나는 재료를 지우는 반응 규칙
새 창에서 열기

07 · 산 부식 — 만나는 재료를 지우는 반응 규칙 — 측정 누적면적 30.269% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

이웃을 지우고 자신도 확률로 소모된다

function updateAcid(x, y, dir) {
  var m = get(x, y);
  if (m !== ACID) return;
  var spots = [[x, y + 1], [x - 1, y], [x + 1, y]];
  var melted = false;
  for (var i = 0; i < spots.length; i++) {
    var nx = spots[i][0], ny = spots[i][1];
    if (isErodible(get(nx, ny))) { set(nx, ny, EMPTY); melted = true; }
  }
  if (melted && rand() < EAT_P) { set(x, y, EMPTY); return; }
  if (moveInto(x, y, x, y + 1, ACID)) return;
  var first = dir > 0 ? -1 : 1;
  if (moveInto(x, y, x + first, y + 1, ACID)) return;
  if (moveInto(x, y, x - first, y + 1, ACID)) return;
  if (moveInto(x, y, x + first, y, ACID)) return;
  moveInto(x, y, x - first, y, ACID);
}

이웃 셋(아래·좌·우)을 지운 뒤에야 EAT_P 0.3 확률로 산 자신이 없어지므로, 산 한 방울이 여러 칸을 먹어치우고서야 소모되는 비대칭이 통로를 굵게 만듭니다.

08모래시계 타이머 — 벽이 유도하는 낙하로 짜는 시간 표시

목이 좁은 두 원뿔 벽을 고정 칸으로 두고 안에 모래를 채우면, 낙하 규칙만으로 위쪽이 비워지고 아래가 차오르는 실제 모래시계 리듬이 나옵니다 — 실측 누적면적 50.2% · 21/23 프레임이 움직였습니다.

모래시계 타이머 — 벽이 유도하는 낙하로 짜는 시간 표시
새 창에서 열기

08 · 모래시계 타이머 — 벽이 유도하는 낙하로 짜는 시간 표시 — 측정 누적면적 50.213% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectrequestAnimationFrame

고정 벽 사이를 낙하 규칙 하나로 통과시킨다

function updateSand(x, y, dir) {
  if (get(x, y) !== SAND) return false;
  if (moveInto(x, y, x, y + 1)) return true;
  if (rand() > SLIDE_P) return false;
  var first = dir > 0 ? -1 : 1;
  if (moveInto(x, y, x + first, y + 1)) return true;
  return moveInto(x, y, x - first, y + 1);
}

벽은 고정 칸이라 규칙에서 아예 건너뛰므로 낙하 함수는 01·02와 똑같고, CYCLE_TICKS 150틱마다 위아래를 뒤집어 다시 채우는 것은 이 함수 밖의 타이머가 하는 일입니다.

09생태계 장면 — 모래·물·불·식물이 같은 격자에서 도는 통합

지형(흙·돌) 위에 호수를 두고 빗물 스포너와 번개 점화기를 더해 모든 규칙을 한 격자에서 돌리면, 불이 풀을 태우고 비가 불을 끄고 물길 따라 다시 자라는 자기완결 생태가 화면 전체에서 흐릅니다 — 실측 누적면적 32.9% · 23/23 프레임이 움직였습니다.

생태계 장면 — 모래·물·불·식물이 같은 격자에서 돌아가는 통합
새 창에서 열기

09 · 생태계 장면 — 모래·물·불·식물이 같은 격자에서 돌아가는 통합 — 측정 누적면적 32.884% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

물을 만난 불은 그 자리에서 꺼진다

function updateFire(x, y) {
  if (get(x, y) !== FIRE) return;
  life[idx(x, y)]++;
  if (life[idx(x, y)] > 46) { set(x, y, rand() < 0.5 ? SMOKE : ASH); return; }
  var around = [[0, 1], [0, -1], [-1, 0], [1, 0], [-1, -1], [1, 1], [-1, 1], [1, -1]];
  var burned = false;
  for (var i = 0; i < around.length; i++) {
    var nx = x + around[i][0], ny = y + around[i][1];
    var m = get(nx, ny);
    if (isFlammable(m) && rand() < 0.34) { set(nx, ny, FIRE); burned = true; }
    else if (m === WATER) { set(x, y, ASH); return; } // 물 만난 불은 재로 소화
  }
  if (burned) set(x, y, ASH); // 태우고 나면 재
}

여덟 방향 이웃을 훑다가 물을 만나면 그 자리에서 즉시 재로 바뀌어 함수를 빠져나가므로, 04 단독 판에는 없던 소화 규칙 한 줄이 09에서만 새로 들어갑니다.

모래 시뮬레이션을 실제로 쓰는 화면

로딩 화면이나 404 페이지처럼 잠깐 머무는 자리에 09 생태계 장면 하나를 얹으면, 방문자가 클릭할 때마다 06 포인터 브러시가 재료를 쏟아 화면이 매번 다르게 채워집니다. 게임·도구 소개 페이지의 히어로 영역에는 08 모래시계 타이머처럼 벽으로 흐름을 가두는 판이 더 어울리는데, 정해진 리듬으로 차고 비어서 대기 문구와 맞물립니다.

02 사면 미끄짐이나 03 물 흐름처럼 규칙 하나만 도는 항목은 사이드바 장식이나 카드 배경처럼 시선을 크게 뺏지 않아도 되는 자리에 둡니다 — 09처럼 여러 재료가 겹치는 판은 화면 전체를 차지하는 자리에만 씁니다.

확률 값 경계를 넘기면 모래가 여기서 깨지는 함정

규칙 순서를 바꾸면 깨지는 것

03에서 아래→대각선→좌우 순서를 좌우 먼저로 바꾸면 물이 바닥에 닿기 전에 옆으로 흩어져 웅덩이가 안 생기고, 09에서 불의 소화 검사(물 이웃)를 점화 검사보다 뒤에 두면 물웅덩이 옆 풀도 한 틱 동안 불이 붙었다 꺼지는 깜빡임이 남습니다.

확률 값 경계에서 깨지는 것

04에서 IGNITE_P를 0.06에서 0.2 근처로 올리면 나무 지형이 한 틱 만에 통째로 불붙어 09 생태계의 "타고-식고-다시 자라는" 순환이 안 보이고, 07에서 EAT_P를 1.0으로 두면 산이 한 칸을 지우자마자 사라져 통로가 한 칸 폭을 못 넘습니다. 08은 CYCLE_TICKS를 짧게 주면 위 칸이 다 비기 전에 뒤집혀 모래시계 리듬이 끊기고, 05는 GROW_P를 너무 낮추면 물 한 방울이 스쳐도 줄기가 안 올라와 화면이 정적으로 보입니다. 아홉 판 전부의 상수를 직접 건드려 확인해 보고 싶다면 zip 을 받아 코드를 열어야 하는데, 압축을 푸는 열쇠는 글 속 jetqny5v 입니다.

재료 칸 크기 하나로 갈리는 세 변형과 접근성 처리

값 하나를 바꾼 세 변형

변형 값 느낌
낙하 속도 01 기본 낙하 매 프레임 1칸 → 3칸 모래가 폭포처럼 쏟아져 화면이 급해진다
안식각 02 사면 미끄짐 대각 시도 확률 1.0 → 0.4 낮은 확률로 두면 모래가 젖은 것처럼 뭉쳐 가파르게 쌓인다
물 퍼짐 폭 03 물 좌우 동시 시도 1칸 → 3칸 물이 넓게 얇게 퍼져 홍수처럼 번진다

움직임을 줄인 사용자에게 남는 정지 프레임

prefers-reduced-motion: reduce를 켠 화면에서는 아홉 판 전부 requestAnimationFrame 루프를 아예 돌리지 않고 한 프레임만 그린 뒤 캔버스에 is-still 클래스를 붙입니다. 셀 오토마타는 매 프레임 격자 전체를 다시 계산하는 구조라 애니메이션을 끄면서도 낙하가 끝난 뒤의 무더기·웅덩이 모양은 그대로 보입니다.

포인터로 만지는 판

06 포인터 브러시는 pointermove 좌표를 격자 좌표(열·행)로 변환해 반경 안 빈 칸에만 재료를 채우고, 커서가 캔버스를 벗어나면(pointerleave) 붓질을 멈춥니다. 09 생태계는 클릭이 아니라 STRIKE_T 초 주기 타이머로 무작위 풀 칸에 번개를 내려 자동으로 점화합니다. 화면 픽셀 좌표를 그대로 칠하지 않고 셀 크기로 나눈 격자 좌표로 바꾸는 변환 한 줄이 브러시가 캔버스 크기와 무관하게 같은 반경으로 그려지게 합니다.

포인터 브러시를 만지기 전에 자주 나오는 질문

이 데모 코드를 그대로 붙여 써도 되나요?

zip 안 아홉 판은 물리 엔진 없이 바닐라 자바스크립트 파일 하나(index.html)로 동작해 HTML 파일을 열기만 하면 그대로 재현됩니다. 격자 크기·재료 색은 demo-tokens.scss 값을 바꿔 자신의 화면에 맞추면 됩니다.

sand game 장르와 이 편의 셀 오토마타는 같은 원리인가요?

같은 원리입니다 — 칸마다 재료 번호 하나를 담는 격자에 이웃 검사 규칙만 반복해 돌리는 것이 이 장르 전체의 공통 뼈대이고, 이 편의 아홉 판은 그 뼈대에 재료(모래·물·불·산·식물)를 하나씩 더해 보여줍니다.

격자를 키우면 프레임이 느려지나요?

01·02·05·06·08처럼 fillRect로 그리는 항목은 격자가 커질수록 칸 수만큼 그리기 호출이 늘어 느려지지만, 03·04·07·09는 ImageData 버퍼 하나로 한 번에 올려 격자 크기에 덜 민감합니다. 셀 크기를 4px보다 크게 두면 두 방식 모두 칸 수가 줄어 여유가 생깁니다.

더 읽을거리

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.