Electromagnetism

Why light exists

A charge sitting still has field lines going straight out forever. Shake it, and a kink runs down every line at the speed of light and never comes back. That kink is light, and this is a drawing of it happening.

EXP-026

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

Start on the dipole and follow one single line outward with your eye. The bends do not stay put: they march away at a fixed speed and leave. Then switch to hard turns, where the charge reverses direction abruptly, and each reversal fires one clean pulse. Raising v/c compresses the pattern ahead of the motion, which is the beginning of relativistic beaming.

The maths

t_ret = t − r/c

The retarded time. The field at distance r right now was set by where the charge was r/c ago, because nothing about the charge can travel faster than c. Everything else follows from taking this seriously.

línea de campo ∥ (r⃗ − r⃗(t_ret))

Purcell's construction: each field line still points radially away from the retarded position, not the current one. Draw that for a moving charge and the kink appears on its own, with no extra physics inserted.

E_rad = q·a·sinθ / (4πε₀c²r)

The transverse field in the kink. Note the r in the denominator, not r²: the radiation field falls off as 1/r while the static field falls as 1/r². Far enough away, the kink is all that is left, and that is why light reaches us from other galaxies.

P = q²a² / (6πε₀c³)

Larmor: radiated power goes as acceleration squared. No acceleration, no light. A charge moving at constant velocity radiates nothing at all, no matter how fast it goes.

How it is built

The trajectory is a pure function of time, so the entire past can be computed instead of waited for. A history buffer is filled at a fixed timestep chosen so that the index into it is exactly the distance divided by c, which makes the retarded lookup a single array access instead of a search. Prefilling matters: without it the canvas sits blank for about five seconds while the buffer accumulates, and the first version did exactly that.

The code, step by step

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

01

The trajectory as a pure function of time

