Astrophysics

Quasar

A supermassive black hole eating a galaxy. The infalling gas heats up until it outshines everything around it, and two jets leave along the rotation axis at nearly light speed.

EXP-012

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

Drop the inclination toward zero to look straight down the jet. That is a blazar: the beaming points almost entirely at you and the object appears hundreds of times brighter than it is. Raise the beaming slider and watch one side of the disk take over.

The maths

v(r) ∝ r^(−1/2)

Keplerian rotation. The inner disk moves much faster than the outer, so the disk shears against itself and that friction is what heats it.

δ = 1/(γ(1 − β·cosθ))

The Doppler factor. Material coming toward you has its emission compressed in time and blueshifted, and both effects multiply.

I ∝ δ³

Observed brightness goes as the cube of that factor. It is why one side of the disk is visibly brighter here, and why most real quasars appear to have only one jet: the receding one is beamed away from us.

How it is built

Nine hundred particles on Keplerian orbits, projected with an inclination you control. Each one is shaded by its own Doppler factor computed from its instantaneous direction of motion relative to the viewer, which is why the asymmetry appears without being drawn in. The jets are separate particle streams along the axis.

The code, step by step

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

01

Keplerian shear

d.a += (2.2 / Math.pow(d.r, 1.5)) * 60;

Angular speed falls as r to the minus three halves, so the inner disk laps the outer one. That differential rotation is the friction that heats the gas, and it is one line.

02

Doppler beaming, per particle

const beta = P.beam * Math.min(1, (Math.min(w, h) * 0.12) / d.r);
const g = 1 / Math.sqrt(1 - beta * beta);
const cosTh = -Math.sin(ax) * Math.cos(incl);
const delta = 1 / (g * (1 - beta * cosTh));
const b = Math.min(1, Math.pow(delta, 3) * 0.18);
ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${b.toFixed(3)})`;

Each particle computes its own Doppler factor from its instantaneous motion relative to the viewer, then brightness goes as that cubed. The bright side is not painted in: it emerges from every particle answering the same formula.

Why it matters

Quasars were found in 1963 as radio sources that looked like stars but had impossible redshifts. Resolving that meant accepting they were billions of light years away and therefore brighter than entire galaxies, powered by something only a few light hours across. That something turned out to be a black hole.

The whole thing

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

FULL

lab.js · quasar

{ id: 'quasar',
      name: L('Quasar', 'Cuásar'),
      note: L('An accretion disk and two jets. One side is brighter, and that is relativity.',
              'Un disco de acreción y dos chorros. Un lado brilla más, y eso es relatividad.'),
      params: [
        { key: 'incl', label: L('Inclination', 'Inclinación'), min: 0.05, max: 1.5, step: 0.02, def: 0.42 },
        { key: 'jet',  label: L('Jet power', 'Potencia del chorro'), min: 0, max: 3, step: 0.05, def: 1.2 },
        { key: 'beam', label: L('Beaming β', 'Beaming β'), min: 0, max: 0.9, step: 0.02, def: 0.55 }
      ],
      make(w, h) {
        let disk = [], jet = [], phi = 0;
        const seed = (w, h) => {
          disk = []; jet = [];
          const R = Math.min(w, h);
          for (let i = 0; i < 900; i++) {
            const u = Math.pow(Math.random(), 0.6);
            disk.push({ r: R * (0.07 + u * 0.34), a: Math.random() * TAU });
          }
        };
        seed(w, h);
        return {
          resize(w, h) { seed(w, h); },
          reset() { seed(w, h); jet = []; },
          step(ctx, w, h, t, acc, P, M) {
            const incl = M.in ? 0.05 + (M.y / h) * 1.45 : P.incl;
            if (M.in) phi = (M.x / w) * TAU; else phi += 0.002;
            const cx = w / 2, cy = h / 2;
            const ky = Math.sin(incl), kz = Math.cos(incl);
            const rgb = acc.split(',').map(Number);

            // Chorros: colimados perpendicular al disco, arriba y abajo
            if (P.jet > 0 && jet.length < 260) {
              for (let k = 0; k < 2; k++)
                jet.push({ z: 0, dir: k ? 1 : -1, o: (Math.random() - 0.5) * 14, v: 2 + Math.random() * 3 });
            }
            jet = jet.filter(j => {
              j.z += j.dir * j.v * P.jet;
              const far = Math.abs(j.z) > Math.min(w, h) * 0.62;
              const y = cy - j.z * kz, x = cx + j.o;
              const fade = 1 - Math.abs(j.z) / (Math.min(w, h) * 0.62);
              ctx.fillStyle = `rgba(${acc},${(fade * 0.5).toFixed(3)})`;
              ctx.beginPath(); ctx.arc(x, y, 1.5, 0, TAU); ctx.fill();
              return !far;
            });

            // Disco: rotación kepleriana, más rápido hacia adentro
            for (const d of disk) {
              d.a += (2.2 / Math.pow(d.r, 1.5)) * 60;
              const ax = d.a + phi;
              const x = d.r * Math.cos(ax), y = d.r * Math.sin(ax);
              const sx = cx + x, sy = cy + y * ky;
              // Beaming Doppler: δ = 1/(γ(1 − β·cosθ)). El lado que se acerca
              // brilla mucho más. Es el mismo efecto que hace que un solo chorro
              // se vea en la mayoría de los cuásares reales.
              const beta = P.beam * Math.min(1, (Math.min(w, h) * 0.12) / d.r);
              const g = 1 / Math.sqrt(1 - beta * beta);
              const cosTh = -Math.sin(ax) * Math.cos(incl);
              const delta = 1 / (g * (1 - beta * cosTh));
              const b = Math.min(1, Math.pow(delta, 3) * 0.18);
              ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${b.toFixed(3)})`;
              ctx.fillRect(sx, sy, 1.6, 1.6);
            }

            // El motor central
            const R0 = Math.min(w, h) * 0.05;
            const g2 = ctx.createRadialGradient(cx, cy, 0, cx, cy, R0);
            g2.addColorStop(0, `rgba(255,255,255,0.9)`);
            g2.addColorStop(0.4, `rgba(${acc},0.7)`);
            g2.addColorStop(1, `rgba(${acc},0)`);
            ctx.fillStyle = g2;
            ctx.beginPath(); ctx.arc(cx, cy, R0, 0, TAU); ctx.fill();
            ctx.fillStyle = '#000';
            ctx.beginPath(); ctx.arc(cx, cy, R0 * 0.28, 0, TAU); ctx.fill();

            ctx.font = '11px monospace';
            ctx.fillStyle = `rgba(${acc},0.55)`;
            ctx.fillText('δ = 1/(γ(1 − β·cosθ))     brightness ∝ δ³', 12, h - 28);
            ctx.fillStyle = `rgba(${acc},0.9)`;
            ctx.fillText('inclination ' + (incl * 57.3).toFixed(0) + '°     β = ' + P.beam.toFixed(2), 12, h - 12);
          }
        };
      }
    },