Three equations with no randomness in them. Run them twice from almost the same starting point and the two paths diverge until they have nothing to do with each other. Nothing was added: the divergence was already in the equations.
EXP-002
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 curve never crosses itself and never closes. It orbits one wing an unpredictable number of times, jumps to the other, and comes back. Which wing it picks next is the part nobody can compute in advance.
The maths
dx/dt = σ(y − x)
σ is the Prandtl number. This term pulls x toward y at a rate set by the fluid viscosity.
dy/dt = x(ρ − z) − y
ρ is the Rayleigh number, the temperature difference driving the convection. Above ρ ≈ 24.74 the system stops settling and never repeats.
dz/dt = xy − βz
β is a geometric factor. The classic values are σ=10, ρ=28, β=8/3, which is what runs here.
How it is built
Forward Euler integration with dt = 0.005, fourteen steps per frame so the curve advances at a readable speed. The trail keeps the last 2.600 points and fades along its length. The projection is simply x on screen-x and z on screen-y: the famous butterfly is the shadow of a three-dimensional curve.
The code, step by step
Cut straight from lab.js. These are the real lines that run above, not a simplified version.
01
Integrate the three equations
const dt = 0.005;
for (let i = 0; i < 14; i++) {
const dx = P.sigma * (y - x), dy = x * (P.rho - z) - y, dz = x * y - P.beta * z;
x += dx * dt; y += dy * dt; z += dz * dt;
trail.push([x, y, z]);
Forward Euler: derivatives at the current point, times dt, added on. Fourteen substeps per frame keep the curve smooth without making dt big enough to blow up. σ, ρ and β come from the sliders.
02
Rotate and project to 2D
const ang = M.in ? (M.x / w - 0.5) * 2.4 : 0;
const ca = Math.cos(ang), sa = Math.sin(ang);
const sc = Math.min(w, h) / 62, cx = w / 2, cy = h / 2 + h * 0.22;
const px = p => cx + (p[0] * ca - p[1] * sa) * sc;
const py = p => cy - p[2] * sc;
The attractor lives in three dimensions and the screen has two. Moving the cursor left and right rotates the view around the vertical axis before projecting, so you can look at the butterfly from the side.
03
Draw with a fading tail
for (let i = 1; i < trail.length; i++) {
ctx.strokeStyle = `rgba(${acc},${((i / trail.length) * 0.75).toFixed(3)})`;
ctx.beginPath();
ctx.moveTo(px(trail[i - 1]), py(trail[i - 1]));
ctx.lineTo(px(trail[i]), py(trail[i]));
ctx.stroke();
Opacity is proportional to the index, so the oldest segments are nearly invisible and the newest is bright. That gradient is what reads as motion in a still frame.
Why it matters
Lorenz found it in 1963 by restarting a weather simulation from a printout rounded to three decimals instead of six. The forecast came out completely different. That accident is why we have a word for the butterfly effect and why weather forecasts have a horizon.
The whole thing
Everything above, in one piece. This is the entire experiment as it lives in lab.js.
FULL
lab.js · lorenz
{ id: 'lorenz',
name: L('Lorenz attractor', 'Atractor de Lorenz'),
note: L('Deterministic and unpredictable. Never repeats, never escapes.',
'Determinista e impredecible. Nunca se repite, nunca se escapa.'),
params: [
{ key: 'rho', label: L('Rayleigh ρ', 'Rayleigh ρ'), min: 1, max: 60, step: 0.5, def: 28 },
{ key: 'sigma', label: L('Prandtl σ', 'Prandtl σ'), min: 1, max: 20, step: 0.5, def: 10 },
{ key: 'beta', label: L('Geometry β', 'Geometría β'), min: 0.5, max: 5, step: 0.05, def: 8 / 3 }
],
make() {
let x = 0.01, y = 0, z = 0, trail = [];
return {
reset() { x = 0.01; y = 0; z = 0; trail = []; },
step(ctx, w, h, t, acc, P, M) {
const dt = 0.005;
for (let i = 0; i < 14; i++) {
const dx = P.sigma * (y - x), dy = x * (P.rho - z) - y, dz = x * y - P.beta * z;
x += dx * dt; y += dy * dt; z += dz * dt;
trail.push([x, y, z]);
}
if (trail.length > 2600) trail.splice(0, trail.length - 2600);
// El mouse gira la proyección alrededor del eje vertical
const ang = M.in ? (M.x / w - 0.5) * 2.4 : 0;
const ca = Math.cos(ang), sa = Math.sin(ang);
const sc = Math.min(w, h) / 62, cx = w / 2, cy = h / 2 + h * 0.22;
const px = p => cx + (p[0] * ca - p[1] * sa) * sc;
const py = p => cy - p[2] * sc;
ctx.lineWidth = 1;
for (let i = 1; i < trail.length; i++) {
ctx.strokeStyle = `rgba(${acc},${((i / trail.length) * 0.75).toFixed(3)})`;
ctx.beginPath();
ctx.moveTo(px(trail[i - 1]), py(trail[i - 1]));
ctx.lineTo(px(trail[i]), py(trail[i]));
ctx.stroke();
}
}
};
}
},