GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Snowflake Generator, 9 Ways (DLA, No Library)

No library, no WebGL — repeat one moment, a wandering particle touching the crystal and sticking, and a six-fold snowflake grows itself, and that's the

Auto-plays · click a tile to jump to its section · all nine in one zip

One Sticking Probability, Nine Snowflakes in Order

The nine scenes climb one axis: what decides where a particle sticks. 01 sticks on first contact with STICK=1.0, the plainest possible DLA rule, so the hexagonal outline is the whole demonstration. 02 keeps that same random-walk aggregation but re-rolls the sticking chance by how far the new cell sits from the seed — TIP_BIAS=2.6 turns the blunt cluster into dendrite arms. 03 runs two of 02's crystals side by side and changes nothing but that same bias width, showing the fern-to-needle habit split as one parameter rather than two separate rules. 04 folds the walk itself onto a 60-degree sector instead of biasing where particles land, so the six-fold symmetry comes from geometry, not from six independent simulations. 05 moves the seed from a single center point to the whole window frame and lets a temperature field pull each walker toward the nearest edge — aggregation runs inward instead of outward. 06 changes nothing about how cells stick and instead records when each one stuck, turning that timestamp into growth-ring color. 07 lets a second seed compete with the first at a click, 08 runs a melt-refreeze cycle on a grown crystal, and 09 stages one large crystal like a macro photograph. Read them in order and the ninth scene isn't a new algorithm — it's the same contact rule from item 01, still running, with a camera and a light rig built around it.

Code per item

Each snippet below is lifted verbatim from that item's own script tag, constant names and all, not summarized into pseudocode — drop it into a file and it grows the same crystal the panel above it does.

01The basic hex cluster — one layer of DLA, freeze on contact

Seed the crystal with a small hexagonal cluster at center, launch particles from an outer ring, and let each take a six-direction random walk across a hex grid until it touches the crystal and sticks. No other rule runs, and the hollow concave corners and sharp tips still appear on their own.

The basic hex cluster — one layer of DLA, random-walk particles sticking where they touch
Open in a new tab

01 · The basic hex cluster — one layer of DLA, random-walk particles sticking where they touch — Measured area 43.511% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

STICK=1.0 is the entire shape-making rule

function step(t) {
  for (var s = 0; s < 20; s++) {          // steps per frame — walk speed
    for (var w = walk.length - 1; w >= 0; w--) {
      var p = walk[w];
      if (rand() < 0.25) p[2] = Math.floor(rand() * 6);   // occasional direction re-roll — isotropy
      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; }  // wandered off-grid — drop and relaunch
      if (rand() < 0.2) p[3] += (rand() - 0.5) * 0.7;
      p[0] = i; p[1] = j;
      if (touching(i, j)) {
        if (rand() < STICK) {              // touched — freeze into that cell with probability 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);                 // drifted too far — unlikely to come back
      }
    }
  }

02Dendrite branches — six tips forking into a dense tree

Bias the sticking probability toward whichever cell sits farthest from the seed and a blunt blob stops being blunt — thin, dense branches reach outward the way real snowflake dendrites do, a simplified stand-in for the Mullins-Sekerka instability.

Dendrite arms — six tips forking into a dense branching tree
Open in a new tab

02 · Dendrite arms — six tips forking into a dense branching tree — Measured area 44.129% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

TIP_BIAS re-rolls the same contact rule by radius

// ── Tip-weighted sticking — the farther from center (closer to a tip), the higher the odds ──
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;
}

function step(t) {
  for (var s = 0; s < 26; 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]);

03Needles and ferns — one bias width splits both habits

Run two crystals side by side that share the exact same tip-weighted aggregation and differ only in how wide that bias curve is — a narrow gap between tip and interior odds fills out into a bushy fern, a wide gap sticks almost only at the tip and grows long needle bundles.

Needles and ferns — shift the humidity band and ferns turn to needles
Open in a new tab

03 · Needles and ferns — shift the humidity band and ferns turn to needles — Measured area 41.614% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

bias is the only value that tells a fern from a needle

// tip-weighted sticking — bias sets the crystal's habit (width)
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;
}

04Six-fold mirror — fold one sector, stamp the rest

Fold each particle's angle onto a 60-degree period so the entire random walk is confined to one sector, then stamp the stuck cells out through 12 rotated-and-mirrored copies — a sixth of the compute, and the flake's real D6 symmetry lands with not one pixel off.

