GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

N Body Simulation, 9 Ways (Galaxy, No Library)

No library, no WebGL — integrate Newtonian gravity on every star, every frame, and two galaxies shear past, throwing tidal tails as they merge: cosmic-scale

Auto-plays · click a tile to jump to its section · all nine in one zip

Before and After the Collision: Nine Scenes on a Timeline

The nine scenes climb one axis: how much of the gravity you actually compute. 01 is the base collision — every star pulls every other star through the full O(N²) pair sum, and the tidal tails that appear at the shear are nobody's drawing. 02 and 03 stay full pair-sum but change the stage: a still disk winding itself into spiral arms, and a sphere of stars where each one orbits on its own plane. 04 is the one approximation of the set — star masses are binned into a radial grid instead of summed pair by pair, and the freed frame budget pays for a live rotation-curve graph. 05 returns to full pairs but starts the clock after the first passage, 06 tracks a single star's orbit through the same kind of shear, 07 lets your pointer throw new stars into the same gravity field, 08 stops erasing afterimages so hundreds of orbits stack into the arms, and 09 stages three galaxies over pre-baked nebula sprites. It shares nothing with a flock bound by three neighbor rules and no shared field at all — there each particle steers by its neighbors alone, here every star feels every other star through the full pair sum — and nothing with a slime mold network grown from deposits and evaporation, where the field is a map the swarm writes for itself. A galaxy gravity simulation is compelling for the opposite reason: the field is nothing but the stars, recomputed every frame.

Code per item

Every snippet below comes straight out of that item's own script tag — constant names intact, comments translated, identifiers untouched. Eight of the nine items compute star-to-star gravity as the full pair sum; only item 04 bins mass into a grid, and its section says so out loud.

01Two galaxies collide — shear past, throw tidal tails, merge

Two disks of 170 stars each approach from opposite corners, every star carrying position and velocity in plain arrays (masses are uniform in this item, folded into G), and every frame sums all 57,630 star pairs of Newtonian acceleration — the moment the disks shear, outer stars shuttle between the two cores, lose energy, and are flung outward into tidal tails that form on their own. Nothing is drawn from a spiral formula; the tails grow out of per-star orbital integration, painted at 20.125% cumulative area — the smallest of the nine — with a 16.048% single-frame jump at the first shear and 59.3 average intensity.

Two galaxies collide — shear past, throw tidal tails, merge
Open in a new tab

01 · Two galaxies collide — shear past, throw tidal tails, merge — Measured area 20.125% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DarcrequestAnimationFrame

Every pair, once — Newton's third law applied to both stars

  function step() {
    // Full pair gravity — a_i = Σ_j G·(x_j - x_i)/(|d|²+ε²)^1.5. Symmetric: compute each pair once, apply to both
    var AX = new Array(N), AY = new Array(N);
    for (var i = 0; i < N; i++) { AX[i] = 0; AY[i] = 0; }
    for (var i = 0; i < N; i++) {
      for (var j = i + 1; j < N; j++) {
        var dx = X[j] - X[i], dy = Y[j] - Y[i];
        var d2 = dx * dx + dy * dy + SOFT2;
        var inv = 1 / Math.sqrt(d2);
        var f = G * inv * inv * inv;          // f = G / d³ — dx·f is the acceleration component
        AX[i] += dx * f; AY[i] += dy * f;
        AX[j] -= dx * f; AY[j] -= dy * f;
      }
    }
    for (var i = 0; i < N; i++) {
      VX[i] += AX[i] * DT; VY[i] += AY[i] * DT;
      X[i] += VX[i] * DT;  Y[i] += VY[i] * DT;
    }
  }

