xxxxxxxxxx
528
let bodies = [];
let sun;
let timeScale = 0.5;
let zoom = 1; // Default zoom shows entire system
let showOrbits = true;
let showLabels = true;
let selectedBody = null;
let followSelected = false;
let viewOffset = { x: 0, y: 0 };
let minPlanetSize = 5; // Minimum size for planets to ensure visibility
function setup() {
createCanvas(1000, 800); // Wider canvas for better horizontal spacing
// Create the Sun at center
sun = new Star(0, 0, 695700, color(255, 215, 0), "Sun", 25.38);
bodies.push(sun);
// Create planets with consistent sizing for visibility
// (parent, distance, eccentricity, inclination, displaySize, color, name, period, rotation, initialAngle)
// Mercury
let mercury = new Planet(sun, 35, 0.206, 7.0, minPlanetSize, color(200, 200, 200),
"Mercury", 87.97, 58.6, 0);
bodies.push(mercury);
// Venus
let venus = new Planet(sun, 65, 0.007, 3.4, minPlanetSize + 1, color(222, 184, 135),
"Venus", 224.7, -243, 1.2);
bodies.push(venus);
// Earth with Moon
let earth = new Planet(sun, 100, 0.017, 0.0, minPlanetSize + 1.5, color(70, 130, 180),
"Earth", 365.25, 1.0, 2.5);
bodies.push(earth);
let moon = new Moon(earth, 15, 0.0549, 5.145, minPlanetSize - 3, color(200, 200, 200),
"Moon", 27.32, 27.32, 0);
bodies.push(moon);
// Mars with Phobos and Deimos
let mars = new Planet(sun, 135, 0.093, 1.8, minPlanetSize + 0.5, color(255, 100, 0),
"Mars", 686.98, 1.03, 3.1);
bodies.push(mars);
let phobos = new Moon(mars, 12, 0.0151, 1.08, minPlanetSize - 3.5, color(180, 180, 180),
"Phobos", 0.32, 0.32, 0);
bodies.push(phobos);
let deimos = new Moon(mars, 18, 0.0005, 1.79, minPlanetSize - 3.5, color(170, 170, 170),
"Deimos", 1.26, 1.26, 2);
bodies.push(deimos);
// Jupiter with Galilean moons
let jupiter = new Planet(sun, 180, 0.048, 1.3, minPlanetSize + 5, color(255, 170, 50),
"Jupiter", 4332.59, 0.41, 4.2);
bodies.push(jupiter);
let io = new Moon(jupiter, 15, 0.0041, 0.04, minPlanetSize - 3, color(255, 220, 50),
"Io", 1.77, 1.77, 0);
bodies.push(io);
let europa = new Moon(jupiter, 22, 0.0094, 0.47, minPlanetSize - 3, color(220, 220, 170),
"Europa", 3.55, 3.55, 1.3);
bodies.push(europa);
let ganymede = new Moon(jupiter, 30, 0.0013, 0.19, minPlanetSize - 2.5, color(180, 180, 220),
"Ganymede", 7.15, 7.15, 2.6);
bodies.push(ganymede);
let callisto = new Moon(jupiter, 40, 0.0074, 0.19, minPlanetSize - 2.5, color(180, 160, 130),
"Callisto", 16.69, 16.69, 3.8);
bodies.push(callisto);
// Saturn with rings and Titan
let saturn = new Planet(sun, 240, 0.054, 2.5, minPlanetSize + 4, color(240, 180, 100),
"Saturn", 10759.22, 0.44, 5.1);
saturn.hasRings = true;
bodies.push(saturn);
let titan = new Moon(saturn, 25, 0.0288, 0.34, minPlanetSize - 2.5, color(210, 180, 60),
"Titan", 15.95, 15.95, 0);
bodies.push(titan);
// Uranus
let uranus = new Planet(sun, 300, 0.047, 0.8, minPlanetSize + 3, color(180, 230, 230),
"Uranus", 30688.5, -0.72, 5.8);
bodies.push(uranus);
// Neptune
let neptune = new Planet(sun, 350, 0.009, 1.8, minPlanetSize + 3, color(100, 150, 255),
"Neptune", 60195, 0.67, 6.5);
bodies.push(neptune);
// Create UI elements
createUI();
}
function draw() {
background(0);
// Apply view transform
translate(width / 2 + viewOffset.x, height / 2 + viewOffset.y);
scale(zoom);
// Update all celestial bodies
for (let body of bodies) {
body.update();
}
// First draw orbits
if (showOrbits) {
for (let body of bodies) {
if (body !== sun) {
body.drawOrbit();
}
}
}
// Then draw the bodies themselves (to ensure proper layering)
for (let body of bodies) {
body.display();
}
// Follow selected body if option is enabled
if (followSelected && selectedBody) {
let scaledPos = {
x: -selectedBody.position.x * zoom,
y: -selectedBody.position.y * zoom
};
viewOffset.x = lerp(viewOffset.x, scaledPos.x, 0.1);
viewOffset.y = lerp(viewOffset.y, scaledPos.y, 0.1);
}
// Show information about selected body
if (selectedBody) {
showBodyInfo(selectedBody);
}
}
// Base class for all celestial bodies
class CelestialBody {
constructor(position, displaySize, color, name, rotationPeriod) {
this.position = position;
this.displaySize = displaySize; // Visual size (not to scale)
this.color = color;
this.name = name;
this.rotationPeriod = rotationPeriod; // Days for one rotation
this.rotationAngle = 0;
this.trailPoints = [];
this.maxTrail = 50;
}
update() {
// Update rotation (day/night cycle)
if (this.rotationPeriod !== 0) {
this.rotationAngle += (0.1 * timeScale) / abs(this.rotationPeriod);
}
}
display() {
// Draw the body at its position
push();
translate(this.position.x, this.position.y);
// Day/night cycle visualization
this.drawDayNightCycle();
// Draw selection indicator if this body is selected
if (this === selectedBody) {
stroke(255);
strokeWeight(1 / zoom);
noFill();
ellipse(0, 0, this.displaySize * 2.5, this.displaySize * 2.5);
}
// Draw label if enabled
if (showLabels) {
this.drawLabel();
}
pop();
}
drawDayNightCycle() {
// Base day/night visualization
push();
rotate(this.rotationAngle);
// Day side (full color)
fill(this.color);
noStroke();
arc(0, 0, this.displaySize * 2, this.displaySize * 2, -HALF_PI, HALF_PI, CHORD);
// Night side (darker)
fill(red(this.color) * 0.4, green(this.color) * 0.4, blue(this.color) * 0.4);
arc(0, 0, this.displaySize * 2, this.displaySize * 2, HALF_PI, -HALF_PI, CHORD);
pop();
}
drawLabel() {
fill(255);
noStroke();
textSize(10 / zoom);
textAlign(CENTER);
text(this.name, 0, this.displaySize + 12 / zoom);
}
containsPoint(x, y) {
// Check if coordinates are within this body
let d = dist(x, y, this.position.x, this.position.y);
return d < this.displaySize;
}
}
class Star extends CelestialBody {
constructor(x, y, radius, color, name, rotationPeriod) {
super({x, y}, 15, color, name, rotationPeriod); // Fixed size for sun
}
drawDayNightCycle() {
// Stars don't have day/night, they glow
noStroke();
// Outer glow
for (let i = 3; i > 0; i--) {
fill(red(this.color), green(this.color), blue(this.color), 50 / i);
ellipse(0, 0, this.displaySize * 2 + i * 6 / zoom, this.displaySize * 2 + i * 6 / zoom);
}
// Main body
fill(this.color);
ellipse(0, 0, this.displaySize * 2, this.displaySize * 2);
}
}
class Planet extends CelestialBody {
constructor(parent, distance, eccentricity, inclination, displaySize, color, name, siderealPeriod, rotationPeriod, initialAngle) {
super({x: 0, y: 0}, displaySize, color, name, rotationPeriod);
this.parent = parent;
this.distance = distance; // Visual distance (not to scale)
this.eccentricity = eccentricity;
this.inclination = inclination * PI / 180; // Convert to radians
this.siderealPeriod = siderealPeriod; // In Earth days
this.trueAnomaly = initialAngle;
this.hasRings = false;
this.actualRadius = displaySize * 1000; // Just for display info
}
update() {
super.update();
// Update orbital position using Kepler's laws
this.trueAnomaly += (0.01 * timeScale) / (this.siderealPeriod / 365.25);
// Calculate position using polar equation of an ellipse
let r = (this.distance * (1 - this.eccentricity * this.eccentricity)) /
(1 + this.eccentricity * cos(this.trueAnomaly));
// Calculate cartesian coordinates
let xOrbit = r * cos(this.trueAnomaly);
let yOrbit = r * sin(this.trueAnomaly) * cos(this.inclination);
// Update position
this.position = {
x: this.parent.position.x + xOrbit,
y: this.parent.position.y + yOrbit
};
// Update trail
if (this.trailPoints.length > this.maxTrail) {
this.trailPoints.shift();
}
this.trailPoints.push({x: this.position.x, y: this.position.y});
}
drawOrbit() {
// Draw orbit trail
if (this.trailPoints.length > 2) {
stroke(red(this.color), green(this.color), blue(this.color), 100);
strokeWeight(1 / zoom);
noFill();
beginShape();
for (let point of this.trailPoints) {
vertex(point.x, point.y);
}
endShape();
}
// Draw calculated elliptical orbit path
noFill();
stroke(50);
strokeWeight(0.3 / zoom);
beginShape();
for (let angle = 0; angle < TWO_PI; angle += 0.1) {
let r = (this.distance * (1 - this.eccentricity * this.eccentricity)) /
(1 + this.eccentricity * cos(angle));
let x = r * cos(angle);
let y = r * sin(angle) * cos(this.inclination);
vertex(this.parent.position.x + x, this.parent.position.y + y);
}
endShape(CLOSE);
}
display() {
super.display();
// Draw rings for Saturn
if (this.hasRings) {
push();
translate(this.position.x, this.position.y);
// Draw rings
noFill();
strokeWeight(1.5 / zoom);
// Inner ring
stroke(240, 180, 100, 150);
ellipse(0, 0, this.displaySize * 4, this.displaySize * 1.5);
// Outer ring
stroke(240, 180, 100, 100);
ellipse(0, 0, this.displaySize * 5, this.displaySize * 2);
pop();
}
}
}
class Moon extends CelestialBody {
constructor(parent, distance, eccentricity, inclination, displaySize, color, name, siderealPeriod, rotationPeriod, initialAngle) {
super({x: 0, y: 0}, displaySize, color, name, rotationPeriod);
this.parent = parent;
this.distance = distance; // Visual distance from parent
this.eccentricity = eccentricity;
this.inclination = inclination * PI / 180; // Convert to radians
this.siderealPeriod = siderealPeriod; // In Earth days
this.trueAnomaly = initialAngle;
this.actualRadius = displaySize * 200; // Just for display info
}
update() {
super.update();
// Update orbital position
this.trueAnomaly += (0.05 * timeScale) / this.siderealPeriod;
// Calculate position using polar equation of an ellipse
let r = (this.distance * (1 - this.eccentricity * this.eccentricity)) /
(1 + this.eccentricity * cos(this.trueAnomaly));
// Calculate cartesian coordinates
let xOrbit = r * cos(this.trueAnomaly);
let yOrbit = r * sin(this.trueAnomaly) * cos(this.inclination);
// Update position relative to parent
this.position = {
x: this.parent.position.x + xOrbit,
y: this.parent.position.y + yOrbit
};
}
drawOrbit() {
// Draw orbit path around parent
noFill();
stroke(50);
strokeWeight(0.2 / zoom);
beginShape();
for (let angle = 0; angle < TWO_PI; angle += 0.1) {
let r = (this.distance * (1 - this.eccentricity * this.eccentricity)) /
(1 + this.eccentricity * cos(angle));
let x = r * cos(angle);
let y = r * sin(angle) * cos(this.inclination);
vertex(this.parent.position.x + x, this.parent.position.y + y);
}
endShape(CLOSE);
}
}
function showBodyInfo(body) {
push();
resetMatrix();
fill(0, 180);
noStroke();
rect(10, 10, 220, 150);
fill(255);
textAlign(LEFT);
textSize(16);
text(body.name, 20, 35);
textSize(12);
let infoY = 60;
if (body instanceof Planet) {
text(`Distance from Sun: ${body.distance} units`, 20, infoY);
infoY += 20;
text(`Eccentricity: ${body.eccentricity.toFixed(4)}`, 20, infoY);
infoY += 20;
text(`Orbital period: ${body.siderealPeriod.toFixed(2)} Earth days`, 20, infoY);
infoY += 20;
} else if (body instanceof Moon) {
text(`Orbiting: ${body.parent.name}`, 20, infoY);
infoY += 20;
text(`Orbital period: ${body.siderealPeriod.toFixed(2)} Earth days`, 20, infoY);
infoY += 20;
}
let rotText = body.rotationPeriod < 0 ?
`Rotation: ${abs(body.rotationPeriod).toFixed(2)} days (retrograde)` :
`Rotation: ${body.rotationPeriod.toFixed(2)} days`;
text(rotText, 20, infoY);
pop();
}
// Helper function to immediately center view on a celestial body
function centerOnBody(body) {
selectedBody = body;
followSelected = true;
// Immediately update viewOffset to center on the body
viewOffset = {
x: -body.position.x * zoom,
y: -body.position.y * zoom
};
}
function createUI() {
// Time scale slider
createP("Simulation Speed:");
let timeSlider = createSlider(0, 5, timeScale, 0.1);
timeSlider.input(() => {
timeScale = timeSlider.value();
});
// Zoom slider
createP("Zoom:");
let zoomSlider = createSlider(0.3, 5, zoom, 0.1);
zoomSlider.input(() => {
zoom = zoomSlider.value();
// If following a planet, update viewOffset immediately with new zoom
if (followSelected && selectedBody) {
viewOffset = {
x: -selectedBody.position.x * zoom,
y: -selectedBody.position.y * zoom
};
}
});
// Toggle options
let orbitToggle = createCheckbox('Show Orbits', showOrbits);
orbitToggle.changed(() => {
showOrbits = orbitToggle.checked();
});
let labelToggle = createCheckbox('Show Labels', showLabels);
labelToggle.changed(() => {
showLabels = labelToggle.checked();
});
let followToggle = createCheckbox('Follow Selected', followSelected);
followToggle.changed(() => {
followSelected = followToggle.checked();
if (!followSelected) {
viewOffset = { x: 0, y: 0 };
} else if (selectedBody) {
// If enabling follow and a body is selected, center on it immediately
viewOffset = {
x: -selectedBody.position.x * zoom,
y: -selectedBody.position.y * zoom
};
}
});
// Focus buttons for quick navigation
createP("Focus on:");
let planetButtons = createDiv();
// Create a button for each major planet
for (let body of bodies) {
if (body instanceof Planet || body === sun) {
let btn = createButton(body.name);
btn.parent(planetButtons);
btn.style('margin', '2px');
btn.mousePressed(() => {
// Use the centerOnBody helper to immediately center the view
centerOnBody(body);
followToggle.checked(true);
});
}
}
// Reset button
let resetButton = createButton("Reset View");
resetButton.mousePressed(() => {
zoom = 1;
zoomSlider.value(1);
timeScale = 0.5;
timeSlider.value(0.5);
viewOffset = { x: 0, y: 0 };
selectedBody = null;
followSelected = false;
followToggle.checked(false);
});
}
function mousePressed() {
// Convert mouse coordinates to simulation coordinates
let simX = (mouseX - width/2 - viewOffset.x) / zoom;
let simY = (mouseY - height/2 - viewOffset.y) / zoom;
// Check if any body was clicked
let foundBody = false;
for (let i = bodies.length - 1; i >= 0; i--) {
if (bodies[i].containsPoint(simX, simY)) {
selectedBody = bodies[i];
foundBody = true;
break;
}
}
if (!foundBody) {
selectedBody = null;
}
}