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

snowflake generator 9종 — DLA로 자라는 눈결정

snowflake generator 아홉 종을 캔버스 픽셀 계산만으로 보여줍니다. 라이브러리·WebGL 없이 바깥에서 날아든 입자 하나가 결정에 닿아 붙는 순간을 반복하는 것만으로 육각 눈결정이 저절로 자랍니다.

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

흡착 확률 하나로 아홉 눈송이를 가르는 순서

순서는 결정 모양의 화려함이 아니라 같은 응집 규칙에 무엇을 얹었는가로 갈립니다. 01은 흡착 확률을 1로 고정한 가장 단순한 육각 응집이고, 02·03은 그 위에 팁 쪽으로 쏠리는 흡착 확률 하나만 더해 뭉툭한 덩어리를 가지(수지상)·바늘·고사리로 갈라놓습니다. 04는 계산을 아예 줄이는 쪽으로 방향을 틀어 섹터 하나만 걷고 나머지 다섯을 거울처럼 접어 완벽한 육중 대칭을 만들고, 05는 씨앗을 중심이 아니라 창틀 전체로 옮겨 프랙탈이 가장자리에서 안으로 번지게 하며, 06은 붙은 시각을 색으로 남겨 성장 자체를 나이테로 읽게 합니다. 07은 씨앗을 하나 더 심어 두 결정이 경쟁하는 상호 응집을, 08은 붙은 셀을 도로 떼어내는 냉동·해동 순환을, 09는 같은 계산을 어두운 무대와 조명 위에 올려 매크로 사진처럼 보여줍니다 — 이 아홉 판을 관통하는 축은 눈송이 그림이 아니라 걸음 하나가 닿았을 때 붙을지 말지를 정하는 확률 하나입니다.

항목별 코드

01기본 육각 응집 — 랜덤워크 입자가 닿은 자리에 붙는 DLA 한 겹

결정 씨앗 하나를 두고 바깥 원에서 출발한 입자가 육각 격자 위를 무작위로 걸으며 결정에 닿는 순간 붙는 규칙을 반복하면 뾰족한 육각 클러스터가 스스로 자랍니다 — 실측 누적면적 43.511% · 강도 평균 59.5 · 23/23 프레임이 움직였습니다.

기본 육각 응집 — 랜덤워크 입자가 닿은 자리에 붙는 DLA 한 겹
새 창에서 열기

01 · 기본 육각 응집 — 랜덤워크 입자가 닿은 자리에 붙는 DLA 한 겹 — 측정 누적면적 43.511% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

랜덤워크 입자가 닿으면 얼어붙는 규칙

function step(t) {
  for (var s = 0; s < 20; s++) {          // 프레임당 스텝 수 — 걸음 속도
    for (var w = walk.length - 1; w >= 0; w--) {
      var p = walk[w];
      if (rand() < 0.25) p[2] = Math.floor(rand() * 6);   // 가끔 방향 재추첨 — 등방성
      var nb = nbr(p[0], p[1], p[2]);
      var i = nb[0], j = nb[1];
      if (!inGrid(i, j)) { walk.splice(w, 1); continue; }  // 튀어나가면 버리고 다시 발사
      if (rand() < 0.2) p[3] += (rand() - 0.5) * 0.7;
      p[0] = i; p[1] = j;
      if (touching(i, j)) {
        if (rand() < STICK) {              // 닿았다 — 확률로 그 칸에 얼어 붙는다
          set(i, j, t);
          stuck++;
          var dc = Math.hypot(i - cx, (j - cy) * 0.92);
          if (dc + 2 > crystalR) crystalR = Math.min(N * 0.46, dc + 2);
        }
        walk.splice(w, 1);
      } else if (Math.hypot(i - cx, j - cy) > crystalR * 1.7 + 8) {
        walk.splice(w, 1);                 // 너무 멀어졌다 — 되돌아올 확률이 얇다
      }
    }
  }
  while (walk.length < PARTICLES) launch();
}