Seeding a disk whose edge sits at circular-orbit speed

  function placeDisk(cx, cy, R, count, phase, dvx, dvy, dir, rnd, sc) {
    // Central mass M — sized so the disk edge (r=R) sits at circular-orbit speed v=sqrt(M/R)
    var M = R * R * 0.90;
    for (var i = 0; i < count; i++) {
      var r = R * Math.sqrt(rnd());           // uniform surface density — r² uniform
      var a = phase + (i / count) * Math.PI * 2 + rnd() * 0.5;
      var v = Math.sqrt(M / Math.max(r, 6)) * dir;
      X.push(cx + Math.cos(a) * r);
      Y.push(cy + Math.sin(a) * r);
      VX.push(dvx * sc * 0.30 + -Math.sin(a) * v + (rnd() - 0.5) * 2);
      VY.push(dvy * sc * 0.30 +  Math.cos(a) * v + (rnd() - 0.5) * 2);
      N++;
    }
    // One core star per galaxy center (mass shown by size — array stays uniform)
  }

02Spiral arms form — a disk of stars self-organizing into arms

A still disk of 320 stars, each given the circular-orbit speed for its radius — inner stars faster, outer slower — plus an undrawn central mass MHALO=6200 and a seeded density ripple: differential rotation winds the ripple up until spiral arms sharpen by themselves. Where a spiral written as a formula paints a shape, this one is grown by integration, and the integrator is leapfrog so the disk does not inflate over many revolutions, painted at 31.310% cumulative area with a 24.639% peak at the loop-rewind moment and 48.0 intensity.

Spiral arms form — a disk of stars self-organizing into arms
Open in a new tab

02 · Spiral arms form — a disk of stars self-organizing into arms — Measured area 31.31% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DfillRectrequestAnimationFrame

Leapfrog — symmetric half-steps that conserve energy

  function stepLeap() {
    // leapfrog (velocity Verlet) — reuse a(t), push in symmetric half-steps, conserve energy
    if (!aReady) { accel(); aReady = true; }
    var h = DT * 0.5;
    for (var i = 0; i < N; i++) {
      VX[i] += AX[i] * h; VY[i] += AY[i] * h;   // v(t+h/2) = v(t) + a(t)·h/2
      X[i]  += VX[i] * DT; Y[i] += VY[i] * DT;  // x(t+h)   = x(t) + v(t+h/2)·h
    }
    accel();                                     // a(t+h)
    for (var i = 0; i < N; i++) {
      VX[i] += AX[i] * h; VY[i] += AY[i] * h;   // v(t+h) = v(t+h/2) + a(t+h)·h/2
    }
  }

The ripple that differential rotation winds into arms

  function seed() {
    // Still disk — per-radius circular-orbit speed (inner faster = differential rotation) plus a seeded ripple.
    // The ripple is a sine wave coupling angle to radius — differential rotation winds it into a spiral.
    X = []; Y = []; VX = []; VY = []; MASS = []; N = 0;
    var rnd = mulberry32(11);
    var cx = W * 0.5, cy = H * 0.5;
    var R = Math.min(W, H) * 0.44;
    for (var i = 0; i < NSTARS; i++) {
      var far = rnd() < 0.50;                             // outer star — wide orbit beyond the disk
      var r = far
        ? R * (0.42 + 2.00 * rnd())
        : 0.12 * R + R * 0.82 * Math.sqrt(rnd());          // dense core + thin outside
      var a = rnd() * Math.PI * 2 + 0.14 * Math.sin(r * 0.16);  // radius-coupled sine ripple
      var v = Math.sqrt(G * MHALO / Math.max(r, 8)) * (far ? 0.82 : 1.0);  // circular speed — larger when r is small
      X.push(cx + Math.cos(a) * r);
      Y.push(cy + Math.sin(a) * r * 0.90);                // slightly squashed disk (visual depth)
      VX.push(-Math.sin(a) * v + (rnd() - 0.5) * 1.5);
      VY.push( Math.cos(a) * v * 0.90 + (rnd() - 0.5) * 1.5);
      MASS.push(0.5 + rnd() * 0.8);
      N++;
    }
  }

03Globular cluster — hundreds of stars orbiting a common center of mass

280 stars scattered at uniform density through a 3D sphere, integrated under full O(N²) 3D gravity — the line-of-sight z enters the computation directly, and the screen is merely a perspective projection at FOCAL=420. With every star at its own angle, differential rotation cannot form arms: each star orbits the live-recomputed center of mass on its own orbital plane, and the whole cluster slowly tumbles, at 22.827% cumulative area, a gentle 6.406% peak, 49.4 intensity.

