Statistical mechanics

Brownian motion

A particle with no direction of its own, hit from every side by things too small to see. It goes nowhere in particular, and how far it gets follows a law you can write down.

EXP-007

Move the cursor over the canvas — it is part of the simulation.

Drag your finger across the canvas — it is part of the simulation.

What to look for

The paths look like they have structure, with long runs and tight clusters. They do not. Every step is independent of the last, and the apparent structure is what randomness actually looks like when you draw it.

The maths

⟨r²⟩ = 4Dt

Mean squared displacement grows linearly with time, not with time squared. That is the signature of diffusion as opposed to travel: doubling the distance takes four times as long.

D = kT / 6πηa

The Stokes-Einstein relation. It connects the jitter of one visible particle to Boltzmann's constant, and through it to the size of atoms.

How it is built

Six independent walkers, three steps per frame, each step a uniform random offset in x and y clamped to the canvas. Each keeps its last 520 positions and draws them as a fading polyline.

The code, step by step

Cut straight from lab.js. These are the real lines that run above, not a simplified version.

01

One step, no memory

k.x += (Math.random() - 0.5) * P.jump;
k.y += (Math.random() - 0.5) * P.jump;
if (M.in && P.drift) {                    // sesgo hacia el cursor
  const dx = M.x - k.x, dy = M.y - k.y, d = Math.hypot(dx, dy) + 1;
  k.x += (dx / d) * P.drift; k.y += (dy / d) * P.drift;
}
k.x = Math.max(4, Math.min(w - 4, k.x)); k.y = Math.max(4, Math.min(h - 4, k.y));
k.p.push([k.x, k.y]);

A uniform random offset per axis, independent of the previous step. That independence is the definition of a random walk. The drift slider adds a bias toward the cursor on top of it.

02

Draw the history

if (k.p.length > 520) k.p.splice(0, k.p.length - 520);
ctx.lineWidth = 1;
for (let i = 1; i < k.p.length; i++) {
  ctx.strokeStyle = `rgba(${acc},${((i / k.p.length) * 0.55).toFixed(3)})`;
  ctx.beginPath(); ctx.moveTo(k.p[i - 1][0], k.p[i - 1][1]);
  ctx.lineTo(k.p[i][0], k.p[i][1]); ctx.stroke();

The path is the interesting object, not the current position. Keeping 520 points and fading along the length shows where the walker has been without unbounded memory.

Why it matters

In 1905 Einstein wrote down how far such a particle should drift. Perrin measured it, got Avogadro's number out of it, and that settled the argument about whether atoms were real objects or just a convenient accounting device.

The whole thing

Everything above, in one piece. This is the entire experiment as it lives in lab.js.

FULL

lab.js · brownian

{ id: 'brownian',
      name: L('Brownian motion', 'Movimiento browniano'),
      note: L('Einstein 1905: the jitter that proved atoms are real.',
              'Einstein 1905: el temblor que demostró que los átomos existen.'),
      params: [
        { key: 'jump',    label: L('Step size', 'Tamaño del paso'), min: 1, max: 18, step: 0.5, def: 6 },
        { key: 'walkers', label: L('Walkers', 'Caminantes'),        min: 1, max: 14, step: 1,   def: 6 },
        { key: 'drift',   label: L('Cursor drift', 'Deriva al cursor'), min: 0, max: 2, step: 0.05, def: 0.5 }
      ],
      make(w, h) {
        let W = [];
        const fit = (n, w, h) => {
          while (W.length < n) W.push({ x: w / 2, y: h / 2, p: [] });
          if (W.length > n) W.length = n;
        };
        return {
          resize(w, h) { W.forEach(k => { k.x = w / 2; k.y = h / 2; k.p.length = 0; }); },
          reset() { W = []; },
          step(ctx, w, h, t, acc, P, M) {
            fit(P.walkers | 0, w, h);
            for (const k of W) {
              for (let i = 0; i < 3; i++) {
                k.x += (Math.random() - 0.5) * P.jump;
                k.y += (Math.random() - 0.5) * P.jump;
                if (M.in && P.drift) {                    // sesgo hacia el cursor
                  const dx = M.x - k.x, dy = M.y - k.y, d = Math.hypot(dx, dy) + 1;
                  k.x += (dx / d) * P.drift; k.y += (dy / d) * P.drift;
                }
                k.x = Math.max(4, Math.min(w - 4, k.x)); k.y = Math.max(4, Math.min(h - 4, k.y));
                k.p.push([k.x, k.y]);
              }
              if (k.p.length > 520) k.p.splice(0, k.p.length - 520);
              ctx.lineWidth = 1;
              for (let i = 1; i < k.p.length; i++) {
                ctx.strokeStyle = `rgba(${acc},${((i / k.p.length) * 0.55).toFixed(3)})`;
                ctx.beginPath(); ctx.moveTo(k.p[i - 1][0], k.p[i - 1][1]);
                ctx.lineTo(k.p[i][0], k.p[i][1]); ctx.stroke();
              }
              ctx.fillStyle = `rgba(${acc},0.9)`;
              ctx.beginPath(); ctx.arc(k.x, k.y, 2.2, 0, TAU); ctx.fill();
            }
          }
        };
      }
    },