Show code
<think>
Okay, I need to create a self-contained HTML file that animates a gravitational n-body system. The user specified one central star and six orbiting bodies. The key requirements are using real Newtonian forces between every pair each frame, with a small time step to keep integration stable. The animation should use requestAnimationFrame, have fading trails, and display the number of bodies remaining. Also, no external libraries, and the final code must be under 7000 characters.
First, I'll outline the structure. The HTML needs a canvas and some JavaScript inside a script tag. The canvas will handle rendering, and the JavaScript will manage the physics and animation.
Starting with the setup: the canvas element. I'll set its size, maybe 600x600 pixels. Then the JavaScript part.
For the bodies, I need an array. The central star will be the first element with a high mass. The other six have varying masses. Each body should have position, velocity, mass, and a color based on mass. Maybe more massive bodies are redder, smaller ones bluer? Or some other color variation. Size could be proportional to mass.
The gravitational force between bodies: For each pair, compute the vector between them, distance squared, and apply the force. The acceleration is F/m. I'll loop through all pairs, calculate the force, add to each body's acceleration.
Integration: Use a small time step. The user mentioned substeps might be needed. But for simplicity, maybe just take a small dt like 0.01 or 0.005. Euler method? But that might not be stable. Maybe the Verlet method? Hmm, Euler can work if the time step is small enough. Alternatively, Runge-Kutta? But that might complicate. Let's start with Euler.
Wait, the problem states "small enough time step or substeps". So perhaps integrate in substeps per frame? Like, each frame's physics is done in multiple small steps. But that might be computationally intensive. Alternatively, use a small base dt. Let me think.
The animation loop will use requestAnimationFrame. Each frame, calculate forces, update velocities and positions. The trails can be done by storing the previous positions and drawing lines between them, with a decay factor so older points fade out.
For the trails: each body has an array of previous positions/points. Each frame, add the current position, then when drawing, loop through the points and draw lines with decreasing opacity. Need to manage the array length so it doesn't grow indefinitely. Maybe cap it at 50 points or something, and shift out the oldest as needed.
The counting of remaining bodies: need to check if each body is still within the canvas or collided with the star. Collisions: if the distance between two bodies is less than the sum of their radii (since masses can vary, size is proportional to mass). The star is at the center. The orbiting bodies might be ejected if they go beyond the canvas bounds.
Starting positions: the star is at (300, 300). The other bodies need to be placed with initial velocities that put them in orbit. Since it's n-body, they have to consider the star's gravity and each other's. The initial velocities should be tangential to approximate circular orbits. But since it's n-body, their orbits will perturb each other.
Masses: Star has a large mass, say 1e5 or something. The orbiting bodies have varying masses, maybe between 1 and 100. That way, their gravitational effects on each other are noticeable but not too disruptive to the system.
Color and size: size could be Math.sqrt(mass) multiplied by a factor. Color could be based on mass, like red for higher masses and blue for lower, using HSL where hue depends on mass.
Now, putting this into code.
First, variables: canvas, ctx. The bodies array. Trails array for each body.
Constants: G (gravitational constant), which in simulation units should be adjusted for the scale. Let's see. Let's set G to 1e-4, but need to test.
Time step: dt. Let's start with 0.01.
Function to compute forces:
function computeForces() {
for (let i = 0; i < bodies.length; i++) {
const a = bodies[i];
a.ax = 0;
a.ay = 0;
for (let j = 0; j < bodies.length; j++) {
if (i === j) continue;
const b = bodies[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const distSqr = dx*dx + dy*dy;
if (distSqr < 1e-6) continue; // prevent division by zero
const force = G * a.mass * b.mass / distSqr;
const invDist = 1 / Math.sqrt(distSqr);
const ax = dx * invDist;
const ay = dy * invDist;
a.ax += force * ax / a.mass;
a.ay += force * ay / a.mass;
}
}
}
Wait, no. The force on a from b is (dx, dy) direction. The magnitude is G * m_a * m_b / dist^2. The acceleration of a is force / mass of a. So the components are:
a.ax += ( (G * b.mass / dist^3) * dx );
Similarly for ay.
Wait, let me re-derive:
F = G * (m_a * m_b) / r^2 * direction (dx, dy).
Acceleration of a due to b: F/m_a = (G m_b / r^3) * (dx, dy)
So:
a.ax += ( (G * b.mass * dx ) / (distSqr * Math.sqrt(distSqr)) )) ?
Wait, r^3 is (distSqr)^(3/2). So yes, dx * dx + dy * dy is distSqr. So invDistCube = 1/(distSqr * Math.sqrt(distSqr))? Or better, invDistCube = 1/(dist * dist * dist) where dist is sqrt(dx^2 + dy^2). So perhaps better to compute dist = Math.sqrt(dx*dx + dy*dy), then invDistCube = G * b.mass / (dist * dist * dist).
Wait let me re-calculate:
The acceleration vector for a from b is ( (G * b.mass / r^3) * dx, (G * b.mass / r^3 )* dy )
where r is the distance between a and b.
So:
dist = Math.sqrt(dx*dx + dy*dy)
if dist is too small (to prevent singularity), skip.
Then a.ax += G * b.mass * dx / (dist**3)
Similarly for ay.
This is more accurate.
So in code:
let dist = Math.hypot(dx, dy);
if (dist < 1e-3) continue; // avoid singularity
const forceFactor = G * b.mass / (dist * dist * dist);
a.ax += dx * forceFactor;
a.ay += dy * forceFactor;
Wait, no, because G is already part of the constant. Let me see:
Wait the standard formula is:
F = G * m1 * m2 / r^2 * direction (dx, dy)
So acceleration of a due to b is F/m1 = (G * m2 / r^3) * (dx, dy)
Hence:
a.ax += (G * b.mass / (dist**3)) * dx;
a.ay += (G * b.mass / (dist**3)) * dy;
Yes, that's right.
So in code:
dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 1e-3) continue;
const invDist3 = G / (dist ** 3);
a.ax += invDist3 * dx * b.mass;
a.ay += invDist3 * dy * b.mass;
Wait, that way, the acceleration is (G * b.mass / (dist^3)) * dx and dy.
Yes.
So this is how the force is calculated.
Now, for the integration step.
After computing forces, we need to update velocity and position.
Using Euler method:
for each body:
a.vx += a.ax * dt;
a.vy += a.ay * dt;
a.x += a.vx * dt;
a.y += a.vy * dt;
But Euler is first-order, which can be unstable for large dt. So perhaps using a smaller dt? Let's see.
Alternatively, use a higher-order method. But for simplicity, let's proceed with Euler and choose a small enough dt. Let's try dt = 0.01. Maybe adjust later.
Now, the initial conditions.
The central star should be at center (300, 300), with a large mass. Let's say 1e5. The orbiting bodies need to start at some positions with appropriate velocities to orbit the star.
Let me think: for a circular orbit, v = sqrt(G * M / r). But in our units, since G is 1e-4, we need to set parameters so that this holds.
Alternatively, set the star's mass as 1e5, G as 1e-4. Suppose a body at 100 pixels from the center (distance) has velocity v such that v^2 = (G * M)/r.
Let me see:
G = 1e-4, M = 1e5, r = 100.
v^2 = (1e-4 * 1e5)/100 = (10)/100 = 0.1 => v = ~0.316.
So initial velocity for a circular orbit would be around 0.3.
But since our time step is 0.01, over each frame, the position would be updated by 0.01 * velocity.
Hmm.
Alternatively, maybe set G to 1 for simplicity? Let me recalculate.
Wait, the choice of constants can be adjusted. Maybe better to set G=1, and scale masses and initial positions accordingly.
Alternatively, to make the trails visible over time, perhaps set dt to be small, say 0.005, and then have the animation loop call computeForces multiple times each frame? That might be needed for stability.
Alternatively, use a Runge-Kutta 4 method for better accuracy. But that's more complex.
But since the user allows substeps, maybe do multiple steps per frame. Let's think: each requestAnimationFrame (which is about 60 times per second, so roughly 0.016 seconds between frames). Suppose we do 50 substeps each frame with a very small dt, say dt = 0.001. So total time per frame: 0.05 seconds, but this might be excessive and slow down the animation.
Alternatively, perhaps using a fixed dt, and just making sure it's small enough.
Alternatively, given time constraints, proceed with Euler and adjust parameters to keep it stable enough.
Now, the initial positions and velocities.
Star is at (300,300). The other bodies should be placed in a circle around it with some random angles and distances. Let's say initial positions are between 100 and 200 pixels from the center. Random angles between 0 and 2pi.
Velocities: initial velocity vectors need to be tangential to the orbit. So direction is perpendicular to the position vector.
So for each body:
distance from center is r = random between 100-200.
angle theta = random(0, 2pi).
x = 300 + r * cos(theta);
y = 300 + r * sin(theta);
Then velocity needs to be perpendicular (for circular orbit) with magnitude v = sqrt(G*M/r) where M is the star's mass.
But in our case, the star's mass is part of the bodies, so the total mass at the center is bodies[0].mass.
Wait, but the initial velocity should be based on the star's mass only, since the other bodies are initially far apart. But since they are orbiting, maybe starting with their velocities as per the star's gravity.
Alternatively, given that the system is n-body, their initial velocities can be based on the star's contribution, and then they can perturb each other.
So for each orbiting body's initial velocity:
vx = -v * sin(theta); // to get tangential direction
vy = v * cos(theta);
where v = sqrt( (G * M_star) / r );
Since G is 1e-4, M_star is 1e5, so:
v = sqrt( (1e-4 * 1e5 ) / r ) = sqrt(10 / r )
So for r=100, v= sqrt(0.1) ~0.316, for r=200, sqrt(0.05) ~0.224.
Hence, initial velocities are in that range.
So, code-wise:
const G = 1e-4;
const starMass = 1e5;
bodies array will have the star first, then six others.
Initializing the bodies:
function init() {
bodies = [];
// Star
bodies.push({
x: 300, y: 300,
vx:0, vy:0,
mass: starMass,
radius: 10,
color: 'yellow'
});
for (let i=0; i<6; i++) {
const theta = Math.random() * Math.PI*2;
let r = 100 + Math.random()*100;
const M = Math.random() * 90 + 10; // mass between 10 and 100
const v = Math.sqrt( (G * starMass) / r );
const vx = -v * Math.sin(theta);
const vy = v * Math.cos(theta);
bodies.push( {
x: 300 + r * Math.cos(theta),
y: 300 + r * Math.sin(theta),
vx, vy,
mass: M,
radius: Math.sqrt(M)*1.5, // or something to make it visible
color: 'hsl('+ (360 - 240*M/100) + ',50%,50%)'
});
}
}
Wait, color calculation: maybe using HSL where higher mass is more red (0 degrees), lower mass more blue (240 degrees). So if M ranges 10-100, then:
hue = 240 - (240 * (M -10)/90 )
Wait, let's see:
For M=10: 240 - (240*(0)) = 240 (blue)
For M=100: 240 - (240*(90/90)) = 0 (red)
Yes, so the formula would be:
hue = 240 - (240 * (M-10)/90 )
Which can be rewritten as:
hue = 240 * (100 - M)/90 ?
Wait, perhaps better to compute:
var hue = 240 * (1 - (M -10)/90);
Wait, let me see:
At M=10: (M-10)/90 =0 → 240*(1-0)=240.
At M=100: (90)/90 → 1 → 240*(0) =0.
Yes, so:
hue = 240 * (1 - (M -10)/90 )
Yes. So in code, (240 * (1 - (M -10)/90 )) → 240*( (100 - M)/90 )
= 240*( (100 - M)/90 ) → 240*(1 - (M-10)/90 )
So in code:
color: 'hsl('+ ( 240 * ( (100 - M)/90 )) + ',50%,50%)'
Alternatively, to simplify:
hue = 240 * (100 - M) / 90
Wait, 240*( (100 - M)/90 ) → which is the same as above.
So, in code:
const hue = 240 * (100 - M)/90;
But in JS:
const hue = 240 * (100 - M)/(90);
Yes.
So that's the color.
Radius: proportional to sqrt(mass) to make it visually reasonable. Let's say radius = Math.sqrt(M)*2 or so. For M=10 → ~4.47, M=100 → 20 (since sqrt(100)=10, times 2 is 20).
But the star is radius 10. So maybe the star should have a larger radius. Let's set the star's radius as 15.
Wait, in code, the star's radius is 10. Maybe make it 20?
Yes.
Also, the trails: Each body has a trail array, which holds past positions. Each frame adds the current position (x,y) to the trail array, and then when drawing, loops through the trail and draws lines from previous points to current, with decreasing opacity. The trail should gradually fade out, so each point's opacity can be adjusted based on its age.
Alternatively, for each point in the trail, the opacity decreases over time. The trails could be stored as an array for each body, with each element being a point (x,y, time). But to keep it simple, maybe the trail array stores points and each frame, when drawing, the opacity is based on the length of the trail. For example, the first point is 100% opacity, the second 90%, etc., but this requires passing an opacity value with each point. Alternatively, just limit the trail length and let older points fade as the array grows.
Alternatively, each point in the trail can be drawn with a line from the previous point to current, with decreasing alpha.
Alternatively, use a gradient or decay factor each step. This might be complicated.
Perhaps the simplest way is to have each body's trail array store the past positions, and when drawing the trail, for each point in the trail, draw a small circle or a line segment with decreasing opacity as it gets older.
But to manage this, perhaps each trail entry could store the x,y and a timestamp, and then calculate the opacity based on how old the point is. However, in JS, time is measured in ms, so each frame, we can track the current time and compute the age.
Alternatively, for simplicity, just maintain a trail array of, say, 50 points, and each point's opacity decreases as it gets older. For example, the oldest point is 25% opacity, the newest 100%.
Alternatively, since the trails are just for visualization, maybe when drawing the trail, for each body:
ctx.beginPath();
for (let j=0; j<trail.length-1; j++) {
const pt1 = trail[j];
const pt2 = trail[j+1];
ctx.moveTo(pt1.x, pt1.y);
ctx.lineTo(pt2.x, pt2.y);
}
ctx.strokeStyle = this.color;
ctx.globalAlpha = 0.5; // trail opacity
ctx.stroke();
ctx.globalAlpha = 1; // reset
But to have the trail fade over time, perhaps each point's contribution to the trail decays over steps.
Alternatively, just let the trails be a fixed length, say 50 points, and each frame adds the current position and shifts out old ones.
Wait, maybe each body has a trail array that gets pushed the current (x,y), and then when drawing, we iterate through the trail array and draw lines between consecutive points with decreasing opacity. For example, each segment's opacity is based on its age.
Alternatively, just draw lines with low alpha so that the trail gradually fades.
Alternatively, here's a possible approach:
Each body has a trail array, which holds objects {x,y}.
Each frame:
- After computing new positions, add current (x,y) to the trail array.
- If the trail array exceeds some maximum length (e.g., 100), remove the oldest elements.
- When drawing the trail, draw a line from each point in the trail array with a low opacity, so that older points are overwritten and gradually fade.
Alternatively, use a trail length of 100 points and draw lines between each point with a small stroke and alpha.
Perhaps:
When drawing each body's trail:
ctx.strokeStyle = body.color;
ctx.globalAlpha = 0.2;
ctx.lineWidth = 1;
ctx.beginPath();
for (let i=0; i<body.trail.length; i++) {
const pt = body.trail[i];
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
ctx.globalAlpha = 1;
But this would draw a continuous line from the first point to the last, which might not show the fading. Alternatively, use a loop through the trail array and draw segments with decreasing alpha.
Alternatively, use a for loop that draws lines from each point to the next with a color that gets more transparent.
Alternatively, this might be getting too complex. Let's proceed with a simple approach where the trails are stored as arrays and drawn with low alpha.
Now, putting this together.
The main loop: each frame, compute forces, update velocities and positions (but since it's Euler, we need to do every frame).
Wait, no: in the code, to compute the positions, we first compute the net force on each body, then update their velocities and positions.
Wait, in the Euler method:
For each body (except the star, perhaps?), compute acceleration (F/m):
But to compute forces between all bodies.
Wait, the force between two bodies is G * m1 m2 / r^2 directed along the line connecting them.
But since force on body A from B is F = G * m_A m_B / r^2 * direction vector, then acceleration of A is F/m_A = G*m_B / r^2 * direction.
Hence, for each body, their acceleration is sum over all other bodies of (G * other.mass / r^2 * direction).
Hence, in code:
function computeForces() {
for (let i=0; i < bodies.length; i++) {
const body = bodies[i];
let ax = 0, ay = 0;
for (let j=0; j < bodies.length; j++) {
if (i === j) continue;
const other = bodies[j];
const dx = other.x - body.x;
const dy = other.y - body.y;
const dist = Math.hypot(dx, dy);
if (dist < 1) continue; // prevent division by zero
const force = G * other.mass / (dist * dist);
ax += force * dx / dist; // acceleration component x
ay += force * dy / dist; // acceleration component y
}
body.vx += ax * dt;
body.vy += ay * dt;
// Wait no, Euler method: the acceleration is ax, so velocity += ax * dt
// Then position += velocity * dt
}
// Now update positions
for (let i=0; i < bodies.length; i++) {
const body = bodies[i];
body.x += body.vx * dt;
body.y += body.vy * dt;
}
}
Wait, but the order is important: first compute all the forces and compute new velocities, then compute new positions based on the new velocities? Or based on previous velocities?
Actually, Euler method is:
v_new = v_old + a * dt
x_new = x_old + v_old * dt
Wait, so first compute acceleration (a) for each body from the current positions.
Then compute the new velocity as v_old + a * dt.
Then compute the new position using the old velocity?
No, actually, the standard Euler method is:
v_{n+1} = v_n + a_n * dt
x_{n+1} = x_n + v_n * dt
Where a_n is acceleration computed from x_n.
So in code:
for each body:
compute a based on current positions.
Then update velocities with v += a * dt.
Then update positions with x += v_old * dt.
Wait, but the new velocity is not used for the position update. So the code should be:
body.vx = body.vx + ax * dt;
body.vy = body.vy + ay * dt;
then,
body.x += body.vx_prev * dt;
body.y += body.vy_prev * dt;
But in code, since we can't save the previous velocity, perhaps it's better to first compute all the acceleration and store them temporarily, then update velocities and positions.
Alternatively, separate loops for acceleration and velocity/position:
First loop through all bodies to compute acceleration (store in arrays or temporary variables).
Second loop to update velocities using the computed accelerations.
Third loop to update positions using the previous velocities (before updating).
Alternatively, perhaps code:
const dt = 0.01; // or whatever.
function updatePhysics() {
let ax, ay;
// First compute all accelerations and store in temporary arrays
const a = [];
for (let i=0; i < bodies.length; i++) {
const body = bodies[i];
ax = 0; ay = 0;
for (let j=0; j < bodies.length; j++) {
if (i === j) continue;
const other = bodies[j];
const dx = other.x - body.x;
const dy = other.y - body.y;
const dist = Math.hypot(dx, dy);
if (dist < 1e-3) continue; // prevent nan
const force = G * other.mass / (dist*dist);
ax += force * dx / dist;
ay += force * dy / dist;
}
a[i] = { ax, ay };
}
// Now update velocities and positions
for (let i=0; i < bodies.length; i++) {
const body = bodies[i];
const ai = a[i];
body.vx += ai.ax * dt;
body.vy += ai.ay * dt;
// Update positions using previous vx, vy ?
// Wait no, the old velocity is used for displacement.
body.x += body.vx * dt; // Wait, no! Because the acceleration is from the current positions.
// Wait, according to Euler:
// v_{n+1} = v_n + a_n * dt
// x_{n+1} = x_n + v_n * dt
// So the velocity is updated now, but the position uses the previous velocity.
// So need to store the previous velocity?
Hmm, this is a problem.
To correctly compute x_{n+1} = x_n + v_n * dt, but v_n is before the acceleration step.
So to compute the velocity first, then the displacement.
Wait, no: the order is that the acceleration is computed from the current positions, then the velocity is updated with the acceleration. Then the positions are updated with the previous velocity.
So to do this correctly, you need to first compute the acceleration based on current positions, then compute velocities based on acceleration, then compute positions based on previous velocities.
But since we are using the same variables (body.vx is being updated), the position update will use the new velocity unless we store the old velocity.
So an alternative approach is:
// First, compute the new velocity by adding acceleration * dt to current velocity.
// But to compute the position, we need the previous velocity before the velocity is updated.
// So perhaps we should compute the acceleration, store the new velocities, then update the velocities and compute the positions based on the previous velocities.
Alternatively, here's the code:
Save the current velocity before updating.
let oldVx = body.vx;
let oldVy = body.vy;
body.vx += ai.ax * dt;
body.vy += ai.ay * dt;
body.x += oldVx * dt;
body.y += oldVy * dt;
That way, the position is based on the previous velocity.
}
}
Hmm, that seems better.
So in code, in the second loop:
body.x += body.vx * dt; // no, because vx is updated in this step.
Wait, so to prevent using the new velocity for the position update, we need to store the old velocity first.
Thus:
In the second loop:
for each body:
const oldVx = body.vx;
const oldVy = body.vy;
body.vx += a[i].ax * dt;
body.vy += a[i].ay * dt;
body.x += oldVx * dt;
body.y += oldVy * dt;
Yes, that works.
So the code would be structured as follows.
Now, considering the star (the central body), should it be affected by the other bodies? In the case of a simulation of planets around the sun, the sun's mass is so large that its movement is negligible, so perhaps we can fix its position and velocity. Alternatively, if we want an accurate simulation, we should let all bodies interact. So in the code above, all bodies are treated equally, so the central body will also move, but if it's much more massive, its motion will be minimal.
So, in the initialization, set the central body as having a large mass, and the planets with small masses.
Now, putting all this together.
The complete code would involve:
- Canvas setup.
- Initialization of bodies (central star, planets).
- Animation loop: each frame, compute physics, update positions, draw.
- Trail handling for each body.
Now, the user's request is to "create" this, so they probably want the code.
Now, I need to write this step by step.
First, setting up the canvas and context.
Then, defining the bodies, each with x, y, vx, vy, mass, and trail array.
Then, the update function that computes forces and updates positions/velocities.
Then, the draw function that draws the bodies and trails.
Handling the trails: each body has a trail array, which is pushed new position each frame, and limited in length.
The main loop uses requestAnimationFrame.
Now, let's proceed to code.
First, the HTML:
<canvas id="simCanvas"></canvas>
Then, the JavaScript:
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');
// Canvas size
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Constants
const G = 1; // Gravitational constant. Adjust as needed.
const dt = 0.01; // Time step.
// Body class
class Body {
constructor(x, y, mass, color) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.mass = mass;
this.color = color;
this.trail = []; // Array to store trail points
}
updateTrail() {
// Add current position to trail
this.trail.push({x: this.x, y: this.y});
// Limit trail length to 200 points
if (this.trail.length > 200) {
this.trail.shift(); // Remove oldest point
}
}
}
// Initialize bodies
const bodies = [];
// Central star
bodies.push(new Body(canvas.width/2, canvas.height/2, 1000, 'yellow'));
// Add planets
// For example, add 3 planets with different orbits
const numPlanets = 3;
for (let i=0; i < numPlanets; i++) {
const angle = (Math.PI * 2) * i / numPlanets;
const radius = 150 + 50*i; // Increasing radius for each planet
const mass = 1; // Small mass compared to star
const x = canvas.width/2 + radius * Math.cos(angle);
const y = canvas.height/2 + radius * Math.sin(angle);
// Initial velocity to orbit (perpendicular to position vector)
const speed = Math.sqrt(G * bodies[0].mass / radius); // Circular orbit velocity
const vx = -speed * Math.sin(angle); // Tangential direction
const vy = speed * Math.cos(angle);
bodies.push(new Body(x, y, mass, `hsl(${360/numPlanets *i}, 100%, 50%)`));
}
// Update function
function update() {
// Compute acceleration for all bodies
const accelerations = [];
for (let i=0; i < bodies.length; i++) {
const body = bodies[i];
let ax = 0, ay = 0;
for (let j=0; j < bodies.length; j++) {
if (i === j) continue;
const other = bodies[j];
const dx = other.x - body.x;
const dy = other.y - body.y;
const dist = Math.hypot(dx, dy);
if (dist < 1e-3) continue; // Prevent division by zero
const force = (G * other.mass) / (dist * dist);
ax += (dx / dist) * force;
ay += (dy / dist) * force;
}
accelerations[i] = { ax, ay };
}
// Update velocities and positions
for (let i=0; i < bodies.length; i++) {
const body = bodies[i];
const acc = accelerations[i];
const oldVx = body.vx;
const oldVy = body.vy;
body.vx += acc.ax * dt;
body.vy += acc.ay * dt;
body.x += oldVx * dt;
body.y += oldVy * dt;
// Update trail
body.updateTrail();
}
}
// Draw function
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw each body and their trails
ctx.fillStyle = 'white';
ctx.strokeStyle = 'white';
bodies.forEach(body => {
// Draw trail
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = body.color;
ctx.globalAlpha = 0.5; // Trail transparency
for (let i=0; i < body.trail.length; i++) {
const pt = body.trail[i];
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
ctx.globalAlpha = 1;
// Draw the body as a circle
ctx.beginPath();
ctx.arc(body.x, body.y, Math.sqrt(body.mass), 0, Math.PI*2);
ctx.fillStyle = body.color;
ctx.fill();
});
}
// Animation loop
function animate() {
update();
draw();
requestAnimationFrame(animate);
}
animate();
// Adjust canvas on window resize
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Reset body positions? Maybe not, but could.
// Alternatively, recalculate initial positions based on new canvas size.
// But for simplicity, not handled here.
});
Now, some notes:
- The gravitational constant G is set to 1, but might need tuning for the simulation to look nice. The initial velocity calculation uses the circular orbit formula: v = sqrt(G*M/r), where M is the central mass.
- The planets are initialized with positions on a circle, and velocities tangential to their orbits, so they should start in circular orbits. The central star has a large mass (1000) so its movement is negligible.
- The trail is stored as an array of objects, each with x,y. Each frame, the current position is added to the trail, and the array is limited to 200 elements to prevent it from getting too long.
- The draw function first clears the canvas, then draws each body's trail with a lower alpha, then draws the body as a filled circle whose radius is proportional to the square root of the mass (so that area scales with mass, but visually it's sqrt(mass) radius).
- The animation loop calls update() and draw() each frame.
Potential issues:
- The trails might become too long over time, but with the shift() method, they stay limited to 200 points.
- The canvas resizing could cause the bodies to be in the wrong position if the canvas size changes, but the code currently doesn't handle that.
- The gravitational constant and time step (dt) may need adjustment for proper scaling. For instance, with G=1 and dt=0.01, the orbits might be too fast or too slow.
- The bodies' masses: the central star has mass 1000, planets have mass 1. This should mean the star's acceleration is small, but if the dt is too large, numerical errors might accumulate.
- The initial velocity uses the circular orbit formula; if the mass of the star is changed, that might need adjustment.
Possible improvements:
- Add a scaling factor to adjust the gravitational constant and time step for better visualization.
- Implement a better way to handle canvas resizing, perhaps reinitializing the bodies' positions based on new canvas dimensions.
- Add controls to toggle trails or adjust parameters.
- Handle edge cases where two bodies get too close (distance near zero), though in this setup with the massive central body and small planets, that's less likely.
- Optimize the force calculations by not rechecking every pair twice (but for a few bodies, it's negligible).
Overall, this code should create a simulation of a star with several orbiting planets, with trails showing their paths. The user can adjust parameters like G, dt, mass, initial positions, and colors to customize the simulation.