theme of the day but its super good

This sketch renders a realistic 3D Earth with a satellite orbiting around it under simulated gravity, surrounded by a glowing starfield. The satellite leaves a glowing orange trail, and interactive controls let you adjust gravity, masses, and initial orbital conditions to see how the orbit changes in real time.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the orbit trail longer — A longer trail shows more orbital history, making the path shape clearer and more beautiful.
  2. Make the starfield sparser — Fewer stars reveal more of the orbit, and the scene loads faster with less to render.
  3. Change the orbit trail to cyan — The trail color shifts from warm orange to cool cyan, changing the aesthetic completely.
  4. Increase Earth's visual size — A larger Earth looks more imposing and makes relative orbital distances feel smaller.
  5. Make the satellite glow red — Changing the satellite body color to red makes it stand out dramatically against the starfield.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a mesmerizing 3D orbital mechanics simulator using p5.js in WEBGL mode. A textured Earth sits at the center while a detailed satellite orbits around it under simulated gravitational attraction. As the satellite moves, it traces an orange glowing trail showing its orbital path. What makes this sketch visually striking is the combination of realistic physics simulation, 3D graphics with lighting and textures, and the interactive dat.gui control panel that lets you tweak gravitational constants, masses, and initial velocities live to see how orbits change.

The code is organized into three main layers: setup and initialization (which loads the Earth texture, generates stars, and creates GUI controls), the physics simulation in draw() that calculates gravitational forces and updates positions each frame, and helper functions that handle force calculations and 3D rendering. By studying this sketch you will learn how to build a 3D WEBGL canvas, apply textures to 3D objects, simulate physics using Newton's laws, manage trail effects with arrays, and create interactive parameter controls that drive a live simulation.

⚙️ How It Works

  1. When the sketch loads, preload() fetches a high-resolution Earth texture from a CDN, and setup() creates a WEBGL canvas, generates 500 random stars scattered in 3D space, and builds a dat.gui control panel with sliders for gravitational constant, mass values, and satellite initial position and velocity.
  2. On each frame, draw() clears the background with black, renders all stars as tiny spheres, and draws the Earth at the origin with the loaded texture mapped onto it.
  3. The physics engine calculates the gravitational force pulling the satellite toward Earth using Newton's universal law of gravitation: F = G × (M₁ × M₂) / r². This force is divided by the satellite's mass to get acceleration.
  4. The satellite's velocity is updated by adding the acceleration each frame, and its position is updated by adding the velocity—a simple integration scheme called Euler's method that approximates continuous motion with discrete steps.
  5. As the satellite moves, its current position is added to the orbitPath array; when the array exceeds 200 positions, the oldest is removed so the trail has a smooth, fading appearance.
  6. Finally, on-screen text displays the current gravitational parameters and orbital statistics like distance to Earth, altitude, speed, and estimated orbital period, calculated from the simulation state.

🎓 Concepts You'll Learn

3D graphics and WEBGL renderingNewtonian gravity and physics simulationVector mathematics and forcesEuler integration for animationTexture mapping and materialsOrbit trails and history arraysInteractive parameter controls with dat.gui

📝 Code Breakdown

preload()

preload() is a special function that runs once before setup(). Use it to load images, fonts, and other assets from the internet. Without preload(), your sketch would start before files finish downloading, causing errors.

