Gravity Simulation: How to Create Realistic Physics Art With Code
Drop an apple. It falls. Aim a rocket at the right angle and speed, and it falls forever — orbiting. Add a second body, and the dance becomes chaotic. Add a thousand, and you get galaxies. Gravity is the simplest force in physics — every mass attracts every other mass — and yet it produces the most visually spectacular structures in the universe. It also produces some of the most beautiful generative art you can make with code.
This guide covers eight working gravity simulations you can build in your browser. Every example uses vanilla JavaScript and HTML Canvas. No physics libraries, no frameworks — just Newton's law of universal gravitation, some vector math, and a requestAnimationFrame loop. You'll go from a single falling ball to a full n-body galaxy simulation, and everything in between.
The physics: F = G × m1 × m2 / r²
Every simulation in this guide is built on one equation. Newton's law of gravitation says that the force between two masses is proportional to the product of their masses and inversely proportional to the square of the distance between them. In code:
function gravitationalForce(body1, body2) {
const dx = body2.x - body1.x;
const dy = body2.y - body1.y;
const distSq = dx * dx + dy * dy;
const dist = Math.sqrt(distSq);
const force = (G * body1.mass * body2.mass) / Math.max(distSq, 100);
return {
fx: force * dx / dist,
fy: force * dy / dist
};
}
The Math.max(distSq, 100) is a softening parameter — it prevents the force from going to infinity when two bodies get very close. Real astrophysicists use the same trick. From this single function, all eight examples below are built.
1. Single body in a gravity field — the basics
The simplest possible gravity simulation: a ball falling and bouncing. This introduces the core simulation loop that every other example builds on — update velocity from force, update position from velocity, repeat.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 400;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const ball = { x: 300, y: 50, vx: 2, vy: 0, r: 12 };
const gravity = 0.3;
const bounce = 0.8;
const friction = 0.99;
function update() {
ball.vy += gravity;
ball.vx *= friction;
ball.x += ball.vx;
ball.y += ball.vy;
if (ball.y + ball.r > canvas.height) {
ball.y = canvas.height - ball.r;
ball.vy *= -bounce;
}
if (ball.x + ball.r > canvas.width || ball.x - ball.r < 0) {
ball.vx *= -1;
ball.x = Math.max(ball.r, Math.min(canvas.width - ball.r, ball.x));
}
}
function draw() {
ctx.fillStyle = 'rgba(10, 10, 20, 0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fillStyle = '#4af';
ctx.fill();
ctx.strokeStyle = '#8cf';
ctx.lineWidth = 2;
ctx.stroke();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
The key insight: velocity accumulates force each frame (vy += gravity), and position accumulates velocity (y += vy). This is Euler integration — the simplest numerical integration method. It's not perfectly accurate, but it's fast and good enough for visual art.
2. Orbital mechanics — circular and elliptical orbits
To make something orbit, you need to give it exactly the right sideways velocity. Too slow and it spirals inward. Too fast and it escapes. The sweet spot creates a circle; slightly off creates an ellipse. This is Kepler's discovery, coded in 40 lines.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const cx = 300, cy = 300;
const G = 800;
const sun = { x: cx, y: cy, mass: 5000, r: 20 };
const planets = [];
for (let i = 0; i < 5; i++) {
const dist = 80 + i * 45;
const speed = Math.sqrt(G * sun.mass / dist) * (0.85 + Math.random() * 0.3);
const angle = Math.random() * Math.PI * 2;
planets.push({
x: cx + Math.cos(angle) * dist,
y: cy + Math.sin(angle) * dist,
vx: -Math.sin(angle) * speed,
vy: Math.cos(angle) * speed,
r: 3 + Math.random() * 4,
hue: (i * 60 + 200) % 360,
trail: []
});
}
function update() {
for (const p of planets) {
const dx = sun.x - p.x, dy = sun.y - p.y;
const distSq = dx * dx + dy * dy;
const dist = Math.sqrt(distSq);
const force = G * sun.mass / Math.max(distSq, 400);
p.vx += force * dx / dist;
p.vy += force * dy / dist;
p.x += p.vx;
p.y += p.vy;
p.trail.push({ x: p.x, y: p.y });
if (p.trail.length > 150) p.trail.shift();
}
}
function draw() {
ctx.fillStyle = 'rgba(5, 5, 15, 0.15)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Sun glow
const grad = ctx.createRadialGradient(sun.x, sun.y, 0, sun.x, sun.y, sun.r * 3);
grad.addColorStop(0, '#ff8');
grad.addColorStop(0.3, '#fa4');
grad.addColorStop(1, 'transparent');
ctx.beginPath();
ctx.arc(sun.x, sun.y, sun.r * 3, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(sun.x, sun.y, sun.r, 0, Math.PI * 2);
ctx.fillStyle = '#ffe080';
ctx.fill();
for (const p of planets) {
if (p.trail.length > 1) {
ctx.beginPath();
ctx.moveTo(p.trail[0].x, p.trail[0].y);
for (let i = 1; i < p.trail.length; i++) {
ctx.lineTo(p.trail[i].x, p.trail[i].y);
}
ctx.strokeStyle = `hsla(${p.hue}, 80%, 60%, 0.4)`;
ctx.lineWidth = 1;
ctx.stroke();
}
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${p.hue}, 80%, 65%)`;
ctx.fill();
}
}
(function loop() { update(); draw(); requestAnimationFrame(loop); })();
The orbital velocity formula v = √(GM/r) gives you a perfectly circular orbit. Deviate from that and you get ellipses — exactly as Kepler described in 1609. The trailing paths reveal the orbital shapes in real time.
3. N-body simulation — mutual gravitational attraction
The n-body problem is one of the oldest unsolved problems in physics. Three or more bodies attracting each other have no general closed-form solution — the system is chaotic. This makes it perfect for generative art: set up initial conditions, hit play, and watch unpredictable beauty emerge.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const G = 0.5;
const N = 80;
const bodies = [];
for (let i = 0; i < N; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = 50 + Math.random() * 200;
const speed = Math.sqrt(G * N * 5 / dist) * 0.5;
bodies.push({
x: 300 + Math.cos(angle) * dist,
y: 300 + Math.sin(angle) * dist,
vx: -Math.sin(angle) * speed + (Math.random() - 0.5) * 0.5,
vy: Math.cos(angle) * speed + (Math.random() - 0.5) * 0.5,
mass: 1 + Math.random() * 3,
r: 2
});
}
function update() {
for (let i = 0; i < N; i++) {
let ax = 0, ay = 0;
for (let j = 0; j < N; j++) {
if (i === j) continue;
const dx = bodies[j].x - bodies[i].x;
const dy = bodies[j].y - bodies[i].y;
const distSq = dx * dx + dy * dy + 200; // softening
const dist = Math.sqrt(distSq);
const force = G * bodies[j].mass / distSq;
ax += force * dx / dist;
ay += force * dy / dist;
}
bodies[i].vx += ax;
bodies[i].vy += ay;
}
for (const b of bodies) {
b.x += b.vx;
b.y += b.vy;
}
}
function draw() {
ctx.fillStyle = 'rgba(5, 5, 15, 0.08)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const b of bodies) {
const speed = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
const hue = 200 + speed * 30;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r + b.mass * 0.5, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${hue}, 80%, 65%)`;
ctx.fill();
}
}
(function loop() { update(); draw(); requestAnimationFrame(loop); })();
The O(n²) force calculation is the bottleneck — every body checks every other body. For 80 bodies that's 6,400 calculations per frame, still smooth at 60fps. Real astrophysics simulations use Barnes-Hut trees (O(n log n)) or fast multipole methods to handle millions of bodies. For art, brute force is fine up to ~500 particles.
4. Galaxy formation — disk rotation with n-body gravity
Real galaxies form when a cloud of matter collapses under gravity while conserving angular momentum. We can simulate this: start with a rotating disk of particles and let gravity do the rest. Spiral arms emerge naturally from density waves and orbital resonances.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const G = 0.8, N = 400, cx = 300, cy = 300;
const centerMass = 8000;
const stars = [];
for (let i = 0; i < N; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = 20 + Math.pow(Math.random(), 0.5) * 250;
const orbitalV = Math.sqrt(G * centerMass / dist);
const jitter = (Math.random() - 0.5) * orbitalV * 0.15;
stars.push({
x: cx + Math.cos(angle) * dist,
y: cy + Math.sin(angle) * dist,
vx: -Math.sin(angle) * (orbitalV + jitter),
vy: Math.cos(angle) * (orbitalV + jitter),
mass: 0.5 + Math.random(),
brightness: 0.4 + Math.random() * 0.6,
hue: dist < 80 ? 40 + Math.random() * 20 : 200 + Math.random() * 60
});
}
function update() {
for (const s of stars) {
// Central mass gravity
let dx = cx - s.x, dy = cy - s.y;
let distSq = dx * dx + dy * dy + 500;
let dist = Math.sqrt(distSq);
let f = G * centerMass / distSq;
s.vx += f * dx / dist;
s.vy += f * dy / dist;
// Nearby star interactions (sample 20 random neighbors for speed)
for (let j = 0; j < 20; j++) {
const other = stars[(Math.random() * N) | 0];
if (other === s) continue;
dx = other.x - s.x;
dy = other.y - s.y;
distSq = dx * dx + dy * dy + 400;
dist = Math.sqrt(distSq);
f = G * other.mass / distSq * 0.3;
s.vx += f * dx / dist;
s.vy += f * dy / dist;
}
s.vx *= 0.9999;
s.vy *= 0.9999;
s.x += s.vx;
s.y += s.vy;
}
}
function draw() {
ctx.fillStyle = 'rgba(2, 2, 8, 0.12)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Central glow
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 40);
grad.addColorStop(0, 'rgba(255, 240, 200, 0.3)');
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.fillRect(cx - 40, cy - 40, 80, 80);
for (const s of stars) {
ctx.globalAlpha = s.brightness;
ctx.fillStyle = `hsl(${s.hue}, 70%, 75%)`;
ctx.fillRect(s.x - 0.8, s.y - 0.8, 1.6, 1.6);
}
ctx.globalAlpha = 1;
}
(function loop() { update(); draw(); requestAnimationFrame(loop); })();
The trick for performance is stochastic sampling: instead of checking all N×N pairs, each star checks 20 random neighbors per frame. This gives a good approximation of the gravitational field at O(20N) instead of O(N²). The result looks remarkably like real spiral galaxies — because the same physics is at work.
5. Interactive gravity well — mouse-controlled attractor
Making gravity interactive transforms a simulation into an instrument. The mouse becomes a gravitational body, pulling particles into orbit, flinging them into chaotic trajectories, and sculpting ephemeral structures in real time.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const particles = [];
const N = 300;
let mx = 300, my = 300, mouseDown = false;
canvas.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
mx = e.clientX - r.left; my = e.clientY - r.top;
});
canvas.addEventListener('mousedown', () => mouseDown = true);
canvas.addEventListener('mouseup', () => mouseDown = false);
for (let i = 0; i < N; i++) {
particles.push({
x: Math.random() * 600,
y: Math.random() * 600,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
hue: Math.random() * 360
});
}
function update() {
const strength = mouseDown ? 800 : 200;
for (const p of particles) {
const dx = mx - p.x, dy = my - p.y;
const distSq = dx * dx + dy * dy + 100;
const dist = Math.sqrt(distSq);
const force = strength / distSq;
p.vx += force * dx / dist;
p.vy += force * dy / dist;
p.vx *= 0.995;
p.vy *= 0.995;
p.x += p.vx;
p.y += p.vy;
// Wrap edges
if (p.x < 0) p.x += 600;
if (p.x > 600) p.x -= 600;
if (p.y < 0) p.y += 600;
if (p.y > 600) p.y -= 600;
p.hue = (p.hue + 0.2) % 360;
}
}
function draw() {
ctx.fillStyle = 'rgba(5, 5, 15, 0.06)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
const size = Math.min(3, 1 + speed * 0.3);
ctx.beginPath();
ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${p.hue}, 80%, ${50 + speed * 5}%)`;
ctx.fill();
}
// Attractor indicator
ctx.beginPath();
ctx.arc(mx, my, mouseDown ? 12 : 6, 0, Math.PI * 2);
ctx.strokeStyle = mouseDown ? 'rgba(255,200,100,0.6)' : 'rgba(255,255,255,0.2)';
ctx.lineWidth = 2;
ctx.stroke();
}
(function loop() { update(); draw(); requestAnimationFrame(loop); })();
Click and hold to increase the gravitational pull. The particles will whip into tight orbits, forming spirals and rings. Release, and they scatter. This is the same principle behind slingshot maneuvers in space exploration — gravity assist trajectories that NASA uses to send probes to the outer planets.
6. Verlet integration — stable and beautiful
Euler integration (velocity += force, position += velocity) is simple but accumulates energy errors over time — orbits slowly spiral outward. Verlet integration solves this by computing position directly from the previous two positions, automatically conserving energy better. It's the standard in molecular dynamics simulations.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const G = 600, cx = 300, cy = 300, sunMass = 4000;
const bodies = [];
for (let i = 0; i < 60; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = 60 + Math.random() * 220;
const v = Math.sqrt(G * sunMass / dist);
const x = cx + Math.cos(angle) * dist;
const y = cy + Math.sin(angle) * dist;
// Store previous position instead of velocity
bodies.push({
x, y,
px: x + Math.sin(angle) * v, // prev x (encodes velocity)
py: y - Math.cos(angle) * v,
mass: 0.5 + Math.random() * 2,
hue: 180 + Math.random() * 120,
trail: []
});
}
function update() {
for (const b of bodies) {
const dx = cx - b.x, dy = cy - b.y;
const distSq = dx * dx + dy * dy + 300;
const dist = Math.sqrt(distSq);
const ax = G * sunMass * dx / (distSq * dist);
const ay = G * sunMass * dy / (distSq * dist);
// Verlet: new_pos = 2*pos - prev_pos + accel
const nx = 2 * b.x - b.px + ax;
const ny = 2 * b.y - b.py + ay;
b.px = b.x;
b.py = b.y;
b.x = nx;
b.y = ny;
b.trail.push({ x: b.x, y: b.y });
if (b.trail.length > 100) b.trail.shift();
}
}
function draw() {
ctx.fillStyle = 'rgba(5, 5, 15, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(cx, cy, 14, 0, Math.PI * 2);
ctx.fillStyle = '#ffe080';
ctx.fill();
for (const b of bodies) {
for (let i = 1; i < b.trail.length; i++) {
ctx.globalAlpha = i / b.trail.length * 0.3;
ctx.beginPath();
ctx.moveTo(b.trail[i - 1].x, b.trail[i - 1].y);
ctx.lineTo(b.trail[i].x, b.trail[i].y);
ctx.strokeStyle = `hsl(${b.hue}, 70%, 60%)`;
ctx.lineWidth = 1;
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(b.x, b.y, 2, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${b.hue}, 80%, 70%)`;
ctx.fill();
}
}
(function loop() { update(); draw(); requestAnimationFrame(loop); })();
Notice the difference: Verlet bodies hold x, y, px, py (current and previous position) instead of x, y, vx, vy. The velocity is implicit in the difference between current and previous position. This makes the simulation time-reversible and energy-conserving — orbits stay stable for thousands of frames without drifting.
7. Gravitational lensing — bending light with mass
Einstein showed that massive objects bend spacetime, curving the path of light. We can simulate this visually: trace rays across the screen and deflect them near massive objects. The result is the iconic Einstein ring and arc patterns seen in Hubble telescope images.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const lens = { x: 300, y: 300, mass: 80 };
const bgStars = [];
for (let i = 0; i < 200; i++) {
bgStars.push({
x: Math.random() * 600,
y: Math.random() * 600,
r: 0.5 + Math.random() * 1.5,
brightness: 0.3 + Math.random() * 0.7
});
}
canvas.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
lens.x = e.clientX - r.left;
lens.y = e.clientY - r.top;
});
function lensedPosition(sx, sy) {
const dx = sx - lens.x, dy = sy - lens.y;
const distSq = dx * dx + dy * dy;
const dist = Math.sqrt(distSq);
if (dist < 1) return { x: sx, y: sy };
const einsteinR = lens.mass;
const deflection = einsteinR * einsteinR / dist;
return {
x: sx + deflection * dx / dist,
y: sy + deflection * dy / dist
};
}
function draw() {
ctx.fillStyle = '#050510';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw lensed stars
for (const s of bgStars) {
// Create multiple images (gravitational lensing produces multiple images)
for (let angle = 0; angle < Math.PI * 2; angle += 0.3) {
const testX = s.x + Math.cos(angle) * 2;
const testY = s.y + Math.sin(angle) * 2;
const lensed = lensedPosition(testX, testY);
const ddx = lensed.x - lens.x, ddy = lensed.y - lens.y;
const lensedDist = Math.sqrt(ddx * ddx + ddy * ddy);
const magnification = Math.max(0.3, Math.min(3, lens.mass / (lensedDist + 10)));
ctx.globalAlpha = s.brightness * Math.min(1, magnification);
ctx.fillStyle = lensedDist < lens.mass * 1.2 ? '#adf' : '#fff';
ctx.beginPath();
ctx.arc(lensed.x, lensed.y, s.r * magnification, 0, Math.PI * 2);
ctx.fill();
}
// Original position (dimmer)
ctx.globalAlpha = s.brightness * 0.3;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Einstein ring
ctx.beginPath();
ctx.arc(lens.x, lens.y, lens.mass, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(100, 180, 255, 0.15)';
ctx.lineWidth = 2;
ctx.stroke();
// Lens center
ctx.beginPath();
ctx.arc(lens.x, lens.y, 4, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 200, 100, 0.6)';
ctx.fill();
}
(function loop() { draw(); requestAnimationFrame(loop); })();
Move your mouse to reposition the gravitational lens. Stars near the lens get distorted, magnified, and duplicated — just like in real gravitational lensing observations. The circular ring that forms when a star is directly behind the lens is called an Einstein ring, first predicted in 1936 and first observed in 1998.
8. Particle gravity field — flow visualization
Instead of simulating individual bodies, we can visualize the gravitational field itself. Drop thousands of massless tracer particles into a field defined by multiple attractors and repulsors. The particles trace the field lines, creating fluid-like flows and revealing the invisible structure of gravity.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const attractors = [
{ x: 200, y: 200, mass: 500, hue: 220 },
{ x: 400, y: 200, mass: 400, hue: 320 },
{ x: 300, y: 420, mass: 600, hue: 120 },
];
const N = 1500;
const particles = [];
function resetParticle(p) {
p = p || {};
p.x = Math.random() * 600;
p.y = Math.random() * 600;
p.vx = 0;
p.vy = 0;
p.life = 200 + Math.random() * 300;
p.age = 0;
return p;
}
for (let i = 0; i < N; i++) particles.push(resetParticle());
let t = 0;
function update() {
t += 0.01;
// Slowly orbit attractors
for (let i = 0; i < attractors.length; i++) {
const a = attractors[i];
const angle = t + i * Math.PI * 2 / 3;
a.x = 300 + Math.cos(angle) * (100 + i * 30);
a.y = 300 + Math.sin(angle) * (100 + i * 30);
}
for (const p of particles) {
let ax = 0, ay = 0;
for (const a of attractors) {
const dx = a.x - p.x, dy = a.y - p.y;
const distSq = dx * dx + dy * dy + 200;
const dist = Math.sqrt(distSq);
const f = a.mass / distSq;
ax += f * dx / dist;
ay += f * dy / dist;
}
p.vx = (p.vx + ax) * 0.96;
p.vy = (p.vy + ay) * 0.96;
p.x += p.vx;
p.y += p.vy;
p.age++;
if (p.age > p.life || p.x < -20 || p.x > 620 || p.y < -20 || p.y > 620) {
resetParticle(p);
}
}
}
function draw() {
ctx.fillStyle = 'rgba(5, 5, 15, 0.04)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
let closestA = attractors[0], closestD = Infinity;
for (const a of attractors) {
const d = (a.x - p.x) ** 2 + (a.y - p.y) ** 2;
if (d < closestD) { closestD = d; closestA = a; }
}
const alpha = Math.min(1, (1 - p.age / p.life) * 0.8);
ctx.globalAlpha = alpha;
ctx.fillStyle = `hsl(${closestA.hue}, 70%, 60%)`;
ctx.fillRect(p.x, p.y, 1.2, 1.2);
}
ctx.globalAlpha = 1;
for (const a of attractors) {
const grad = ctx.createRadialGradient(a.x, a.y, 0, a.x, a.y, 20);
grad.addColorStop(0, `hsla(${a.hue}, 80%, 70%, 0.6)`);
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(a.x, a.y, 20, 0, Math.PI * 2);
ctx.fill();
}
}
(function loop() { update(); draw(); requestAnimationFrame(loop); })();
Three attractors orbit each other while 1,500 tracer particles flow through the combined gravitational field. Each particle is colored by its nearest attractor, making the gravitational "territory" of each body visible. The boundaries where colors meet are gravitational Lagrange points — saddle points where forces balance.
Performance tips for gravity simulations
Gravity simulations are computationally expensive because every body potentially interacts with every other body. Here are techniques to keep frame rates smooth:
- Softening parameter: Add a small constant to distance² to prevent division by near-zero and infinite forces at close range.
- Stochastic sampling: Instead of checking all N² pairs, sample random subsets. 20 random neighbors per frame gives a surprisingly good approximation.
- Spatial hashing: Divide space into grid cells. Only check interactions within nearby cells. Reduces complexity to roughly O(N) for short-range forces.
- Barnes-Hut algorithm: Build a quadtree and approximate distant groups of bodies as a single body. Reduces to O(N log N). Essential for >1000 bodies.
- Verlet integration: More stable than Euler integration, conserves energy better, and only stores positions (no separate velocity array).
- Fixed timestep: Use a fixed dt instead of frame-dependent timing. This prevents instability when frame rate drops.
- Canvas tricks: Use fillRect instead of arc for point particles (2-4x faster). Use semi-transparent background fills instead of clearRect for cheap motion trails.
From Newton to generative art
Gravity is unique among forces for generative art because it's universally attractive and infinite in range. Every particle affects every other particle. This creates emergent structures at every scale — clusters, filaments, voids, spirals — the same patterns we see in the real universe, from atoms to galaxy superclusters.
On Lumitree, gravity is one of the fundamental forces that drives micro-world generation. When a visitor plants a seed, some branches grow into orbital systems, particle galaxies, or gravitational flow fields — all rendered within the 50KB constraint using the same vanilla JavaScript techniques shown here. Each branch is a unique gravitational universe.
The eight simulations in this guide progress from trivial (a bouncing ball) to complex (gravitational field visualization), but they all share the same core: F = GMm/r². Start with example 1 to understand the simulation loop. Try example 5 to feel gravity with your mouse. Then modify example 4 to create your own galaxy. That's the beauty of gravity simulations — simple physics, infinite art.