The six-fold mirror — fold by 60 degrees and grow six times faster
Open in a new tab

04 · The six-fold mirror — fold by 60 degrees and grow six times faster — Measured area 40.29% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotateImageDatarequestAnimationFrame

Folding the angle before the walk ever leaves the sector

// Fold the angle onto (-SECTOR/2 .. SECTOR/2) on a 60-degree period — rotational folding (C6).
// A step that would exit the wedge is folded back in, so the simulated space is one sector.
function foldTh(th) {
  var P = SECTOR, m = (th + P * 0.5) % P;
  if (m < 0) m += P;
  return m - P * 0.5;
}
// relative coordinates → folded (angle, radius)
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];
}
function cellOcc(fx, fy) {
  var fo = fold(fx, fy);
  if (fo[1] > N) return 0;
  var gx = AX + Math.round(Math.sin(fo[0]) * fo[1]);
  var gy = AY + Math.round(Math.cos(fo[0]) * fo[1]);
  if (gx < 1 || gx >= WS - 1 || gy < 1 || gy >= HS - 1) return 0;
  return occ[gy * WS + gx];
}

05Frosted window — fractal ice creeping in from the edge

Seed the entire window frame instead of a center point, fold a temperature field that resists near the middle into the sticking probability, and drift each walker roughly half its steps toward the nearest edge — aggregation creeps inward the way real window frost does.

Frosted window — fractal ice creeping inward from the frame edge
Open in a new tab

05 · Frosted window — fractal ice creeping inward from the frame edge — Measured area 45.515% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

BIAS drifts the walk, COLD resists the center

function stepWalker(w) {
  var d;
  if (rand() < BIAS) {
    // temperature-field drift — one step toward the nearest frame edge (direction 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;   // a random step — without this, freezing runs in straight lines and never branches
  }
  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;
  // temperature field — closer to center (smaller dist) freezes less readily, thinning the branches
  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;

06Growth-ring coloring — etching contact order as pastel rings

Nothing about the sticking rule changes here — the only new step is recording the order a cell joined the crystal, then coloring by that order through a pastel ramp, so growth reads like tree rings, cells that stuck at the same moment sharing a band.

Lattice coloring — etching the age of each cell as pastel rings
Open in a new tab

06 · Lattice coloring — etching the age of each cell as pastel rings — Measured area 9.325% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

order.push is the only line that gives cells their age

w.gx = nx; w.gy = ny;
var i = idx(nx, ny);
if (occ[i]) return false;
var nc = (occ[idx(nx - 1, ny)] ? 1 : 0) + (occ[idx(nx + 1, ny)] ? 1 : 0) +
          (occ[idx(nx, ny - 1)] ? 1 : 0) + (occ[idx(nx, ny + 1)] ? 1 : 0);
if (!nc) return false;
// branch probability — the fewer neighbors already touching, the more readily a cell sticks (DLA dendrite bias)
var p = nc === 1 ? 1.0 : nc === 2 ? 0.62 : nc === 3 ? 0.22 : 0.10;
if (rand() >= p) return false;
occ[i] = 1;
order.push(i);
age[i] = order.length;                  // age = the order it stuck in — the sole source of color
respawn(w);
return true;

07Pointer seeding — a second crystal competes at your click

Drop another seed wherever you click or tap and particles start competing between two crystals for the same walkers — the sticking rule never changes, a particle sticks to whichever crystal it touches, so arms from each crystal tangle together where they meet.

Pointer seeding — a second crystal grows wherever you click
Open in a new tab

07 · Pointer seeding — a second crystal grows wherever you click — Measured area 34.032% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermoveImageDatarequestAnimationFrame

plantSeed never changes the rule, only where it starts

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);   // freeze one ring around the seed to give it a starting edge
  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 });
}

08Melt and refreeze — peeling the rim, keeping the core

Run a temperature cycle over a fully grown crystal: on the warm half, probabilistically peel exposed surface cells into drifting particles; on the cold half, those particles re-stick. The older core survives at a lower melt odds while only the rim keeps regrowing.

Melt-and-refreeze — peeling the outermost particles and re-sticking them
Open in a new tab

08 · Melt-and-refreeze — peeling the outermost particles and re-sticking them — Measured area 37.253% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