PARTICLES 34개가 동시에 걷고, nbr() 이 짝수·홀수 열마다 다른 6방향 오프셋(DJ_EVEN·DJ_ODD)을 돌려주는 것이 이 격자를 육각으로 만드는 전부입니다. STICK 을 1.0 으로 두면 닿는 즉시 얼어붙어 가장 거친 DLA 뼈대가 나옵니다.

02가지 결정(dendrite) — 여섯 방향 팁이 촘촘히 갈라지는 나무 성장

입자가 결정 중심에서 먼 팁 쪽에 더 잘 붙도록 흡착 확률을 기울이면 뭉툭한 덩어리 대신 진짜 눈결정의 가지가 뻗습니다 — 실측 누적면적 44.129% · 강도 평균 65.4 · 23/23 프레임이 움직였습니다.

가지 결정(dendrite) — 여섯 방향 팁이 촘촘히 갈라지는 나무 성장
새 창에서 열기

02 · 가지 결정(dendrite) — 여섯 방향 팁이 촘촘히 갈라지는 나무 성장 — 측정 누적면적 44.129% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

팁 가중 흡착으로 가지를 뻗는 법

// ── 팁 가중 흡착 — 반경이 클수록(팁에 가까울수록) 흡착 확률을 높인다 ──
function stickProb(i, j) {
  var dc = Math.hypot(i - cx, (j - cy) * 0.92);
  var k = Math.min(1, dc / Math.max(1, crystalR));
  return Math.pow(k, 1) * 0.15 + Math.pow(k, TIP_BIAS) * 0.85 + 0.05;
}

칸이 중심에서 멀수록(k 가 1에 가까울수록) Math.pow(k, TIP_BIAS) 항이 커져 흡착 확률이 뛰고, TIP_BIAS 2.6이 이 편향의 세기입니다 — 소스 주석대로 이 값을 0으로 낮추면 01과 똑같은 뭉툭한 클러스터로 돌아갑니다.

03침·고사리 결정 — 습도 밴드를 바꾸면 나뭇잎에서 바늘로

같은 팁 가중 응집 규칙에 편향 지수 하나만 다르게 준 두 결정을 나란히 두면, 값이 낮은 쪽은 넓은 고사리형으로 값이 높은 쪽은 좁은 침형으로 갈라집니다 — 실측 누적면적 41.614% · 강도 평균 75.9 · 23/23 프레임이 움직였습니다.

침·고사리 결정 — 습도 밴드를 바꾸면 나뭇잎에서 바늘로
새 창에서 열기

03 · 침·고사리 결정 — 습도 밴드를 바꾸면 나뭇잎에서 바늘로 — 측정 누적면적 41.614% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

습도 편향 값 하나로 갈라지는 두 결정

// 팁 가중 흡착 — bias 가 결정의 습관(폭)을 정한다
function stickProb(c, i, j) {
  var dc = Math.hypot(i - c.cx, (j - c.cy) * 0.92);
  var k = Math.min(1, dc / Math.max(1, c.r));
  return Math.pow(k, 1) * 0.15 + Math.pow(k, c.bias) * 0.85 + 0.05;
}

02의 stickProb 와 거의 같은 함수지만 bias 를 결정 객체 c 마다 따로 받는 것이 다릅니다 — 왼쪽은 BIAS_FERN 1.6, 오른쪽은 BIAS_NEEDLE 4.6을 물려 같은 함수 하나가 두 습관을 동시에 그립니다.

04육중 대칭 거울 — 60도마다 접어 여섯 배 속도로 자라기

섹터 하나(60도 부채꼴)만 시뮬레이션하고 나머지 다섯을 회전 복사하면 계산량이 6분의 1이 되면서도 대칭이 어긋나지 않는 눈결정을 얻습니다 — 실측 누적면적 40.29% · 강도 평균 45.8 · 23/23 프레임이 움직였습니다.

육중 대칭 거울 — 60도마다 접어 여섯 배 속도로 자라기
새 창에서 열기