function preload() {
  // Load Earth texture from a reliable CDN
  // Using the specific r147 tag for reliable access on raw.githubusercontent.com
  earthTexture = loadImage('https://raw.githubusercontent.com/mrdoob/three.js/r147/examples/textures/planets/earth_atmos_2048.jpg');

  // Load a font for WEBGL text from fontsource CDN (unpkg.com is reliable)
  webglFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

function-call Load Earth Texture earthTexture = loadImage('https://raw.githubusercontent.com/mrdoob/three.js/r147/examples/textures/planets/earth_atmos_2048.jpg');

Fetches a high-resolution 2048×2048 pixel image of Earth from a CDN and stores it in the earthTexture variable

function-call Load WEBGL Font webglFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');

Loads the Roboto font from a CDN so that 3D text in WEBGL mode renders cleanly and readable

earthTexture = loadImage('https://raw.githubusercontent.com/mrdoob/three.js/r147/examples/textures/planets/earth_atmos_2048.jpg');
loadImage() downloads the image from the URL and stores it. This image will be wrapped around the Earth sphere later using the texture() function.
webglFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
loadFont() downloads the font file from the CDN and stores it. p5.js needs this font loaded before setup() runs so text() calls in WEBGL mode will render properly.

setup()

setup() runs once when the sketch starts. It creates the canvas, initializes all variables, loads assets, and sets up the interactive controls. For 3D sketches, the WEBGL parameter is essential—without it, you cannot use 3D functions like sphere() or texture().

🔬 This loop creates stars with random coordinates multiplied by 2 (the `width * 2` part). What happens if you change `width * 2` to `width * 10`? The stars will spread out farther or closer?

  // Generate star positions
  for (let i = 0; i < NUM_STARS; i++) {
    stars.push(createVector(
      random(-width * 2, width * 2), // Larger range for stars
      random(-height * 2, height * 2),
      random(-width * 2, width * 2)
    ));
  }
function setup() {
  // Create a WEBGL canvas for 3D rendering
  createCanvas(windowWidth, windowHeight, WEBGL);

  // Set up camera controls (mouse interaction to rotate/zoom the scene)
  orbitControl();

  // No outlines for the spheres
  noStroke();

  // Set a frame rate for more consistent simulation steps
  frameRate(60);

  // Set the font for WEBGL text
  textFont(webglFont);
  textSize(16);

  // Generate star positions
  for (let i = 0; i < NUM_STARS; i++) {
    stars.push(createVector(
      random(-width * 2, width * 2), // Larger range for stars
      random(-height * 2, height * 2),
      random(-width * 2, width * 2)
    ));
  }

  // --- Initialize GUI ---
  const gui = new dat.GUI();
  gui.add(params, 'gConstant', 0.0001, 0.01).step(0.0001).name('G Constant').onChange(resetSimulation);
  gui.add(params, 'earthMass', 100, 5000).step(10).name('Earth Mass').onChange(resetSimulation);
  gui.add(params, 'satelliteMass', 0.1, 5).step(0.1).name('Satellite Mass').onChange(resetSimulation);

  const satelliteFolder = gui.addFolder('Satellite Initial State');
  satelliteFolder.add(params, 'satelliteInitialX', -width, width).step(1).name('Position X').onChange(resetSimulation);
  satelliteFolder.add(params, 'satelliteInitialY', -height, height).step(1).name('Position Y').onChange(resetSimulation);
  satelliteFolder.add(params, 'satelliteInitialZ', -width, width).step(1).name('Position Z').onChange(resetSimulation);
  satelliteFolder.add(params, 'satelliteInitialVelX', -10, 10).step(0.1).name('Velocity X').onChange(resetSimulation);
  satelliteFolder.add(params, 'satelliteInitialVelY', -10, 10).step(0.1).name('Velocity Y').onChange(resetSimulation);
  satelliteFolder.add(params, 'satelliteInitialVelZ', -10, 10).step(0.1).name('Velocity Z').onChange(resetSimulation);
  satelliteFolder.add(params, 'resetSimulation').name('Reset Simulation');

  // Initial simulation setup
  resetSimulation();
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

function-call Create WEBGL Canvas createCanvas(windowWidth, windowHeight, WEBGL);

Creates a 3D canvas that fills the browser window and enables WEBGL rendering mode for 3D graphics

for-loop Star Position Generation for (let i = 0; i < NUM_STARS; i++) {

Loops 500 times to create random 3D coordinates for each star scattered throughout the background

function-call GUI Control Panel Creation const gui = new dat.GUI();

Creates an interactive control panel in the top-right corner where users can adjust physics parameters in real time

createCanvas(windowWidth, windowHeight, WEBGL);
Creates a full-screen canvas in WEBGL mode, which enables 3D graphics, lighting, textures, and camera control in p5.js.
orbitControl();
Enables mouse-based camera control so users can drag to rotate the view, scroll to zoom, and right-click to pan—perfect for exploring a 3D scene.
noStroke();
Disables black outlines around all drawn shapes, giving spheres and boxes a cleaner appearance without visible edges.
frameRate(60);
Sets the frame rate to exactly 60 frames per second, making the physics simulation and animation consistent across all computers.
textFont(webglFont);
Tells p5.js to use the Roboto font (loaded in preload) for all text rendered in WEBGL mode.
for (let i = 0; i < NUM_STARS; i++) {
This loop runs 500 times (NUM_STARS = 500), creating a new random 3D position for each star.
stars.push(createVector(
createVector() creates a 3D point, and push() adds it to the stars array so we can draw it later.
const gui = new dat.GUI();
Creates a new dat.gui interface object that will hold all our interactive sliders and buttons.
gui.add(params, 'gConstant', 0.0001, 0.01).step(0.0001).name('G Constant').onChange(resetSimulation);
Adds a slider for gravitational constant ranging from 0.0001 to 0.01; whenever the user moves it, resetSimulation() is called to restart the orbit with the new value.
resetSimulation();
Calls the function that initializes the satellite's position and velocity based on the current GUI parameters.

resetSimulation()

resetSimulation() is called whenever the user changes any GUI parameter. It recalculates the satellite's starting position and velocity based on physics formulas, then clears the old orbit trail. The key insight is the orbital velocity formula: v = √(GM/r)—a planet farther away orbits slower, and a star with greater mass requires faster orbits.

🔬 The first line sets a Y velocity that would make a perfect circle. Then the next three lines replace all three components with GUI values. What happens if you comment out those three lines? Will the orbit stay perfectly circular?

  // Default velocity for circular orbit, then apply GUI overrides
  satelliteVel = createVector(0, circularSpeed, 0);

  // Apply GUI velocity overrides
  satelliteVel.x = params.satelliteInitialVelX;
  satelliteVel.y = params.satelliteInitialVelY;
  satelliteVel.z = params.satelliteInitialVelZ;
function resetSimulation() {
  // Initialize satellite position and velocity for an initial orbit
  // For a stable circular orbit, v = sqrt(G*M/r)
  const initialDistance = EARTH_RADIUS * 3; // Start satellite 3 times Earth's radius away
  satellitePos = createVector(
    params.satelliteInitialX,
    params.satelliteInitialY,
    params.satelliteInitialZ
  );

  // Calculate initial velocity for a roughly circular orbit at this distance
  // If satellite is at origin, use default initialDistance to avoid division by zero
  const currentDistance = satellitePos.mag();
  const effectiveDistance = currentDistance === 0 ? initialDistance : currentDistance;
  const circularSpeed = sqrt(params.gConstant * params.earthMass / effectiveDistance);

  // Default velocity for circular orbit, then apply GUI overrides
  satelliteVel = createVector(0, circularSpeed, 0);

  // Apply GUI velocity overrides
  satelliteVel.x = params.satelliteInitialVelX;
  satelliteVel.y = params.satelliteInitialVelY;
  satelliteVel.z = params.satelliteInitialVelZ;

  // Clear the orbit path
  orbitPath = [];
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Satellite Position Setup satellitePos = createVector(

Creates the satellite's starting position in 3D space using values from the GUI controls

calculation Circular Orbital Speed Formula const circularSpeed = sqrt(params.gConstant * params.earthMass / effectiveDistance);

Uses the physics formula v = √(GM/r) to calculate the speed needed for a stable circular orbit at the current distance

assignment Apply GUI Velocity Overrides satelliteVel.x = params.satelliteInitialVelX;

Replaces the calculated circular velocity with whatever the user set in the GUI, allowing elliptical or escape orbits

const initialDistance = EARTH_RADIUS * 3; // Start satellite 3 times Earth's radius away
Sets a fallback distance for calculating circular orbit speed. EARTH_RADIUS is 30, so this is 90 units away.
satellitePos = createVector(
Initializes the satellite's position vector using the current GUI values for X, Y, and Z coordinates.
const currentDistance = satellitePos.mag();
mag() returns the length of the position vector—how far the satellite is from Earth (at the origin).
const effectiveDistance = currentDistance === 0 ? initialDistance : currentDistance;
If the satellite is at the exact center (distance 0), use the fallback distance to avoid dividing by zero; otherwise use the actual distance.
const circularSpeed = sqrt(params.gConstant * params.earthMass / effectiveDistance);
This is the physics formula for orbital velocity: v = √(GM/r). It calculates the exact speed needed to orbit in a perfect circle at this distance.
satelliteVel = createVector(0, circularSpeed, 0);
Sets the satellite's initial velocity to the calculated circular speed in the Y direction, creating a stable starting orbit.
satelliteVel.x = params.satelliteInitialVelX;
Overrides the calculated Y velocity with whatever the user set in the GUI, allowing them to create non-circular orbits.
orbitPath = [];
Clears the old orbit trail array so the new simulation starts with a clean slate.

draw()

draw() is called 60 times per second. It combines rendering (drawing stars, Earth, and satellite), physics (calculating gravity and updating positions), and visualization (the glowing trail and on-screen text). The order matters: we clear the background first (removing the old frame), update physics, draw everything, then display statistics. This pattern—clear, update, draw—powers nearly all interactive p5.js sketches.

🔬 These two lines are the heart of the physics animation—they update velocity first, then position. What happens if you swap them? What if you add the acceleration twice: satelliteVel.add(acceleration); satelliteVel.add(acceleration);

  // Update satellite's velocity
  satelliteVel.add(acceleration);

  // Update satellite's position
  satellitePos.add(satelliteVel);

🔬 This loop draws a line by connecting vertices. What happens if you change the loop to jump every 2nd position? Change `i++` to `i += 2`. Will the trail become dashed or dotted?

  // Draw the path as a line
  push();
  stroke(255, 150, 0, 200); // Orange for orbit path, slightly transparent
  noFill();
  beginShape();
  for (let i = 0; i < orbitPath.length; i++) {
    vertex(orbitPath[i].x, orbitPath[i].y, orbitPath[i].z);
  }
  endShape();
  pop();
function draw() {
  // --- Rendering ---
  background(0); // Black background (space)
  ambientLight(50); // General ambient light
  directionalLight(255, 255, 255, -1, 0, 0); // White light from the right side

  // --- Draw Stars ---
  push();
  fill(255);
  for (let i = 0; i < NUM_STARS; i++) {
    push();
    translate(stars[i].x, stars[i].y, stars[i].z);
    sphere(0.5); // Small sphere for each star
    pop();
  }
  pop();

  // --- Draw Earth (Central Body) ---
  push();
  texture(earthTexture); // Apply Earth texture
  sphere(EARTH_RADIUS);
  pop();

  // --- Physics Calculation ---
  // Calculate the gravitational force on the satellite
  const force = calculateGravitationalForce(satellitePos, params.satelliteMass, createVector(0, 0, 0), params.earthMass);

  // Apply the force to get acceleration (F = ma => a = F/m)
  const acceleration = p5.Vector.div(force, params.satelliteMass);

  // Update satellite's velocity
  satelliteVel.add(acceleration);

  // Update satellite's position
  satellitePos.add(satelliteVel);

  // --- Draw Detailed Satellite ---
  drawDetailedSatellite();

  // --- Draw Orbit Trail ---
  orbitPath.push(satellitePos.copy()); // Store current position
  if (orbitPath.length > ORBIT_TRAIL_LENGTH) {
    orbitPath.shift(); // Remove oldest position if path is too long
  }

  // Draw the path as a line
  push();
  stroke(255, 150, 0, 200); // Orange for orbit path, slightly transparent
  noFill();
  beginShape();
  for (let i = 0; i < orbitPath.length; i++) {
    vertex(orbitPath[i].x, orbitPath[i].y, orbitPath[i].z);
  }
  endShape();
  pop();
  noStroke(); // Turn stroke off for next frame

  // --- Display Information ---
  push();
  // Move text to a fixed position on the screen
  camera(); // Reset camera transformations for text
  translate(-width / 2 + 20, -height / 2 + 20, 0); // Top-left corner

  fill(255); // White text
  textAlign(LEFT, TOP);
  let distanceToEarth = satellitePos.mag();
  let altitude = distanceToEarth - EARTH_RADIUS; // Altitude relative to Earth's surface
  let satelliteSpeed = satelliteVel.mag();

  // Estimate orbital period (very rough for non-circular orbits)
  let orbitalPeriodEstimate = (TWO_PI * distanceToEarth) / satelliteSpeed;

  text(`Simulation Details:`, 0, 0);
  text(`G Constant: ${params.gConstant}`, 0, 20);
  text(`Earth Mass: ${params.earthMass}`, 0, 40);
  text(`Satellite Mass: ${params.satelliteMass}`, 0, 60);

  text(`\nSatellite Orbit Details:`, 0, 80);
  text(`Distance to Earth: ${distanceToEarth.toFixed(2)} units`, 0, 100);
  text(`Altitude: ${altitude.toFixed(2)} units`, 0, 120);
  text(`Satellite Speed: ${satelliteSpeed.toFixed(2)} units/frame`, 0, 140);
  if (satelliteSpeed > 0) {
    text(`Estimated Orbital Period: ${orbitalPeriodEstimate.toFixed(2)} frames`, 0, 160);
  } else {
    text(`Estimated Orbital Period: N/A`, 0, 160);
  }
  pop();
}
Line-by-line explanation (22 lines)

🔧 Subcomponents:

for-loop Star Rendering Loop for (let i = 0; i < NUM_STARS; i++) {

Loops through all 500 stars, translating to each position and drawing a tiny sphere to create the starfield backdrop

function-call Earth Rendering sphere(EARTH_RADIUS);

Draws the main Earth sphere with the loaded texture mapped onto its surface, creating the central gravitational body

function-call Gravitational Force Calculation const force = calculateGravitationalForce(satellitePos, params.satelliteMass, createVector(0, 0, 0), params.earthMass);

Calls the physics function to compute the gravitational force between Earth and the satellite

calculation Euler Integration Step satelliteVel.add(acceleration);

Updates velocity by adding acceleration, then updates position by adding velocity—the two-step process that animates the satellite

for-loop Trail Rendering Loop for (let i = 0; i < orbitPath.length; i++) {

Draws a line connecting all stored positions in the orbitPath array to visualize the satellite's orbital history

function-call On-Screen Statistics Display text(`Distance to Earth: ${distanceToEarth.toFixed(2)} units`, 0, 100);

Renders live orbital statistics like distance, altitude, speed, and estimated orbital period on the screen

background(0); // Black background (space)
Fills the entire canvas with black before drawing each frame, erasing previous content and preventing trails.
ambientLight(50); // General ambient light
Adds a dim ambient light that illuminates all objects from all directions, preventing parts of the Earth from being completely black.
directionalLight(255, 255, 255, -1, 0, 0); // White light from the right side
Adds a bright directional light coming from the right (-1 in X direction), creating highlights and shadows on the Earth and satellite.
push();
Saves the current transformation state (translate, rotate, scale) so changes inside this block don't affect code outside it.
for (let i = 0; i < NUM_STARS; i++) {
Loops 500 times to draw all stars stored in the stars array.
translate(stars[i].x, stars[i].y, stars[i].z);
Moves the drawing position to the location of the current star in 3D space.
sphere(0.5); // Small sphere for each star
Draws a tiny sphere (diameter 0.5) at the translated position, creating a single star in the background.
texture(earthTexture); // Apply Earth texture
Tells p5.js to wrap the loaded Earth image around the upcoming sphere, making it look like a real planet.
const force = calculateGravitationalForce(satellitePos, params.satelliteMass, createVector(0, 0, 0), params.earthMass);
Calls the gravity function to calculate the attraction between the satellite (at satellitePos) and Earth (at the origin).
const acceleration = p5.Vector.div(force, params.satelliteMass);
Uses Newton's second law (F = ma) rearranged as a = F/m to convert the force into acceleration.
satelliteVel.add(acceleration);
Updates the satellite's velocity vector by adding the acceleration—each frame, gravity slightly changes the velocity.
satellitePos.add(satelliteVel);
Updates the satellite's position vector by adding the velocity—each frame, the satellite moves in the direction it's traveling.
orbitPath.push(satellitePos.copy()); // Store current position
Adds a copy of the current position to the orbitPath array so we can draw the trail later. We use copy() to store a snapshot, not a reference.
if (orbitPath.length > ORBIT_TRAIL_LENGTH) {
Checks whether the trail has more than 200 stored positions. If so, we remove the oldest one to keep the trail at a fixed length.
orbitPath.shift(); // Remove oldest position if path is too long
shift() removes the first element (oldest position) from the array, keeping the trail from growing infinitely long and slowing down the sketch.
stroke(255, 150, 0, 200); // Orange for orbit path, slightly transparent
Sets the line color to orange (255, 150, 0) with an alpha of 200, making it semi-transparent so it glows nicely.
vertex(orbitPath[i].x, orbitPath[i].y, orbitPath[i].z);
Adds each stored position as a point in the line being drawn, connecting all past positions to create the continuous glowing trail.
camera(); // Reset camera transformations for text
Resets the 3D camera so the following text is drawn in screen space (fixed position on the canvas) rather than floating in the 3D world.
let distanceToEarth = satellitePos.mag();
mag() returns the length of the position vector—the straight-line distance from the satellite to Earth at the origin.
let altitude = distanceToEarth - EARTH_RADIUS; // Altitude relative to Earth's surface
Subtracts Earth's radius from the distance to get altitude—how far above Earth's surface the satellite is.
let orbitalPeriodEstimate = (TWO_PI * distanceToEarth) / satelliteSpeed;
Estimates how many frames it would take the satellite to complete one orbit by dividing the orbital circumference (2π × r) by the current speed.
text(`Distance to Earth: ${distanceToEarth.toFixed(2)} units`, 0, 100);
Renders the distance as on-screen text. toFixed(2) limits it to 2 decimal places so it's readable.

calculateGravitationalForce()

This function implements Newton's universal law of gravitation: F = G × M₁ × M₂ / r². It receives the satellite's position and mass, Earth's position and mass, and returns a force vector pointing from the satellite toward Earth. The force's magnitude depends on both masses and decreases with the square of the distance—this 1/r² relationship is what makes circular orbits stable.

function calculateGravitationalForce(pos1, mass1, pos2, mass2) {
  // Vector from pos1 to pos2
  const direction = p5.Vector.sub(pos2, pos1);
  const distance = direction.mag();

  // Ensure distance is not zero to prevent division by zero
  if (distance === 0) {
    return createVector(0, 0, 0);
  }

  // Calculate magnitude of gravitational force
  const forceMagnitude = (params.gConstant * mass1 * mass2) / (distance * distance);

  // Normalize direction vector and scale by force magnitude
  direction.normalize();
  direction.mult(forceMagnitude);

  return direction;
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

calculation Direction Vector const direction = p5.Vector.sub(pos2, pos1);

Computes the vector pointing from the satellite toward Earth, which determines which way gravity pulls

conditional Division by Zero Guard if (distance === 0) {

Prevents crashes by checking if satellite and Earth are at the same location before dividing by distance

calculation Newton's Law of Universal Gravitation const forceMagnitude = (params.gConstant * mass1 * mass2) / (distance * distance);

Implements the physics formula F = G × M₁ × M₂ / r² to calculate how strong gravity is at this distance

function-call Force Vector Construction direction.normalize();

Converts the direction vector to unit length so we can scale it by the force magnitude without changing its direction

const direction = p5.Vector.sub(pos2, pos1);
Subtracts pos1 from pos2 to get a vector pointing from the satellite toward Earth. For Earth at (0,0,0) and satellite at (50,0,0), this gives (-50,0,0).
const distance = direction.mag();
mag() calculates the length of the direction vector—the straight-line distance between the two bodies.
if (distance === 0) {
Checks whether the two bodies are at the exact same point. If so, returning (0,0,0) prevents division by zero errors.
return createVector(0, 0, 0);
Returns a zero-magnitude force vector when distance is zero, safely avoiding undefined behavior.
const forceMagnitude = (params.gConstant * mass1 * mass2) / (distance * distance);
This is Newton's law of universal gravitation: F = G × M₁ × M₂ / r². Larger masses or smaller distances produce larger forces.
direction.normalize();
Normalizes the direction vector so it has length 1, preserving direction but removing the distance information so we can scale it separately.
direction.mult(forceMagnitude);
Multiplies the unit direction vector by the force magnitude, producing a vector pointing toward Earth with length equal to the force strength.
return direction;
Returns the complete force vector, pointing from the satellite toward Earth with magnitude calculated by the physics formula.

drawDetailedSatellite()

drawDetailedSatellite() creates a 3D model of the satellite by composing simple shapes (boxes and a cylinder). Each push() and pop() pair isolates the transformation (translate) for that component, so changes to one part don't affect the others. The satellite is positioned at its current orbital location (satellitePos) and rotates with the camera as you interact with the scene.

🔬 The main body is scaled by factors like 0.8 and 1.5. What happens if you change SATELLITE_RADIUS * 1.5 to SATELLITE_RADIUS * 3 in the main body line? Will the satellite become taller and potentially look more like a rocket?

  // Main body (a small box)
  box(SATELLITE_RADIUS * 0.8, SATELLITE_RADIUS * 1.5, SATELLITE_RADIUS * 0.8);

  // Solar panels
  push();
  fill(50, 50, 100); // Dark blue for solar panels
  translate(SATELLITE_RADIUS * 0.8, 0, 0);
  box(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 2, SATELLITE_RADIUS * 1.5);
  pop();
function drawDetailedSatellite() {
  push();
  translate(satellitePos.x, satellitePos.y, satellitePos.z);
  specularMaterial(150, 150, 150); // Gray metallic for satellite body

  // Main body (a small box)
  box(SATELLITE_RADIUS * 0.8, SATELLITE_RADIUS * 1.5, SATELLITE_RADIUS * 0.8);

  // Solar panels
  push();
  fill(50, 50, 100); // Dark blue for solar panels
  translate(SATELLITE_RADIUS * 0.8, 0, 0);
  box(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 2, SATELLITE_RADIUS * 1.5);
  pop();

  push();
  fill(50, 50, 100); // Dark blue for solar panels
  translate(-SATELLITE_RADIUS * 0.8, 0, 0);
  box(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 2, SATELLITE_RADIUS * 1.5);
  pop();

  // Antenna
  push();
  fill(200, 150, 0); // Gold/brass color for antenna
  translate(0, SATELLITE_RADIUS * 0.75, 0);
  cylinder(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 0.5);
  pop();

  pop();
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

function-call Main Satellite Body box(SATELLITE_RADIUS * 0.8, SATELLITE_RADIUS * 1.5, SATELLITE_RADIUS * 0.8);

Draws the central rectangular body of the satellite using scaled dimensions based on SATELLITE_RADIUS

function-call Left Solar Panel box(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 2, SATELLITE_RADIUS * 1.5);

Draws a thin rectangular panel extending to the left of the satellite body to represent a solar panel

function-call Right Solar Panel box(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 2, SATELLITE_RADIUS * 1.5);

Draws another thin rectangular panel mirroring the left one, creating symmetrical solar panel wings

function-call Antenna cylinder(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 0.5);

Draws a small cylindrical antenna extending upward from the satellite to enhance its realistic appearance

push();
Saves the current transformation state before applying local translations and rotations specific to the satellite.
translate(satellitePos.x, satellitePos.y, satellitePos.z);
Moves the drawing origin to the satellite's current 3D position, so all child shapes are drawn relative to it.
specularMaterial(150, 150, 150); // Gray metallic for satellite body
Applies a gray shiny material that reflects light, making the satellite look metallic and respond to the directional light in the scene.
box(SATELLITE_RADIUS * 0.8, SATELLITE_RADIUS * 1.5, SATELLITE_RADIUS * 0.8);
Draws the main body of the satellite as a box—scaled proportionally to SATELLITE_RADIUS (which is 5), creating a box of size 4×7.5×4.
push();
Saves the transformation state again before drawing the first solar panel, so the panel's translation doesn't affect other components.
fill(50, 50, 100); // Dark blue for solar panels
Sets the color to a dark blue (RGB: 50,50,100), making the solar panels visually distinct from the gray body.
translate(SATELLITE_RADIUS * 0.8, 0, 0);
Moves the drawing origin to the right by 4 units (5 × 0.8), positioning the first solar panel to extend from the right side of the main body.
box(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 2, SATELLITE_RADIUS * 1.5);
Draws a thin rectangular panel (0.5×10×7.5) to look like a solar array extending outward.
translate(-SATELLITE_RADIUS * 0.8, 0, 0);
Moves to the left side of the body by translating -4 units in X, creating a symmetric second solar panel.
fill(200, 150, 0); // Gold/brass color for antenna
Changes the fill color to a golden brass (RGB: 200,150,0) to make the antenna stand out as a distinct component.
translate(0, SATELLITE_RADIUS * 0.75, 0);
Moves the drawing position upward by 3.75 units (from the satellite's center) so the antenna extends from the top of the body.
cylinder(SATELLITE_RADIUS * 0.1, SATELLITE_RADIUS * 0.5);
Draws a small cylinder with radius 0.5 and height 2.5, representing a transmitter antenna sticking up from the satellite.
pop();
Restores the transformation state, so the antenna's translation doesn't affect the code that follows.

windowResized()

windowResized() is a p5.js function that gets called automatically whenever the user resizes the browser window. Without it, the canvas would maintain its original size and not fill the new window dimensions. The resizeCanvas() function updates the width and height to match windowWidth and windowHeight.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Automatically adjusts the canvas size whenever the browser window is resized, ensuring the sketch always fills the viewport.

📦 Key Variables

params object

Stores all tunable physics parameters (gravitational constant, masses, initial position/velocity) and connects them to the dat.gui control panel

const params = { gConstant: 0.005, earthMass: 1000, ... };
EARTH_RADIUS number

Visual radius of the Earth sphere in pixels; defines the boundary between Earth and space

const EARTH_RADIUS = 30;
SATELLITE_RADIUS number

Reference size for drawing all satellite components; scales the body, panels, and antenna proportionally

const SATELLITE_RADIUS = 5;
ORBIT_TRAIL_LENGTH number

Maximum number of past positions stored in the orbitPath array; controls how long the glowing orange trail appears

const ORBIT_TRAIL_LENGTH = 200;
NUM_STARS number

Total count of stars generated in the background starfield

const NUM_STARS = 500;
satellitePos p5.Vector

3D position vector of the satellite—updated each frame by adding velocity to simulate orbital motion

let satellitePos;
satelliteVel p5.Vector

3D velocity vector of the satellite—updated each frame by adding gravitational acceleration

let satelliteVel;
orbitPath array

Array of p5.Vector positions representing the satellite's past locations; used to draw the glowing orange trail

let orbitPath = [];
earthTexture image

Loaded image of Earth's surface that gets mapped onto the 3D sphere to create a realistic planet

let earthTexture;
webglFont font

Font file used for rendering text in WEBGL mode; must be loaded in preload()

let webglFont;
stars array

Array of p5.Vector objects representing random 3D positions for stars in the background starfield

let stars = [];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - star rendering loop

Each frame, all 500 stars are translated and spheres are drawn. For large NUM_STARS (>1000), this becomes expensive.

💡 Pre-generate a graphics buffer of the starfield once in setup() using createGraphics(). Then display only the texture in draw(), avoiding expensive recomputation each frame.

BUG calculateGravitationalForce()

When distance approaches zero (satellite crashes into Earth), the force becomes infinite. No maximum clamp is applied, potentially causing velocity to explode and the satellite to teleport.

💡 Add a minimum distance threshold: `const safeDist = max(distance, EARTH_RADIUS + SATELLITE_RADIUS);` and use safeDist in the force calculation instead of raw distance.

STYLE preload()

The Earth texture URL is hardcoded. If the CDN goes down or the URL changes, the sketch breaks silently with a blank white sphere.

💡 Add error handling: `earthTexture = loadImage(..., () => {}, () => console.error('Failed to load Earth texture'));` to log failures. Consider hosting the texture locally in the project folder.

FEATURE draw()

The estimated orbital period assumes circular motion but doesn't account for elliptical orbits, making the estimate very inaccurate for non-circular paths.

💡 Use Kepler's third law more rigorously: period = 2π√(a³/GM), where a is the semi-major axis. For now, accept the estimate as approximate and label it clearly.

BUG draw() - information display

If satellite speed is exactly 0, orbitalPeriodEstimate becomes Infinity and displays as 'Infinity' on screen instead of 'N/A'.

💡 Check for zero speed before calculation: `if (satelliteSpeed > 0.001)` rather than `if (satelliteSpeed > 0)` to account for floating-point precision.

🔄 Code Flow

Code flow showing preload, setup, resetsimulation, draw, calculategravitationalforce, drawdetailedsatellite, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> preload[preload] preload --> load-earth-texture[load-earth-texture] preload --> load-webgl-font[load-webgl-font] setup --> create-webgl-canvas[create-webgl-canvas] setup --> gui-creation[gui-creation] setup --> draw[draw loop] click preload href "#fn-preload" click setup href "#fn-setup" click load-earth-texture href "#sub-load-earth-texture" click load-webgl-font href "#sub-load-webgl-font" click create-webgl-canvas href "#sub-create-webgl-canvas" click gui-creation href "#sub-gui-creation" draw --> star-generation-loop[star-generation-loop] star-generation-loop -->|loops 500 times| star-rendering-loop[star-rendering-loop] star-rendering-loop --> earth-rendering[earth-rendering] earth-rendering --> gravity-calculation[gravity-calculation] gravity-calculation --> direction-calculation[direction-calculation] direction-calculation --> zero-distance-check[zero-distance-check] zero-distance-check -->|if not zero| force-magnitude-calculation[force-magnitude-calculation] force-magnitude-calculation --> force-vector-scaling[force-vector-scaling] force-vector-scaling --> physics-integration[physics-integration] physics-integration --> trail-management-loop[trail-management-loop] trail-management-loop --> text-display[text-display] click draw href "#fn-draw" click star-generation-loop href "#sub-star-generation-loop" click star-rendering-loop href "#sub-star-rendering-loop" click earth-rendering href "#sub-earth-rendering" click gravity-calculation href "#sub-gravity-calculation" click direction-calculation href "#sub-direction-calculation" click zero-distance-check href "#sub-zero-distance-check" click force-magnitude-calculation href "#sub-force-magnitude-calculation" click force-vector-scaling href "#sub-force-vector-scaling" click physics-integration href "#sub-physics-integration" click trail-management-loop href "#sub-trail-management-loop" click text-display href "#sub-text-display"

❓ Frequently Asked Questions

What visual effects does the p5.js sketch 'theme of the day but its super good' produce?

The sketch visually creates a rotating 3D Earth surrounded by a glowing starfield, with a satellite orbiting and leaving a smooth trail behind it.

How can users interact with the sketch to explore the satellite's orbit?

Users can drag to rotate the scene and adjust gravity and satellite settings via on-screen controls to observe real-time changes in the satellite's orbit.

What creative coding concepts are showcased in this p5.js sketch?

This sketch demonstrates concepts of 3D rendering, orbital mechanics, and interactive simulations using p5.js.

Preview

theme of the day but its super good - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of theme of the day but its super good - Code flow showing preload, setup, resetsimulation, draw, calculategravitationalforce, drawdetailedsatellite, windowresized
Code Flow Diagram