Globular cluster — hundreds of stars orbiting a common center of mass
Open in a new tab

03 · Globular cluster — hundreds of stars orbiting a common center of mass — Measured area 22.827% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DarcrequestAnimationFrame

The same pair sum, with dz added

  function step() {
    // Full pair O(N²) 3D gravity — a_i = Σ G·(x_j - x_i)/(|d|²+ε²)^1.5 (no approximation)
    var i, j, dx, dy, dz, d2, inv, f;
    for (i = 0; i < N; i++) { AX[i] = 0; AY[i] = 0; AZ[i] = 0; }
    for (i = 0; i < N; i++) {
      for (j = i + 1; j < N; j++) {
        dx = X[j] - X[i]; dy = Y[j] - Y[i]; dz = Z[j] - Z[i];
        d2 = dx * dx + dy * dy + dz * dz + SOFT2;
        inv = 1 / Math.sqrt(d2);
        f = G * inv * inv * inv;
        AX[i] += dx * f; AY[i] += dy * f; AZ[i] += dz * f;
        AX[j] -= dx * f; AY[j] -= dy * f; AZ[j] -= dz * f;
      }
    }
    for (i = 0; i < N; i++) {
      VX[i] += AX[i] * DT; VY[i] += AY[i] * DT; VZ[i] += AZ[i] * DT;
      X[i] += VX[i] * DT;  Y[i] += VY[i] * DT;  Z[i] += VZ[i] * DT;
    }
  }

04Dark-matter halo — orbits change the moment invisible mass is added

The one approximate item of the nine: star masses are not summed pair by pair here — every step, each star's mass is accumulated into one of K=28 radial bins to build the enclosed spherical mass M(r), and each star is pulled toward the center by the mass inside its radius, an isotropic approximation that discards directional information. The halo toggle overlays an invisible mass profile ρ ∝ 1/(r²+rc²) on the same grid, inflating M(r) so outer stars keep their circular speed — the flat rotation curve, graphed live top-right — and the freed frame budget is what pays for that graph, at 37.479% cumulative area, the largest of the nine, 12.855% peak, 50.5 intensity.

Dark-matter halo — orbits change the moment invisible mass is added
Open in a new tab

04 · Dark-matter halo — orbits change the moment invisible mass is added — Measured area 37.479% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

The invisible mass profile, integrated into bins once

  function buildHalo() {
    // Halo profile — a fixed distribution (cold dark matter that never collides with stars).
    // ρ ∝ 1/(r²+rc²) integrated into grid bins and stored (computed once)
    var rc = Math.min(W, H) * 0.15;
    var mstar = 0;
    for (var i = 0; i < N; i++) mstar += MASS[i];
    var raw = [];
    for (var k = 0; k < K; k++) {
      var r0 = rEdge(k), r1 = rEdge(k + 1);
      raw.push((Math.atan(r1 / rc) - Math.atan(r0 / rc)) * rc);  // ∫ r²/(r²+rc²) dr approximated
    }
    var sum = 0;
    for (k = 0; k < K; k++) sum += raw[k];
    for (k = 0; k < K; k++) haloMass[k] = raw[k] / sum * mstar * HALOBOOST;
  }

Each star pulled inward by the enclosed mass only

    // Each star's acceleration — pulled toward the center by the enclosed mass M(r)
    // (isotropic approximation: directional information is discarded)
    for (i = 0; i < N; i++) {
      var dx = cx - X[i], dy = cy - Y[i];
      var r = Math.hypot(dx, dy);
      var bk = binOf(r);
      var Mr = cumMass[bk] - binMass[bk] * 0.5;      // bin midpoint approximation
      var d3 = Math.pow(r * r + SOFT2, 1.5);
      var f = G * Mr / d3;
      AX[i] = dx * f; AY[i] = dy * f;
    }
  }

05Merger remnant — a spiral torn apart into a bland elliptical