const posAt = (tt, mode, beta, w, h) => {
  // Lo único que se ve es la longitud de onda λ = c·T, así que se fija a
  // una fracción del lienzo para que quepan varias, y la amplitud sale de
  // ahí: A = βλ/2π. Esa es la relación real de un dipolo, y explica por
  // qué la carga casi no se mueve mientras el campo ondea entero.
  const LAM = Math.min(w, h) * 0.38, om = (TAU * C) / LAM, A = (beta * C) / om;
  if (mode === 0) return [w / 2 + A * Math.sin(om * tt), h / 2];
  if (mode === 1) return [w / 2 + A * Math.cos(om * tt), h / 2 + A * Math.sin(om * tt)];
  // Rebote duro: velocidad constante y reversiones bruscas. Su recorrido
  // se elige para que los quiebres queden separados ~200px y se vean varios.
  const AB = LAM * 0.18, T = (AB * 4) / (beta * C), ph = ((tt % T) + T) % T;
  const x = ph < T / 2 ? -AB + (ph / (T / 2)) * 2 * AB : AB - ((ph - T / 2) / (T / 2)) * 2 * AB;
  return [w / 2 + x, h / 2];

Writing the motion as position(t) rather than stepping it forward is what makes everything else easy: the past is computable rather than something you have to have lived through. Three modes — an oscillating dipole, a circle, and a hard bounce with abrupt reversals.

02

Prefill the past

if (hist.length < need)
  for (let i = hist.length; i < need; i++) hist.push(posAt(st - i * DT, mode, P.beta, w, h));
// Reloj propio de paso fijo: así el índice del historial ES la distancia
acct += 1 / 60;
while (st < acct) {
  st += DT;
  hist.unshift(posAt(st, mode, P.beta, w, h));

The first version only recorded positions as they happened, which meant about 600 samples had to accumulate before a single line could be drawn — five seconds of blank canvas, and I only caught it by running the experiment headless and finding it drew nothing. Since the motion is analytic, the history is just evaluated backwards at startup. The fixed timestep is chosen so the array index equals distance divided by c, making the retarded lookup a single array access.

03

Draw one field line

for (let r = 6; r < RMAX; r += dr) {
  const idx = Math.min(hist.length - 1, Math.round(r / (C * DT)));
  const p = hist[idx];
  const x = p[0] + r * ct, y = p[1] + r * sn;
  started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);

Walk outward along a fixed angle. At each radius, look up where the charge was when the news left, and offset from there. Nothing in this loop knows about acceleration or radiation — the kinks appear because the retarded position keeps moving. That is the entire Purcell argument, expressed as four lines of drawing code.

Why it matters

This is the answer to why light exists at all, and it is not usually shown as a picture. Radio antennas are this with electrons in a wire, synchrotrons are this on a curve, and the blue of the sky is this happening in every air molecule. Purcell put the construction in his textbook in 1965 because he thought the algebra was hiding it.

The whole thing

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

FULL

lab.js · radiation

{ id: 'radiation',
      name: L('Why light exists', 'Por qué existe la luz'),
      note: L('Field lines drawn from the retarded position. Shake a charge and you see the kink leave.',
              'Líneas de campo desde la posición retardada. Sacude una carga y ves salir el quiebre.'),
      params: [
        { key: 'mode',  label: L('Motion', 'Movimiento'), min: 0, max: 2, step: 1, def: 0 },
        { key: 'beta',  label: L('Speed (v/c)', 'Rapidez (v/c)'), min: 0.05, max: 0.75, step: 0.01, def: 0.35 },
        { key: 'lines', label: L('Field lines', 'Líneas de campo'), min: 8, max: 40, step: 2, def: 24 }
      ],
      make() {
        // Construcción de Purcell: una línea de campo apunta radialmente desde
        // donde ESTABA la carga hace r/c, no desde donde está. Al acelerar, esa
        // discrepancia se acumula en un quiebre que viaja a c. El quiebre es la luz.
        const DT = 1 / 120, C = 210;         // px por segundo
        let hist = [], st = 0, acct = 0;
        const posAt = (tt, mode, beta, w, h) => {
          // Lo único que se ve es la longitud de onda λ = c·T, así que se fija a
          // una fracción del lienzo para que quepan varias, y la amplitud sale de
          // ahí: A = βλ/2π. Esa es la relación real de un dipolo, y explica por
          // qué la carga casi no se mueve mientras el campo ondea entero.
          const LAM = Math.min(w, h) * 0.38, om = (TAU * C) / LAM, A = (beta * C) / om;
          if (mode === 0) return [w / 2 + A * Math.sin(om * tt), h / 2];
          if (mode === 1) return [w / 2 + A * Math.cos(om * tt), h / 2 + A * Math.sin(om * tt)];
          // Rebote duro: velocidad constante y reversiones bruscas. Su recorrido
          // se elige para que los quiebres queden separados ~200px y se vean varios.
          const AB = LAM * 0.18, T = (AB * 4) / (beta * C), ph = ((tt % T) + T) % T;
          const x = ph < T / 2 ? -AB + (ph / (T / 2)) * 2 * AB : AB - ((ph - T / 2) / (T / 2)) * 2 * AB;
          return [w / 2 + x, h / 2];
        };
        return {
          reset() { hist = []; st = 0; acct = 0; },
          step(ctx, w, h, t, acc, P, M) {
            const mode = P.mode | 0;
            const RMAX = Math.hypot(w, h) * 0.52;
            const need = Math.ceil(RMAX / (C * DT)) + 4;
            // El movimiento es una función pura del tiempo, así que el pasado se
            // puede calcular: se precarga y el campo aparece formado desde el
            // primer cuadro en vez de tardar segundos en llenarse.
            if (hist.length < need)
              for (let i = hist.length; i < need; i++) hist.push(posAt(st - i * DT, mode, P.beta, w, h));
            // Reloj propio de paso fijo: así el índice del historial ES la distancia
            acct += 1 / 60;
            while (st < acct) {
              st += DT;
              hist.unshift(posAt(st, mode, P.beta, w, h));
              if (hist.length > need) hist.length = need;
            }

            const N = P.lines | 0, dr = C * DT * 2;
            for (let k = 0; k < N; k++) {
              const th = (k / N) * TAU;
              const ct = Math.cos(th), sn = Math.sin(th);
              ctx.beginPath();
              let started = false;
              for (let r = 6; r < RMAX; r += dr) {
                const idx = Math.min(hist.length - 1, Math.round(r / (C * DT)));
                const p = hist[idx];
                const x = p[0] + r * ct, y = p[1] + r * sn;
                started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
              }
              ctx.strokeStyle = `rgba(${acc},0.42)`;
              ctx.lineWidth = 1;
              ctx.stroke();
            }

            const now = hist[0];
            ctx.fillStyle = `rgba(${acc},1)`;
            ctx.beginPath(); ctx.arc(now[0], now[1], 4.5, 0, TAU); ctx.fill();
            ctx.strokeStyle = `rgba(${acc},0.14)`;
            ctx.beginPath(); ctx.arc(now[0], now[1], 5.5, 0, TAU); ctx.stroke();

            ctx.font = '11px monospace';
            ctx.fillStyle = `rgba(${acc},0.55)`;
            ctx.fillText(['dipole — the textbook antenna',
                          'circular — this is synchrotron light',
                          'hard turns — every kink is a burst'][mode], 12, h - 28);
            ctx.fillStyle = `rgba(${acc},0.95)`;
            ctx.fillText('v/c = ' + P.beta.toFixed(2) +
              '   lines point at the charge as it was r/c ago', 12, h - 12);
          }
        };
      }
    },