Wave optics

Interference

Two sources emitting the same wave. Where the crests meet, light. Where a crest meets a trough, nothing. The pattern is not in either wave: it only exists in the sum.

EXP-001

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

Move the cursor over it. One of the sources follows you, so you can open and close the fringes by hand. Notice the hyperbolic shape of the dark bands: those are the points where the path difference is exactly half a wavelength.

The maths

ψ(r,t) = A/√r · cos(kr − ωt)

A single spherical source. Amplitude falls as 1/√r because energy spreads over a growing circumference; k = 2π/λ is the wavenumber and ω the angular frequency.

ψ_total = Σ ψ_i

Superposition. Waves add linearly, which is the whole reason interference exists. If they multiplied, there would be no pattern.

I = |ψ|²

What you actually see is the intensity: the square of the amplitude. Squaring is why the fringes are always positive and why the dark bands are exactly zero.

How it is built

The canvas is a grid of sample points spaced 6px apart. For each point I sum the contribution of every source, square it, and map the result to the radius and opacity of a dot. Nothing is precomputed: the whole field is evaluated every frame, which is roughly 10.000 evaluations at 60fps.

The code, step by step

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

01

Place the sources

const STEP = 6, k = TAU / P.lambda;
const src = [];
if (M.in) src.push([M.x, M.y, 1]);
else src.push([w * (0.5 + 0.22 * Math.cos(t * 0.25)), h * 0.36, 1]);
for (let i = 1; i < P.sources; i++) {
  const a = (i / (P.sources - 1)) * Math.PI + 0.6;
  src.push([w * (0.5 + 0.3 * Math.cos(a)), h * (0.55 + 0.22 * Math.sin(a)), 0.92]);

k = 2π/λ turns a wavelength in pixels into a phase per pixel. The first source follows your cursor when it is over the canvas; the rest are spread on an arc, and the slider decides how many.

02

Add every wave at that point

let psi = 0;
for (const s of src) {
  const r = Math.hypot(x - s[0], y - s[1]) + 8;
  psi += s[2] * (22 / Math.sqrt(r)) * Math.cos(k * r - t * P.speed);

Superposition, literally a running sum. r is the distance to the source, the +8 avoids the singularity at r=0, and 22/√r is the amplitude decay of a circular wave.

03

Square it and draw

const I = Math.min(1, psi * psi / 9);
if (I < 0.04) continue;
ctx.fillStyle = `rgba(${acc},${(0.09 + I * 0.72).toFixed(3)})`;
ctx.beginPath(); ctx.arc(x, y, 0.5 + I * 2.2, 0, TAU); ctx.fill();

Intensity is amplitude squared. Squaring turns a signed wave into something visible and makes the dark fringes exactly zero. I then drives both the dot radius and its opacity.

Why it matters

This is the experiment that forced physics to accept that light is a wave, and later that electrons are too. Young ran it with sunlight and a card in 1801. The same maths describes noise-cancelling headphones and the antenna array in a phone.

The whole thing

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

FULL

lab.js · interference

{ id: 'interference',
      name: L('Interference', 'Interferencia'),
      note: L('Two point sources. |ψ|² where their waves overlap.',
              'Dos fuentes puntuales. |ψ|² donde sus ondas se cruzan.'),
      params: [
        { key: 'lambda',  label: L('Wavelength λ', 'Longitud de onda λ'), min: 12, max: 70, step: 1, def: 34, unit: 'px' },
        { key: 'sources', label: L('Sources', 'Fuentes'),                 min: 2,  max: 5,  step: 1, def: 2 },
        { key: 'speed',   label: L('Speed', 'Velocidad'),                 min: 0,  max: 4,  step: 0.1, def: 2.2 }
      ],
      make() {
        return {
          step(ctx, w, h, t, acc, P, M) {
            const STEP = 6, k = TAU / P.lambda;
            const src = [];
            if (M.in) src.push([M.x, M.y, 1]);
            else src.push([w * (0.5 + 0.22 * Math.cos(t * 0.25)), h * 0.36, 1]);
            for (let i = 1; i < P.sources; i++) {
              const a = (i / (P.sources - 1)) * Math.PI + 0.6;
              src.push([w * (0.5 + 0.3 * Math.cos(a)), h * (0.55 + 0.22 * Math.sin(a)), 0.92]);
            }
            for (let x = STEP / 2; x < w; x += STEP) {
              for (let y = STEP / 2; y < h; y += STEP) {
                let psi = 0;
                for (const s of src) {
                  const r = Math.hypot(x - s[0], y - s[1]) + 8;
                  psi += s[2] * (22 / Math.sqrt(r)) * Math.cos(k * r - t * P.speed);
                }
                const I = Math.min(1, psi * psi / 9);
                if (I < 0.04) continue;
                ctx.fillStyle = `rgba(${acc},${(0.09 + I * 0.72).toFixed(3)})`;
                ctx.beginPath(); ctx.arc(x, y, 0.5 + I * 2.2, 0, TAU); ctx.fill();
              }
            }
            ctx.strokeStyle = `rgba(${acc},0.5)`; ctx.lineWidth = 1;
            for (const s of src) { ctx.beginPath(); ctx.arc(s[0], s[1], 3.5, 0, TAU); ctx.stroke(); }
          }
        };
      }
    },