The start is not the first passage but just before the second: the seed places the two disks close together and pre-integrates PREPASS=420 leapfrog steps, so the first shear and its tails are already in the past when on-screen time begins. As time flows, orbit directions mix — virialization — and the spiral structure scatters into a smooth featureless ellipsoid: the final state of two spiral galaxies, derived by integration rather than drawn, at 32.382% cumulative area with the smallest peak of the nine at 4.899%, which is expected when a scene starts late and evolves gently, 48.4 intensity.

Merger remnant — a spiral torn apart into a bland elliptical
Open in a new tab

05 · Merger remnant — a spiral torn apart into a bland elliptical — Measured area 32.382% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DellipserequestAnimationFrame

Starting the clock after the first passage

  function seed() {
    // Place the two disks just before the second passage (close together) and pre-integrate
    // PREPASS steps — on-screen t=0 already shows tails lined out from the first shear
    X = []; Y = []; VX = []; VY = []; N = 0; aReady = false;
    var rnd = mulberry32(9);
    var half = NSTARS >> 1;
    var R = Math.min(W, H) * 0.44;
    var sc = R;
    placeDisk(W * 0.36, H * 0.34, R, half, 0, 0.62, -0.42, 1.0, rnd, sc);
    placeDisk(W * 0.64, H * 0.66, R, NSTARS - half, Math.PI, -0.62, 0.42, 0.85, rnd, sc);
    for (var s = 0; s < PREPASS; s++) stepLeap();
  }

06Tidal tail close-up — the fling seen through one star's trail

The computation is the same full O(N²) pair sum as item 01 — the same kind of shear on a slightly different seed, integrated with leapfrog instead of Euler — no approximation, and only the rendering differs: the crowd is faint dots with short afterimages, while one tracked star, planted at a disk edge, accumulates a thick amber trail in a 180-coordinate ring buffer and draws it segment by segment, oldest faintest. The individual orbit shuttles between the two cores, Lissajous-like, then is flung out by the tidal kick — the tail explained as one star's story, not a statistic, at 20.762% cumulative area, 6.303% peak, 55.0 intensity.

Tidal tail close-up — the fling seen through one star's trail
Open in a new tab

06 · Tidal tail close-up — the fling seen through one star's trail — Measured area 20.762% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DlineTorequestAnimationFrame

One trail, fading from old to new

    // Tracked trail — thick amber line. Older segments drawn fainter (fade)
    var tn = trailX.length;
    if (tn > 1) {
      for (var s = 1; s < tn; s++) {
        var age = s / tn;                    // 0 old → 1 newest
        ctx.strokeStyle = rgba(C_HOT, 0.10 + age * 0.75);
        ctx.lineWidth = 0.8 + age * 1.9;
        ctx.beginPath();
        ctx.moveTo(trailX[s - 1], trailY[s - 1]);
        ctx.lineTo(trailX[s], trailY[s]);
        ctx.stroke();
      }
    }

07Launch a comet — drag angle and power to throw a star in yourself

The stage is a disk galaxy — an undrawn central mass MHALO=5400 plus 180 stars — and your pointer is a slingshot: drag from any point, release, and the new star is appended to the same arrays with no special treatment, joining the same O(N²) gravity field as-is. By the angle and speed of your throw it settles into an elliptical orbit or escapes on a hyperbola, and the render auto-launches comets at fixed deterministic angles — up to five in flight at once — so the panel is alive unattended, at 26.731% cumulative area, 6.271% peak, 55.3 intensity.

Launch a comet — drag angle and power to throw a star in yourself
Open in a new tab

07 · Launch a comet — drag angle and power to throw a star in yourself — Measured area 26.731% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerdownpointermoverequestAnimationFrame

Pull back, release, and the star joins the arrays

  field.addEventListener('pointerup', function (e) {
    if (!aim) return;
    var p = ptr(e);
    var dx = aim.x0 - p.x, dy = aim.y0 - p.y;       // opposite of the pull = launch direction
    var len = Math.hypot(dx, dy);
    if (len > 6) {
      var sp = Math.min(260, len * 2.2) * LAUNCHV;
      addComet(aim.x0, aim.y0, dx / len * sp, dy / len * sp, simSteps);
    }
    aim = null; aimLbl.hidden = true;
  });