pCore protects the old center while the rim keeps cycling

// ── Melting — surface cells (exposure ≥ 3) peel off as drifting particles at a temperature-driven odds ──
var drifters = [];   // {x, y, vx, vy, born, hot} particles that thawed and are drifting
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;                                  // fully buried cells never melt
    var age = now - tStick[id];
    var pCore = Math.max(0.02, 1 - age / 5200);            // an old core melts at much lower odds
    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({

09Macro-photo scene — one flake staged like microscopy

Light a dark stage with a central radial gradient, grow one crystal past half the frame on a timelapse, and keep the surface slowly reshuffling once growth completes so a depth-of-field macro shot never quite sits still.

Macro-photo scene — one flake in shallow depth of field on a dark stage
Open in a new tab

09 · Macro-photo scene — one flake in shallow depth of field on a dark stage — Measured area 36.536% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createRadialGradientImageDatarequestAnimationFrame

reshuffle keeps a finished crystal's rim quietly cycling

// ── Reshuffle — a finished crystal's surface slowly wriggles (a fine melt-and-restick cycle) ──
function reshuffle(now) {
  for (var i = 0; i < RESHUFFLE_RATE * 10; 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 >= 5 || n === 0) continue;            // buried or isolated cells stay put
    if (distC[id] < maxR * 0.55) continue;      // the core is stable — only the rim moves
    if (rand() < 0.35) { frozen[id] = 0; }      // peel it
    // re-stick it nearby so the texture shifts by a hair
    var nx2 = nbX(y), ny2 = nbY(y);
    var k = (rand() * 6) | 0;
    var ax = x + nx2[k], ay = y + ny2[k];
    if (inside(ax, ay) && !frozen[idx(ax, ay)] && frozenNb(ax, ay) > 0) stick(ax, ay, now);
  }
}

Filling a pre-drawn outline is coloring, not diffusion-limited aggregation — where it breaks

Pre-drawing an outline and filling it isn't DLA

A hexagonal shape that comes from filling particles inside a pre-drawn snowflake outline is coloring, not aggregation — the hollow concave corners and sharp tips in every item on this page are the direct output of the contact rule, not decoration applied afterward. Skip the random walk and touching check and there's nothing left to call diffusion-limited.

Ballistic particles produce a dense blob, not tips

Firing particles straight at the center instead of letting them wander removes the "diffusion" from diffusion-limited aggregation — a random walk is what lets particles get turned away from the interior and caught at a tip instead, and a straight shot produces a dense, tip-free blob no matter how the sticking probability is tuned.

A square 4-neighbor grid fakes hex symmetry badly

Items 01 through 03 read neighbors off row-parity DI/DJ tables built for a true hex grid, and item 04's angle-folding assumes the same six-direction geometry underneath. Simulating on a square grid and rotating a copy 60 degrees afterward doesn't reproduce that texture — the walk itself has to move in six directions, not four.

clearRect every frame erases the afterglow that keeps 30%+ painted

None of the nine items call clearRect on the crystal layer — a darkening overlay hangs over it instead, so drifted particles' trails fade in as afterglow rather than vanishing outright. Redraw from scratch every frame and painted area drops well under the 34-45% range the other eight items measure (06's colored-crystal-only frame runs a deliberately narrower 9.3%, covered in the FAQ below), along with the 23-of-23 transitioning frames.

Why snowflakes grow by DLA? — random-walk aggregation and six-fold symmetry

A random walk that freezes on contact is the whole mechanism

Every item on this page traces back to one operation: launch a particle from outside the crystal, let it take a random walk across a hex grid, and freeze it the instant it touches an already-frozen cell. 01 runs that operation with STICK=1.0 and nothing else, and the result is a snowflake generator online free of any pre-drawn outline: hollow concave corners and sharp tips that the contact rule alone produces — the same diffusion-limited aggregation Witten and Sander described for how frost and mineral dendrites actually form.

Tip bias is what turns a blob into a branch

02 doesn't add a second mechanism for its dendrite arms — it re-rolls the same contact probability by how far the new cell sits from the seed, so tips reaching outward catch particles far more easily than concave interior spots. That single-parameter bias (TIP_BIAS=2.6) is a simplified version of the Mullins-Sekerka instability, the real reason a growing ice surface amplifies any small bump into a full branch instead of smoothing itself flat.