04 · 육중 대칭 거울 — 60도마다 접어 여섯 배 속도로 자라기 — 측정 누적면적 40.29% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotateImageDatarequestAnimationFrame

섹터 하나만 걷고 나머지를 접어서 그리는 법

// 각도를 섹터(-SECTOR/2 .. SECTOR/2)로 60도 주기 접는다 — 회전 접기(C6). 걸음이 부채꼴을
// 나가도 접혀 안으로 되돌아오므로 시뮬레이션 공간이 섹터 하나로 끝난다.
function foldTh(th) {
  var P = SECTOR, m = (th + P * 0.5) % P;
  if (m < 0) m += P;
  return m - P * 0.5;
}
// 상대 좌표 → 접힌 (각도, 반경)
function fold(fx, fy) {
  var r = Math.sqrt(fx * fx + fy * fy);
  var th = r < 1e-9 ? 0 : foldTh(Math.atan2(fx, fy));
  return [th, r];
}

걸음이 섹터 경계를 넘으면 foldTh 가 그 각도를 곧바로 되접어 시뮬레이션 공간을 섹터 하나로 가두고, cellOcc 는 fold() 로 되접은 좌표를 다시 조회해 점유 여부를 판정합니다. COPIES(Math.round(360 / SECTOR_DEG))가 접힌 섹터를 몇 번 회전 복사할지 정합니다.

05서리 낀 창 — 가장자리에서 안쪽으로 기어드는 프랙탈 얼음

씨앗을 중앙이 아니라 창틀 가장자리 전체에 깔고 온도 필드를 안쪽으로 낮추면, 응집이 창가 프랙탈 무늬처럼 가장자리에서 안쪽으로 뻗어갑니다 — 실측 누적면적 45.515% · 강도 평균 51.6 · 23/23 프레임이 움직였습니다.

서리 낀 창 — 가장자리에서 안쪽으로 기어드는 프랙탈 얼음
새 창에서 열기

05 · 서리 낀 창 — 가장자리에서 안쪽으로 기어드는 프랙탈 얼음 — 측정 누적면적 45.515% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

창틀로 쏠리는 드리프트와 중심 냉기