08Trail history — a star's past stacking as afterimages

Where items 01 through 07 erased a little each frame, this one removes decay entirely and only lays prior frames down with globalAlpha — three alpha layers per step, LAYERS=3 — so orbits keep stacking until the spiral arms reveal themselves as an accumulation of hundreds of orbits rather than a single snapshot, which is the scene's whole point. Computation is the same full pair sum plus central mass and leapfrog as item 02, and the result shines at 62.6 average intensity, the brightest of the nine, at 32.485% cumulative area and 12.053% peak.

Trail history — a star's past stacking as afterimages
Open in a new tab

08 · Trail history — a star's past stacking as afterimages — Measured area 32.485% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DglobalAlpharequestAnimationFrame

No erasing — just alpha layers, stacked

  function stamp() {
    // Accumulation — lay low-globalAlpha layers each step and orbits harden into lines
    if (prev) {
      for (var L = 0; L < LAYERS; L++) {   // LAYERS deep — a stacking-speed variant
        chainCtx.globalAlpha = 0.22 / LAYERS * (L + 1);
        for (var i = 0; i < N; i++) {
          var r = Math.hypot(X[i] - W * 0.5, Y[i] - H * 0.5);
          var col = r < Math.min(W, H) * 0.18 ? C_HOT : C_GLOW;
          chainCtx.strokeStyle = rgba(col, 0.9);
          chainCtx.lineWidth = r < Math.min(W, H) * 0.18 ? 2.6 : 2.1;
          chainCtx.beginPath();
          chainCtx.moveTo(prev[0][i], prev[1][i]);
          chainCtx.lineTo(X[i], Y[i]);
          chainCtx.stroke();
        }
      }
      chainCtx.globalAlpha = 1;
    }
    if (!prev) { prev = [X.slice(), Y.slice()]; }
    else { prev[0] = X.slice(); prev[1] = Y.slice(); }
  }

09Deep-field scene — a stage of galactic evolution over eons

The closing scene: three nebula sprites — periwinkle, rose, amber — are pre-baked on separate canvases with createRadialGradient and composited each frame with drawImage, so the 330-star computation keeps its frame budget; on top, three galaxies of 82 stars each plus 84 strays filling the field, one pastel hue per galaxy, are seeded on crossing diagonal orbits, and the evolution sequence of shear → tidal tail → merger plays out in order within one loop, full pair sum plus leapfrog, no approximation, at 30.189% cumulative area, 7.303% peak, 52.2 intensity.

Deep-field scene — a stage of galactic evolution over eons
Open in a new tab

09 · Deep-field scene — a stage of galactic evolution over eons — Measured area 30.189% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createRadialGradientCanvasRenderingContext2DrequestAnimationFrame

Nebula sprites baked once, not rebuilt per frame

  function bakeNebulas() {
    nebCvs = [];
    var rnd = mulberry32(77);
    var cols = [C_GLOW, C_TAIL, C_HOT];
    for (var k = 0; k < NEBULA; k++) {
      var s = document.createElement('canvas');
      var sz = Math.max(160, Math.round(Math.min(W, H) * (0.55 + rnd() * 0.5)));
      s.width = sz; s.height = sz;
      var sc = s.getContext('2d');
      var g = sc.createRadialGradient(sz * 0.5, sz * 0.5, 0, sz * 0.5, sz * 0.5, sz * 0.5);
      var c = cols[k % 3];
      g.addColorStop(0.00, rgba(c, 0.16));
      g.addColorStop(0.35, rgba(c, 0.07));
      g.addColorStop(0.70, rgba(c, 0.02));
      g.addColorStop(1.00, rgba(c, 0));
      sc.fillStyle = g;
      sc.fillRect(0, 0, sz, sz);
      nebCvs.push({ cv: s, x: 0.12 + rnd() * 0.76, y: 0.10 + rnd() * 0.80, sz: sz });
    }
  }

