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.
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();
}
}
};
}
},