function stepWalker(w) {
  var d;
  if (rand() < BIAS) {
    // 온도 필드 드리프트 — 가장 가까운 창틀로 한 걸음(방향 index: 0=+x,1=-x,2=+y,3=-y)
    var dl = w.gx, dr = GW - 1 - w.gx, dt = w.gy, db = GH - 1 - w.gy;
    var m = Math.min(dl, dr, dt, db);
    d = m === dl ? 1 : m === dr ? 0 : m === dt ? 3 : 2;
  } else {
    d = (rand() * 4) | 0;   // 무작위 걸음 — 이게 없으면 직선으로만 얼어 가지가 안 갈라진다
  }
  var nx = w.gx + DX[d], ny = w.gy + DY[d];
  if (nx < 0 || nx >= GW || ny < 0 || ny >= GH) { respawn(w); return false; }
  w.gx = nx; w.gy = ny;
  var i = idx(nx, ny);
  if (occ[i]) return false;
  var nc = (nx > 0 && occ[idx(nx - 1, ny)] ? 1 : 0) + (nx < GW - 1 && occ[idx(nx + 1, ny)] ? 1 : 0) +
            (ny > 0 && occ[idx(nx, ny - 1)] ? 1 : 0) + (ny < GH - 1 && occ[idx(nx, ny + 1)] ? 1 : 0);
  if (!nc) return false;
  // 온도 필드 — 중심에 가까울수록(dist 작을수록) 차가워 얼기 어렵다(가지가 가늘어진다)
  var dist = Math.min(1, edgeDist(nx, ny) / maxDist);
  var temp = 1 - dist * COLD;
  var p = (nc === 1 ? 1.0 : nc === 2 ? 0.58 : nc === 3 ? 0.24 : 0.12) * temp;
  if (rand() >= p) return false;
  occ[i] = 1;

BIAS 0.55 확률로 걸음이 가장 가까운 창틀 쪽 한 걸음을 택하고, 나머지는 무작위로 흔들려 직선이 아니라 갈라지는 가지를 만듭니다. 붙은 이웃 수 nc 와 중심까지 거리로 낮아지는 temp 를 곱해 흡착 확률 p 를 정하는데, 이 값이 05의 서리 결이 창틀에서 진하고 중심으로 갈수록 성겨지는 이유입니다.

06결정 격자 채색 — 나이(접촉 시각)를 파스텔 고리로 새기기

입자가 붙은 순간의 시각을 기록해 두었다가 나이별로 색을 달리하면, 결정의 성장 연륜이 나무 나이테처럼 파스텔 동심원으로 읽힙니다 — 실측 누적면적 9.325% · 강도 평균 73.3 · 23/23 프레임이 움직였습니다.

결정 격자 채색 — 나이(접촉 시각)를 파스텔 고리로 새기기
새 창에서 열기

06 · 결정 격자 채색 — 나이(접촉 시각)를 파스텔 고리로 새기기 — 측정 누적면적 9.325% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

접촉 시각을 파스텔 나이테로 읽는 법

function buildLUT() {
  // 띠 경계마다 살짝 밝은 테(나이테 선) — 같은 나이끼리 묶여 동심원으로 보인다
  var cs = [C_DUSK, C_DUSK, C_HALO, C_MINT, C_MIST, C_SKY, C_MINT, C_CORE];
  for (var i = 0; i < 256; i++) {
    var t = i / 255;
    var band = Math.min(cs.length - 1, 1 + Math.floor(t * (cs.length - 1)));
    var c0 = cs[band - 1], c1 = cs[band];
    var f = Math.min(1, t * (cs.length - 1) - (band - 1) * 2 + 1);   // 띠 안에서 부드럽게
    var ring = t * (cs.length - 1) % 1;
    var edge = ring < 0.16 ? 0.35 * (1 - ring / 0.16) : 0;           // 띠 시작 = 밝은 나이테 선
    var r0 = c0[0] + (c1[0] - c0[0]) * f, g0 = c0[1] + (c1[1] - c0[1]) * f, b0 = c0[2] + (c1[2] - c0[2]) * f;
    lut.r[i] = r0 + (C_CORE[0] - r0) * edge;
    lut.g[i] = g0 + (C_CORE[1] - g0) * edge;
    lut.b[i] = b0 + (C_CORE[2] - b0) * edge;
  }
}

RINGS 8단으로 나눈 파스텔 색을 256단계 LUT로 미리 구워 두고, 그리는 쪽은 age / total(붙은 시각을 전체 응집 수로 나눈 비율)로 이 표를 찾아보기만 합니다 — edge 항이 띠가 바뀌는 경계마다 밝은 테를 얹어 나이테 선을 만들고, 격자가 정사각 46칸으로 좁아 프레임 사이 바뀌는 픽셀 비율(누적면적)은 아홉 판 중 가장 낮게 잡혔습니다.

07포인터 씨앗 — 클릭한 자리에서 두 번째 결정이 같이 자라기

클릭(또는 터치)한 지점에 씨앗을 하나 더 심으면 입자들이 두 결정 사이에서 경쟁하며 응집합니다 — 닿는 순간 서로의 가지가 얽히는 상호 성장이고, 실측 누적면적 34.032% · 강도 평균 56.9 · 23/23 프레임이 움직였습니다.

포인터 씨앗 — 클릭한 자리에서 두 번째 결정이 같이 자라기
새 창에서 열기

07 · 포인터 씨앗 — 클릭한 자리에서 두 번째 결정이 같이 자라기 — 측정 누적면적 34.032% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermoveImageDatarequestAnimationFrame

두 씨앗이 경쟁하며 응집하는 규칙

function stick(x, y, own, now) {
  var id = idx(x, y);
  tStick[id] = now; owner[id] = own;
  if (sparkles.length > 96) sparkles.shift();
  sparkles.push({ x: x * CELL + 1, y: y * CELL + 1, t: now });
}

function plantSeed(fx, fy, now) {
  if (crystals.length >= MAX_CRYSTALS) return;
  var cx = Math.round(fx * GW), cy = Math.round(fy * GH);
  if (!inside(cx, cy) || tStick[idx(cx, cy)] >= 0) return;
  var id = crystals.length;
  crystals.push({ cx: cx, cy: cy, maxR: 2, born: now, count: 0, id: id });
  tStick[idx(cx, cy)] = now; owner[idx(cx, cy)] = id;
  var nx = hexNbr(cy), ny = hexNbrY(cy);   // 씨앗 주변 한 바퀴를 얼려 출발점을 만든다
  for (var k = 0; k < 6; k++) {
    var ax = cx + nx[k], ay = cy + ny[k];
    if (inside(ax, ay) && tStick[idx(ax, ay)] < 0) { tStick[idx(ax, ay)] = now; owner[idx(ax, ay)] = id; }
  }
  rings.push({ x: cx * CELL + 1, y: cy * CELL + 1, t: now });
}

owner 배열이 칸마다 어느 결정에 붙었는지 기억해 두는 것이 경쟁의 전부입니다 — AUTO_SEEDS 두 자리에서 자동으로 시작하고, 화면을 클릭하면 plantSeed 가 그 자리에 결정을 하나 더 등록해 같은 응집 계산을 두 owner 로 나눠 갖습니다. TINTS 배열이 결정마다 다른 파스텔을 입혀 어느 쪽이 어느 씨앗에서 자랐는지 눈으로 구분됩니다.

08녹았다 얼기 반복 — 가장 바깥 입자부터 떼어내 다시 붙이는 순환

일정 시간마다 결정 표면의 입자를 확률적으로 떼어내 주변으로 풀어놓으면 결정이 오래된 부분은 남기고 가장자리만 다시 자라는 숨쉬는 결정이 됩니다 — 실측 누적면적 37.253% · 강도 평균 32.7 · 23/23 프레임이 움직였습니다.

녹았다 얼기 반복 — 가장 바깥 입자부터 떼어내 다시 붙이는 순환
새 창에서 열기

08 · 녹았다 얼기 반복 — 가장 바깥 입자부터 떼어내 다시 붙이는 순환 — 측정 누적면적 37.253% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataputImageDatarequestAnimationFrame

표면 셀을 확률로 녹이고 다시 얼리는 순환

function meltStep(heat, now) {
  var tries = Math.round(MELT_RATE * heat * 10);
  for (var i = 0; i < tries; i++) {
    var x = (rand() * GW) | 0, y = (rand() * GH) | 0;
    var id = idx(x, y);
    if (!frozen[id]) continue;
    var n = frozenNb(x, y);
    if (n >= 6) continue;                                  // 완전 매립 셀은 안 녹는다
    var age = now - tStick[id];
    var pCore = Math.max(0.02, 1 - age / 5200);            // 오래된 코어는 녹을 확률이 낮다
    var p = (n / 6) * heat * pCore * 0.5;
    if (rand() < p) {
      frozen[id] = 0;
      var ang = rand() * Math.PI * 2, sp = 0.25 + rand() * 0.8;
      drifters.push({
        x: x * CELL + 1, y: y * CELL + 1,
        vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp,
        born: now, hot: true
      });
    }
  }
}

주변이 이미 꽉 채워진(n >= 6) 코어 칸은 아예 후보에서 빠지고, pCore 가 오래 붙어 있던 칸일수록 녹을 확률을 낮춰 표면만 녹았다 얼기를 반복합니다. 녹은 칸은 drifters 로 풀려났다가 refreezeStep 이 표면 이웃 수에 비례한 확률로 다시 붙입니다.

09매크로 사진 씬 — 다크 배경 위 촛점 심도로 뜨는 결정 한 편

검은 배경에 중앙 조명을 깔고 결정을 크게 키우면 현미경 사진 같은 한 컷이 됩니다 — 잔광과 부유 입자로 화면이 계속 살아 움직이고, 실측 누적면적 36.536% · 강도 평균 38.3 · 23/23 프레임이 움직였습니다.

매크로 사진 씬 — 다크 배경 위 촛점 심도로 뜨는 결정 한 편
새 창에서 열기

09 · 매크로 사진 씬 — 다크 배경 위 촛점 심도로 뜨는 결정 한 편 — 측정 누적면적 36.536% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createRadialGradientImageDatarequestAnimationFrame

조명 아래서 확대되는 응집 예산

function aggregate(now, budget) {
  var stuck = 0, attempts = 0;
  while (stuck < budget && attempts < budget * 9) {
    attempts++;
    var ang = rand() * Math.PI * 2;
    var rr = maxR + 6 + rand() * 16;
    var x = Math.round(CX + Math.cos(ang) * rr);
    var y = Math.round(CY + Math.sin(ang) * rr * 0.95);
    var steps = 0;
    while (steps < 460) {
      steps++;
      var dir = (rand() * 6) | 0;
      var bx = nbX(y), by = nbY(y);
      x += bx[dir]; y += by[dir];
      if (!inside(x, y)) break;
      if (frozen[idx(x, y)]) { x -= bx[dir]; y -= by[dir]; continue; }
      var n = frozenNb(x, y);
      if (n > 0) {
        var p = n === 1 ? 1 : n === 2 ? 0.5 : 0.13;
        var dist = distC[idx(x, y)];
        p *= 0.35 + 0.65 * Math.min(1, dist / (maxR + 4));
        if (dist < RMAX && rand() < p) {
          stick(x, y, now); stuck++;
          if (dist > maxR) maxR = dist;

STICKS_PER_FRAME 160개를 한 프레임 예산으로 태워 2초 안에 결정이 화면을 채우는 타임랩스 속도를 냅니다. p 를 정할 때 붙은 이웃 수뿐 아니라 중심까지 거리(dist)까지 곱해, 결정이 커질수록 바깥 팁이 안쪽보다 잘 붙는 09만의 성장 곡선을 만듭니다.

사진같은 히어로와 포인터로 키우는 결정 — 이 아홉 개를 어디에 쓰나

눈 내리는 시즌 캠페인이나 겨울 테마 랜딩 페이지의 히어로라면 09 매크로 사진 씬이 가장 빠릅니다. 다크 배경 위 촛점 심도로 뜬 결정 한 편이 이미 사진처럼 완성돼 있어 카피만 얹으면 됩니다. 방문자가 직접 눌러 보길 바라는 인터랙티브 영역에는 07 포인터 씨앗을 앉히면, 클릭한 자리에서 두 번째 결정이 자라나는 반응이 그대로 체험이 됩니다.

습도와 결정 형태의 관계를 설명하는 과학 교육 콘텐츠라면 03 침·고사리 결정이 맞습니다. 습도 밴드 값 하나를 바꾸는 것만으로 나뭇잎 모양에서 바늘 모양으로 넘어가는 과정이 화면에서 바로 확인됩니다. 세 판 모두 파일 하나로 끝나 있어 별도 프레임워크 없이 정적 섹션에 바로 붙습니다.

눈결정은 왜 DLA 로 자라나? — 랜덤워크 응집과 육각 대칭의 셈

랜덤워크가 결정 모양을 만드는 이유

이 아홉 판이 공유하는 시뮬레이션 규칙은 단 한 줄입니다 — 입자 하나가 결정 밖에서 무작위로 걷다가 이미 붙은 칸에 닿으면, 그 자리에서 확률만큼 얼어붙습니다. 이 확산제한응집(DLA)은 눈결정이 실제로 자라는 방식과 같은 셈입니다 — 수증기 분자가 공기 중에서 무작위로 떠돌다 결정 표면에 닿아야만 얼음이 되고, 한 번 붙은 자리는 다시 떨어지지 않습니다.

육각 대칭이 격자에서 저절로 나온다

01·02·03·07·08·09는 DJ_EVEN·DJ_ODD 같은 짝수·홀수 열 오프셋 배열로 이웃 6칸을 정의하는 육각 격자를 씁니다 — 사각 격자 4이웃 대신 6이웃을 쓰는 것만으로 랜덤워크가 항상 60도 간격으로만 꺾여 눈결정 특유의 육각 대칭이 계산에서 저절로 나옵니다. 04는 반대로 격자를 접어(회전 대칭 C6) 계산을 줄이는 쪽으로, 05·06은 정사각 격자에 4방향 이웃만 써서 결을 다르게 냅니다.

흡착 확률 하나가 가지 모양을 정한다

01은 흡착 확률을 1로 고정해 가장 거친 뼈대를 남기고, 02·03·05는 반경(또는 창틀까지 거리)이 멀수록 확률을 높이는 stickProb·temp 항 하나만 더해 뭉툭한 덩어리를 뾰족한 가지·바늘·서리로 바꿉니다. 실제 눈결정도 과냉각·습도에 따라 이 흡착이 얼마나 팁에 쏠리는지가 달라져 판·바늘·별가지 같은 서로 다른 형태로 갈립니다.

격자 방식과 파티클 방식이 나뉘는 지점

01~04·06·07·09는 격자 칸에 1을 채우는 방식으로 응집을 기록하고, 05·08도 같은 격자를 쓰지만 08은 붙은 칸을 다시 지울 수 있다는 점이 다릅니다. 격자에 값을 채우거나 지우는 것만으로 성장·해빙을 함께 표현할 수 있다는 것이 파티클을 계속 새로 그리는 방식보다 이 아홉 판에 더 잘 맞는 이유입니다.

습도 밴드와 대칭 배수를 바꾸는 세 변형과 접근성

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌
가지 확률 02 수지상 가지 확률 0.3 → 0.6 가지가 촘촘해질수록 진짜 눈결정에 가까운 레이스가 된다
응집 속도 01 입자 220개/초 → 700개/초 속도를 올리면 결정이 눈에 띄게 빨리 자라며 데모가 짧아진다
대칭 접기 04 섹터 각 60도 → 30도 섹터를 절반으로 줄이면 가지가 더 가늘고 정교해진다

세 축 모두 응집 규칙 자체는 건드리지 않고 상수 하나만 고쳐 밀도·속도·정교함을 조절합니다.

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

prefers-reduced-motion: reduce 를 켠 사용자에게는 아홉 판 모두 requestAnimationFrame 루프를 시작하지 않고 stillFrame·stillDraw 함수가 결정이 자라다 만 한 장을 미리 계산해 그립니다 — 01~03은 랜덤워크를 정해진 스텝만큼 미리 돌린 뒤 멈춘 장면을, 04~09는 격자 시뮬레이션을 같은 방식으로 몇 스텝 앞서 돌린 장면을 보여 줍니다. 매 프레임 다시 그리지 않을 뿐 응집 계산 자체는 그대로라 결정 형태는 움직임을 켰을 때와 다르지 않습니다.

포인터로 만지는 한 판과 나머지 여덟 판

아홉 판 전부 role="application" 과 tabindex="0" 을 가진 실험판이라 탭으로 초점을 옮길 수 있습니다. 그중 07만 포인터(터치 포함)로 화면을 눌러 그 자리에 두 번째 결정 씨앗을 심을 수 있고, 나머지 여덟 판은 자동으로 자라는 장면이라 초점을 옮기면 포커스 링만 뜹니다 — snowflake generator online free 서비스 다수가 결과만 보여 주는 것과 달리, 이 아홉 판은 결정이 자라는 계산 전체가 zip 소스에 그대로 들어 있습니다.

팁 가중치보다 동시 성장 팁 수가 면적을 가르는 함정

팁 편향을 0으로 낮추면 가지가 아니라 덩어리가 된다

02의 소스 주석은 TIP_BIAS 를 "0이면 01과 동일"이라고 못 박아 둡니다 — 팁 쪽 흡착을 밀어주는 지수가 없으면 입자가 오목한 자리에도 똑같이 잘 붙어 가지 사이가 금방 메워지고, 01의 뭉툭한 육각 클러스터로 되돌아갑니다. 가지를 더 가늘게 만들고 싶을 땐 지수를 낮추는 게 아니라 오히려 키워야 합니다.

화면이 바뀌는 비율만 보고 판단하면 잘못 고친다

02는 2026-09-15 재튜닝 전 누적면적이 28.5%로 품질 기준(30%+)에 못 미쳤는데, 원인은 팁 가중치가 약해서가 아니라 동시에 걷는 입자 수와 결정이 자랄 수 있는 최대 반경이 작았던 탓이었습니다. PARTICLES 를 42에서 56으로, R_CAP 을 0.46에서 0.48로 올리고 나서야 화면에서 바뀌는 픽셀 비율이 44.129%까지 올라왔습니다 — 정체성(팁 가중 단일 씨앗 덴드라이트)은 그대로 두고 동시 성장 팁 수·최종 크기만 키운 것입니다. 아홉 판의 zip 안 소스에서 이 상수들을 직접 흔들어 보려면 암호 zip 을 열어야 하고, 여는 암호는 fcyyjg6v 입니다.

걸음 수 예산을 늘려 주지 않으면 성장이 멈춘 것처럼 보인다

07의 WALKER_MAX_STEPS 는 420걸음이 지나면 입자를 포기시키고 새로 태어나게 하는데, 소스 주석대로 "넘으면 죽고 새 입자가 태어난다"가 반복되면 결정이 커져 씨앗까지 거리가 멀어진 뒤로는 그 구간의 응집이 눈에 띄게 느려집니다. 결정이 화면 절반을 넘게 자라는 장면을 만들려면 걸음 수 예산도 그만큼 같이 키워야 합니다.

눈결정 성장을 페이지에 붙이기 전 자주 나오는 질문

snowflake generator online 도구와 이 데모는 뭐가 다른가요?

웹에서 흔히 보는 snowflake generator online 도구는 미리 그려둔 패턴을 회전·확대해 보여 주는 경우가 많습니다. 여기 아홉 판은 매 프레임 랜덤워크 입자가 걷고 붙는 계산을 실제로 돌려 시드가 같아도 매번 조금씩 다른 결이 남는 DLA 응집이고, 그 계산 자체를 코드로 그대로 들고 갈 수 있습니다.

snowflake generator svg 처럼 벡터로 저장할 수 있나요?

이 아홉 판은 canvas 픽셀(ImageData)에 격자 칸을 찍는 방식이라 결과는 래스터입니다. 벡터로 쓰려면 격자 칸 좌표를 다각형 경로로 바꾸는 별도 단계가 필요하고, zip 안 소스에는 그 변환이 들어 있지 않습니다 — snowflake generator python 스크립트로 같은 DLA 를 짜 본 사람이라면 이 랜덤워크·흡착 확률 구조가 언어만 바뀐 채 그대로 옮겨진다는 걸 알아볼 수 있습니다.

코드를 몰라도 이 결정을 내 페이지에 붙여 쓸 수 있나요?

붙여 쓸 수 있습니다. 프레임워크나 빌드 도구 없이 캔버스 태그 하나와 순수 자바스크립트 함수만으로 움직여, 암호 zip 에서 바닐라 판을 꺼내 <script> 로 붙이면 그대로 동작합니다. 색은 각 판 scss 맨 위의 파스텔 토큰 변수만 바꾸면 되고, React 판도 같은 zip 에 들어 있습니다.

더 읽을거리

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

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