Where these nine actually land on a real page

A space or science-themed landing page reaches for item 09 first — the deep-field scene already compresses eons of evolution into one composition, so it drops in as a hero background with nothing more than a headline on top. A lab or tutorial page built for visitors who like to poke at things does better with item 07: dragging an angle and a power to throw a star in turns the simulation into a hands-on demo instead of a passive backdrop.

For a physics write-up that wants to show orbits accumulating rather than just moving, item 08's trail history does the explaining for you — the afterimages alone show why an arm is a traffic pattern, not a fixed structure. Item 01's head-on collision is dramatic enough on its own to open a blog post or a slide deck without any surrounding text.

Skip the softening term and stars fling off in a single frame, breaking the scene

Pure 1/r² with no softening flings stars off-screen in one frame

Set ε² to zero and the first near-encounter produces an acceleration that scales as the inverse square of a sub-pixel distance — the two stars involved leave the canvas in a single step, and after a few frames the scene is empty except for a shrinking knot. The softening constants here range from 16 to 90 depending on star size and expected closeness; they are not decoration, they are what makes an n body simulation online-runnable at all. Item 04 pushes softening furthest at ε²=90 precisely because its radial-bin gravity concentrates mass at the center.

Forward Euler spirals the disk apart

Item 01 ships semi-implicit Euler and stays stable inside its 2.0s loop — but give it a longer loop and the disk slowly inflates, because the integrator adds a little energy every step. The three-variants table at the bottom makes this a feature: switch item 02's METHOD from leapfrog to euler and within a few revolutions the disk visibly puffs, the difference between conserving and not conserving energy landing right in front of you.

Math.pow per pair is visibly slow at 58k pairs per frame

The inner loop computes d2 = dx·dx + dy·dy + SOFT2, takes inv = 1/sqrt(d2) once, then multiplies inv by itself twice for f = G·inv³ — three multiplies. Writing Math.pow(d2, 1.5) or computing Math.hypot per pair instead is measurably slower at 60fps with 340 stars, and the stutter appears exactly when two galaxies overlap and every pair is live. The same discipline is why the comets of item 07 are appended to the star arrays instead of being tracked separately — one loop, one shape of arithmetic.

clearRect every frame deletes the tidal tails

The afterimage canvas is the memory of the orbits — erase the main canvas each frame and redraw from stored points, and you get a polyline, not ink. Items 01 through 07 erase a little each frame with destination-out so tails fade as they age; item 08 erases nothing and keeps stacking with globalAlpha, which is why it posts the highest average intensity of the nine at 62.6. For a galaxy simulation python or canvas implementation alike, the lesson is the same: structures like tidal tails and spiral arms are accumulations of orbits, and a per-frame clear hides exactly the structure the computation exists to produce.

Three collision variants, tuned for reduced-motion

One constant, three different collisions

Name Change Feel
Star count 01 NSTARS 340 → 120 Individual orbits emerge where statistical structure used to be
Closest approach 01 SKIP 0.60 → 0.25 (head-on) The cores punch through and stars scatter explosively, no twin tails
Integrator 02 METHOD leapfrog → euler Within a few revolutions the disk visibly inflates before your eyes

Reduced motion still shows the tidal tails

Every item checks matchMedia('(prefers-reduced-motion: reduce)') before starting the requestAnimationFrame loop, and for those visitors the loop never runs — instead each scene is pre-integrated to mid-evolution and one frozen frame is stamped with an .is-still class. The tails, the wound arms, and the first launched comets are all there, complete and legible, just not moving.

One drag to make, eight panels that run themselves

Only item 07 wants a pointer and only item 04 wants a click — every other panel runs unattended on requestAnimationFrame, and each canvas host carries an aria-label and a tabindex so the whole page is reachable from the keyboard. Nothing here was retyped or shortened: hpcmbqut unlocks the same nine source files the snippets came from, HTML, CSS, and JS together, inside this page's zip.