One bias width maps the whole Nakaya diagram

03 changes nothing about the aggregation rule between its fern and its needle crystal except the width of that bias curve — a narrow gap between tip and interior odds fills out sideways into a fern, a wide gap starves the interior and grows long needles. Compressed into one number, that's the same axis Ukichiro Nakaya's habit diagram maps across temperature and supersaturation for real snow crystals.

Folding a sector is geometry, not a second simulation

04 never runs six separate crystals to get six-fold symmetry — it folds every particle's angle onto a 60-degree period so the random walk only ever explores one wedge, then stamps that wedge out through 12 rotated-and-mirrored copies. The D6 symmetry real snowflakes show isn't imposed after the fact here; it's the boundary condition the walk obeys while it grows.

Three variants that swap bias width and mirror count, tuned for reduced-motion

One constant, three different crystals

Variant Changed value What it feels like
Dendrite bias 02 tip bias 0.3 → 0.6 Branches pack tighter, closer to a real snowflake's lace
Aggregation speed 01 particle rate 220/sec → 700/sec The crystal visibly speeds up and the demo runs shorter
Symmetry fold 04 sector angle 60° → 30° Half the sector doubles the stamped copies and thins the branches

Nothing here is drawn by hand, so widening one constant reshapes the whole crystal instead of asking someone to redraw a snowflake generator svg frame by frame.

What reduced-motion keeps

A @media (prefers-reduced-motion: reduce) check decides whether the requestAnimationFrame loop ever starts — when a visitor has asked for less motion, every item draws one fixed frame instead and adds an .is-still class to its canvas. Growth stops, but the crystal itself, mid-formation, still renders rather than leaving a blank panel.

Pointer input never blocks a keyboard visitor

07's pointer seeding reads pointerdown on the field element, which also carries a tabindex so a keyboard visitor landing on the demo isn't stuck behind a mouse-only interaction — without pointer input, AUTO_SEEDS plants seeds on a fixed timetable instead. The other eight items read no pointer or keyboard input at all, since their growth runs entirely on a clock. Every code block above is copied from the running demo's actual source, fcyyjg6v not retyped, and each item's full file (HTML, CSS, and JS) sits inside the zip behind the password on this page.

Questions before you wire a DLA snowflake into a page

How is a DLA snowflake generator different from a snowflake generator svg tool

A snowflake generator svg tool typically draws one fixed shape from a formula and exports it as a static vector — nothing here is pre-drawn. Every crystal on this page is the pixel-by-pixel outcome of thousands of random-walk particles freezing on contact frame after frame, rendered live to a canvas rather than composed as vector paths.

Can I paste this snowflake generator code in and use it as-is

Yes — nothing in this set of nine reaches out to an external library, WebGL, or CDN, so the canvas markup and script tag can be lifted into any page and it grows the same crystal it does here.

Why does item 06 paint the smallest area but still read so vivid

Item 06 records the lowest painted area of the nine at 9.3% because it colors only the crystal body itself, with no snow, haze, or bloom layered over an empty stage the way the other eight items fill their frame. That same narrow focus is what pushes its average frame intensity to 73.3, second only to item 03's needle-and-fern pair at 75.9 — every lit pixel belongs to the crystal, so the color reads at near-full strength.

Sources: MDN — CanvasRenderingContext2D · MDN — ImageData · MDN — Window.requestAnimationFrame() · MDN — prefers-reduced-motion · Wikipedia — Diffusion-limited aggregation · Wikipedia — Snowflake · a value-noise elevation field folded the same way item 05 folds a temperature field · a reaction-diffusion field that grows Turing patterns from the same neighbor-counting logic as item 06's rings.

A photo-style hero and a click-to-grow crystal — where these nine actually get used

A winter campaign or a snow-themed landing page reaches for item 09 first — one flake staged like microscopy on a dark background is already a finished shot, so a headline is the only thing left to add. A hero area built for visitors who like to click does better with item 07: a second crystal competing at the pointer turns a passive diagram into something a reader triggers.

A science write-up explaining how humidity shapes a crystal is where item 03 belongs — sliding one bias-width value carries the shape from a fern to a needle in view, with no separate chart needed. Each of the three lives in a single file with nothing to compile first, so any one of them drops into a static section exactly as it is.

Enter the archive password

The password is inside this article. You will find it as you read.