Why do stars self-organize into spirals? — gravity integration vs. a true spiral

A spiral formula draws a shape; integration grows one

Nothing in these scenes knows what a spiral arm is. The disk of item 02 is seeded with positions and velocities only — a radius-coupled sine ripple in the angles, circular speeds that fall off with radius. Inner stars complete a revolution in less time than outer stars, so any overdense ridge in the seed is sheared: the ridge's inner end runs ahead, its outer end lags, and after a few revolutions the ridge has been wound into a trailing spiral. The arm is a traffic pattern, not an object — stars pass through it continuously, the way a wave in a stadium is made of different people at every moment. A formula-drawn spiral, by contrast, has no stars at all; delete the formula and the picture is gone, while here deleting the seed ripple only makes the arms start later, because gravity and differential rotation keep doing the winding.

The pair sum, the softening, and the integrator are the whole physics

The acceleration a_i = Σ G·(x_j − x_i)/(|d|²+ε²)^1.5 is Newtonian gravity with one numerical stabilizer: the softening ε², which keeps two close-approaching stars from diverging to infinite acceleration in a single frame. The pair loop exploits Newton's third law — compute each pair once, apply it to both stars with opposite signs — which halves the work and makes the total momentum exactly conserved by construction. The integrator choice is what keeps the picture honest over time: forward Euler pumps energy in and orbits spiral outward, semi-implicit Euler (velocity first, then position) is the minimum viable fix used by items 01 and 03, and leapfrog — used by 02 and by 04 through 09 — pushes velocity in symmetric half-steps so the disk stays un-inflated over many revolutions. The same energy-conservation arithmetic is what lets a lightning bolt fork and die by one field's collapse hold its shape — except there the field is rebuilt each frame from scratch, and here everything persists: position, velocity, and the accumulated afterimage canvas that remembers where each star has been. A galaxy simulation 3d stage is only honest if the third axis is real, which is why item 03 integrates z directly and projects afterward rather than faking depth with scale alone.

Questions before you wire this n-body sim into a page

Is an n body simulation like this one actually full gravity, or is it faked

Eight of the nine panels compute the genuine full pair sum — every star pulls every other star, every frame, with no shortcuts. Only item 04, the dark-matter halo, approximates: it bins star mass into 28 radial cells and pulls each star inward by the enclosed mass M(r) instead of summing pairs, because that scene needs its frame budget for the live rotation-curve graph. The distinction is stated in the item's own comments and never quietly mixed into the other scenes — the price of the approximation is directional information, and pretending it is free would be a lie the render exposes anyway.

Can I run a galaxy simulation online in my own page without a library

Yes — that is the entire premise of the zip. Every scene is one HTML file plus one style.css of tokens, vanilla JavaScript on a canvas 2D context: no WebGL, no CDN, no physics library, no Barnes-Hut tree code. At the star counts used here (up to 340, about 58 thousand pairs per frame) the plain double loop holds 60fps on an ordinary laptop, and seeding with a deterministic PRNG (mulberry32) means the same galaxies render on the same orbits every visit, which is what makes any screenshot of these panels reproducible. The n body simulation code in each section is the exact source the panels above run.

Why does the dark-matter item paint the largest area of the nine

Because its loop runs longest and its stars never leave. Item 04 cycles over 6.0 seconds — the longest loop of the set, and roughly twice most of the others — and its halo-on state holds outer stars at their circular speed instead of letting them drift, so ink keeps landing inside the frame until the cumulative painted area reaches 37.479%. The runner-up, item 08 at 32.485%, gets there the opposite way: a 5-second loop whose afterimages never erase, stacking orbit on orbit. The two numbers together show that painted area measures interaction between motion and memory — fast changes with fading ink, or slow changes with permanent ink, both fill a canvas.

Sources: MDN — CanvasRenderingContext2D · MDN — Window.requestAnimationFrame() · MDN — Math.hypot() · MDN — prefers-reduced-motion · Wikipedia — N-body problem · Wikipedia — Galaxy collision

Enter the archive password

The password is inside this article. You will find it as you read.