Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
              
            
!

CSS

              
                * {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

canvas {
  display: block;
  background-color: #000000;
}
              
            
!

JS

              
                /*
 * You can verify this simulation with this NASA website:
 * http://space.jpl.nasa.gov/
 * The keplerian data is from https://ssd.jpl.nasa.gov/txt/p_elem_t2.txt
 */

const au = 149597870.7;

/*
 * KEPLERIAN ELEMENTS:
 *
 * Semimajor axis (a),
 * orbit eccentricity (e),
 * orbital inclination (I),
 * mean longitude (L),
 * longitude of perihelion (lp),
 * longitude of ascending node (ln)
 */
const data = {
  mercury: {
    name: "Mercury",
    color: "Chocolate",
    radius: 2439.7,
    keplerianElements: {
      semi_major_axis: 0.38709843,
      eccentricity: 0.20563661,
      orbital_inclination: 7.00559432,
      longitude_mean: 252.25166724,
      longitude_perihelion: 77.45771895,
      longitude_ascending_node: 48.33961819,
      rates: {
        semi_major_axis: 0,
        eccentricity: 0.00002123,
        orbital_inclination: -0.00590158,
        longitude_mean: 149472.67486623,
        longitude_perihelion: 0.15940013,
        longitude_ascending_node: -0.12214182,
      },
    },
  },
  venus: {
    name: "Venus",
    color: "BurlyWood",
    radius: 6051.8,
    keplerianElements: {
      semi_major_axis: 0.72332102,
      eccentricity: 0.00676399,
      orbital_inclination: 3.39777545,
      longitude_mean: 181.9797085,
      longitude_perihelion: 131.76755713,
      longitude_ascending_node: 76.67261496,
      rates: {
        semi_major_axis: -0.00000026,
        eccentricity: -0.00005107,
        orbital_inclination: 0.00043494,
        longitude_mean: 58517.8156026,
        longitude_perihelion: 0.05679648,
        longitude_ascending_node: -0.27274174,
      },
    },
  },
  earthMoon: {
    name: "Earth + moon",
    color: "SteelBlue",
    radius: 6371,
    keplerianElements: {
      semi_major_axis: 1.00000018,
      eccentricity: 0.01673163,
      orbital_inclination: -0.00054346,
      longitude_mean: 100.46691572,
      longitude_perihelion: 102.93005885,
      longitude_ascending_node: -5.11260389,
      rates: {
        semi_major_axis: -0.00000003,
        eccentricity: -0.00003661,
        orbital_inclination: -0.01337178,
        longitude_mean: 35999.37306329,
        longitude_perihelion: 0.3179526,
        longitude_ascending_node: -0.24123856,
      },
    },
  },
  mars: {
    name: "Mars",
    color: "red",
    radius: 3389.5,
    keplerianElements: {
      semi_major_axis: 1.52371243,
      eccentricity: 0.09336511,
      orbital_inclination: 1.85181869,
      longitude_mean: -4.56813164,
      longitude_perihelion: -23.91744784,
      longitude_ascending_node: 49.71320984,
      rates: {
        semi_major_axis: 0.00000097,
        eccentricity: 0.00009149,
        orbital_inclination: -0.00724757,
        longitude_mean: 19140.29934243,
        longitude_perihelion: 0.45223625,
        longitude_ascending_node: -0.26852431,
      },
    },
  },
  jupiter: {
    name: "Jupiter",
    color: "Peru",
    radius: 69911,
    keplerianElements: {
      semi_major_axis: 5.20248019,
      eccentricity: 0.0485359,
      orbital_inclination: 1.29861416,
      longitude_mean: 34.33479152,
      longitude_perihelion: 14.27495244,
      longitude_ascending_node: 100.29282654,
      rates: {
        semi_major_axis: -0.00002864,
        eccentricity: 0.00018026,
        orbital_inclination: -0.00322699,
        longitude_mean: 3034.90371757,
        longitude_perihelion: 0.18199196,
        longitude_ascending_node: 0.13024619,
      },
    },
  },
  saturn: {
    name: "Saturn",
    color: "AliceBlue",
    radius: 58232,
    keplerianElements: {
      semi_major_axis: 9.54149883,
      eccentricity: 0.05550825,
      orbital_inclination: 2.49424102,
      longitude_mean: 50.07571329,
      longitude_perihelion: 92.86136063,
      longitude_ascending_node: 113.63998702,
      rates: {
        semi_major_axis: -0.00003065,
        eccentricity: -0.00032044,
        orbital_inclination: 0.00451969,
        longitude_mean: 1222.11494724,
        longitude_perihelion: 0.54179478,
        longitude_ascending_node: -0.25015002,
      },
    },
  },
  uranus: {
    name: "Uranus",
    color: "LightSkyBlue",
    radius: 25362,
    keplerianElements: {
      semi_major_axis: 19.18797948,
      eccentricity: 0.0468574,
      orbital_inclination: 0.77298127,
      longitude_mean: 314.20276625,
      longitude_perihelion: 172.43404441,
      longitude_ascending_node: 73.96250215,
      rates: {
        semi_major_axis: -0.00020455,
        eccentricity: -0.0000155,
        orbital_inclination: -0.00180155,
        longitude_mean: 428.49512595,
        longitude_perihelion: 0.09266985,
        longitude_ascending_node: 0.05739699,
      },
    },
  },
  neptune: {
    name: "Neptune",
    color: "MidnightBlue",
    radius: 24622,
    keplerianElements: {
      semi_major_axis: 30.06952752,
      eccentricity: 0.00895439,
      orbital_inclination: 1.7700552,
      longitude_mean: 304.22289287,
      longitude_perihelion: 46.68158724,
      longitude_ascending_node: 131.78635853,
      rates: {
        semi_major_axis: 0.00006447,
        eccentricity: 0.00000818,
        orbital_inclination: 0.000224,
        longitude_mean: 218.46515314,
        longitude_perihelion: 0.01009938,
        longitude_ascending_node: -0.00606302,
      },
    },
  },
  pluto: {
    name: "Pluto",
    color: "pink",
    radius: 1188,
    keplerianElements: {
      semi_major_axis: 39.48686035,
      eccentricity: 0.24885238,
      orbital_inclination: 17.1410426,
      longitude_mean: 238.96535011,
      longitude_perihelion: 224.09702598,
      longitude_ascending_node: 110.30167986, // rotation
      rates: {
        semi_major_axis: 0.00449751,
        eccentricity: 0.00006016,
        orbital_inclination: 0.00000501,
        longitude_mean: 145.18042903,
        longitude_perihelion: -0.00968827,
        longitude_ascending_node: -0.00809981,
      },
    },
  },
};

function degreesToRadians(deg) {
  return deg * (Math.PI / 180);
}

function radiansToDegrees(rad) {
  return rad / (Math.PI / 180);
}

/**
 *
 * Used to solve for E
 * Credits: Juergen Giesen, http://www.jgiesen.de/kepler/kepler.html
 *
 * @param {*} e eccentricity
 * @param {*} m  mean anomaly
 * @param {*} dp number of decimal places
 */
function eccentricAnomaly(e, m, dp) {
  const maxIter = 30;
  const delta = Math.pow(10, -dp);

  let E = 0;
  let F = 0;

  m = m / 360;
  m = 2 * Math.PI * (m - Math.floor(m));

  if (e < 0.8) {
    E = m;
  } else {
    E = Math.PI;
  }

  F = E - e * Math.sin(m) - m;

  let i = 0;
  while (Math.abs(F) > delta && i < maxIter) {
    E = E - F / (1 - e * Math.cos(E));
    F = E - e * Math.sin(E) - m;
    i = i + 1;
  }

  E = radiansToDegrees(E);

  return Math.round(E * Math.pow(10, dp)) / Math.pow(10, dp);
}

function calculateFoci(e, t) {
  const n = Math.pow(e, 2) - Math.pow(t, 2);
  return Math.sqrt(n);
}

class FPS {
  constructor() {
    this.now = Date.now();
    this.then = Date.now();
    this.avgCount = 0;
    this.count = 0;
    this.delta = 0;
  }
}

class Body {
  constructor(opts) {
    this.name = opts.name;
    this.color = opts.color;
    this.radius = opts.radius;
    this.keplerianElements = opts.keplerianElements;

    this.x = undefined;
    this.y = undefined;
    this.z = undefined;

    this.orbitalElements = {
      semi_major_axis: undefined,
      eccentricity: undefined,
      inclination: undefined,
      longitude_ascending_node: undefined,
      longitude_perihelion: undefined,
      longitude_mean: undefined,
      anomaly_mean: undefined,
      anomaly_eccentric: undefined,
      anomaly_true_argument: undefined,
      anomaly_true: undefined,
      radius_vector: undefined,
    };
  }

  plot(t) {
    const kE = this.keplerianElements;

    // semi_major_axis
    let D = kE.semi_major_axis + kE.rates.semi_major_axis * t;

    // eccentricity
    let e = kE.eccentricity + kE.rates.eccentricity * t;

    // inclination
    let i = kE.orbital_inclination + kE.rates.orbital_inclination * t;
    i = i % 360;

    i = 0;

    // longitude of ascending node
    let long_node =
      kE.longitude_ascending_node + kE.rates.longitude_ascending_node * t;
    long_node = long_node % 360;

    // longitude of perihelion
    let long_peri = kE.longitude_perihelion + kE.rates.longitude_perihelion * t;
    long_peri = long_peri % 360;
    if (long_peri < 0) {
      long_peri = 360 + long_peri;
    }

    // mean longitude
    let L = kE.longitude_mean + kE.rates.longitude_mean * t;
    L = L % 360;
    if (L < 0) {
      L = 360 + L;
    }

    // mean anomaly used this to determine perihelion
    let M = L - long_peri;
    if (M < 0) {
      M = 360 + M;
    }

    // eccentric anomaly
    let E = eccentricAnomaly(e, M, 6);

    // argument of true anomaly
    let u = Math.sqrt((1 + e) / (1 - e)) * Math.tan(degreesToRadians(E) / 2);

    // true anomaly
    let v = 0;
    if (u < 0) {
      v = 2 * (radiansToDegrees(Math.atan(u)) + 180);
    } else {
      v = 2 * radiansToDegrees(Math.atan(u));
    }

    // radius vector
    let rv = D * (1 - e * Math.cos(degreesToRadians(E)));

    const x =
      rv *
      (Math.cos(degreesToRadians(long_node)) *
        Math.cos(degreesToRadians(v + long_peri - long_node)) -
        Math.sin(degreesToRadians(long_node)) *
          Math.sin(degreesToRadians(v + long_peri - long_node)) *
          Math.cos(degreesToRadians(i)));
    const y =
      rv *
      (Math.sin(degreesToRadians(long_node)) *
        Math.cos(degreesToRadians(v + long_peri - long_node)) +
        Math.cos(degreesToRadians(long_node)) *
          Math.sin(degreesToRadians(v + long_peri - long_node)) *
          Math.cos(degreesToRadians(i)));
    const z =
      rv *
      (Math.sin(degreesToRadians(v + long_peri - long_node)) *
        Math.sin(degreesToRadians(i)));

    this.orbitalElements = {
      semi_major_axis: D,
      eccentricity: e,
      orbital_inclination: i,
      longitude_ascending_node: long_node,
      longitude_perihelion: long_peri,
      longitude_mean: L,
      anomaly_mean: M,
      anomaly_eccentric: E,
      anomaly_true_argument: u,
      anomaly_true: v,
      radius_vector: rv,
    };

    // heliocentric ecliptic coordinates
    this.x = x;
    this.y = y;
    this.z = z;
  }
}

class View {
  constructor(x, y, width, height) {
    this.x = x - width / 2;
    this.y = y - height / 2;
    this.originalWidth = width;
    this.originalHeight = height;
    this.width = width;
    this.height = height;

    this.zoomOffsetX = 0;
    this.zoomOffsetY = 0;

    // Scales
    this.scaleFactor = 14959787.07;
    this.scale = 1;
    // au = 14959787.07

    this.keysPressed = [];
    this.keyCodeArrowLeft = 37;
    this.keyCodeArrowUp = 38;
    this.keyCodeArrowRight = 39;
    this.keyCodeArrowDown = 40;

    this.movementSpeedX = 0;
    this.movementSpeedY = 0;
    this.movementSpeedFactorX = 0;
    this.movementSpeedFactorY = 0;
    this.maxMovementSpeed = au;

    window.addEventListener("wheel", this.zoom.bind(this));
    window.addEventListener("keydown", this.keydown.bind(this));
    window.addEventListener("keyup", this.keyup.bind(this));
  }

  resize(width, height) {
    this.originalWidth = width;
    this.originalHeight = height;
    this.width = width;
    this.height = height;
  }

  zoom(e) {
    let step = undefined;
    if (e.wheelDelta) {
      step = -e.wheelDelta / 100;
    }

    if (e.deltaY) {
      step = -e.deltaY / 100;
    }

    if (step === undefined) {
      return;
    }

    const oldScale = this.scale;

    this.scale *= Math.exp(step * 0.2);
    if (this.scale < 0.0001) {
      this.scale = 0.0001;
    }

    const sw = this.originalWidth / oldScale;
    const sx = this.x / oldScale;
    const newTargetX = sx + sw * (e.clientX / this.originalWidth);

    const sh = this.originalHeight / oldScale;
    const sy = this.y / oldScale;
    const newTargetY = sy + sh * (e.clientY / this.originalHeight);

    this.width = this.originalWidth * this.scale;
    this.height = this.originalHeight * this.scale;

    const scalechange = this.scale - oldScale;
    this.x += newTargetX * scalechange;
    this.y += newTargetY * scalechange;
  }

  keydown(e) {
    if (!this.keysPressed.includes(e.keyCode)) {
      this.keysPressed.push(e.keyCode);
    }
  }

  keyup(e) {
    const index = this.keysPressed.indexOf(e.keyCode);
    if (index >= 0) {
      this.keysPressed.splice(index, 1);
    }
  }

  update() {
    const left = this.keysPressed.includes(this.keyCodeArrowLeft);
    const right = this.keysPressed.includes(this.keyCodeArrowRight);
    const up = this.keysPressed.includes(this.keyCodeArrowUp);
    const down = this.keysPressed.includes(this.keyCodeArrowDown);

    if (left) {
      this.movementSpeedX -= 1;
    }
    if (right) {
      this.movementSpeedX += 1;
    }
    if ((left && right) || (!left && !right)) {
      if (this.movementSpeedX !== 0) {
        this.movementSpeedX -= Math.sign(this.movementSpeedX);
      }
    }

    if (up) {
      this.movementSpeedY -= 1;
    }
    if (down) {
      this.movementSpeedY += 1;
    }
    if ((up && down) || (!up && !down)) {
      if (this.movementSpeedY !== 0) {
        this.movementSpeedY -= Math.sign(this.movementSpeedY);
      }
    }

    this.x += this.movementSpeedX / 100;
    this.y += this.movementSpeedY / 100;
  }
}

class System {
  constructor(sim) {
    this.sim = sim;
    this.view = this.sim.view;

    this.data = data;
    this.bodies = {};

    this.t = 0;
  }

  init() {
    for (let val in this.data) {
      this.bodies[val] = new Body(this.data[val]);
    }
  }

  update(t) {
    this.t = t;
    for (const [key, body] of Object.entries(this.bodies)) {
      body.plot(this.t);
    }
  }

  render() {
    const ctx = this.sim.ctx;

    const centerX = this.view.width / 2 - this.view.x;
    const centerY = this.view.height / 2 - this.view.y;

    let sunRadius = (1391400 / au) * this.view.scale;
    if (sunRadius < 0.5) {
      sunRadius = 0.5;
    }

    // Sun
    ctx.save();
    ctx.beginPath();
    ctx.fillStyle = "yellow";
    ctx.arc(centerX, centerY, sunRadius, 0, 2 * Math.PI, true);
    ctx.fill();
    ctx.closePath();
    ctx.restore();

    for (const [key, body] of Object.entries(this.bodies)) {
      if (body.x === undefined || body.y === undefined) {
        continue;
      }

      // Draw body
      this.drawBody(body);

      // Draw orbit
      this.drawOrbit(body);
    }
  }

  drawBody(body) {
    const ctx = this.sim.ctx;

    const centerX = this.view.width / 2 - this.view.x;
    const centerY = this.view.height / 2 - this.view.y;

    const x = centerX + body.x * this.view.scale;
    const y = centerY - body.y * this.view.scale;
    // const z = centerY + body.z * this.view.scale;

    let r = (body.radius / au) * this.view.scale;
    if (r < 0.5) {
      r = 0.5;
    }

    ctx.save();

    ctx.fillStyle = body.color;

    ctx.beginPath();
    ctx.arc(x, y, r, 0, 2 * Math.PI, true);
    ctx.fill();
    ctx.closePath();

    // ctx.beginPath();
    // ctx.arc(x, z, r, 0, 2 * Math.PI, true);
    // ctx.fill();
    // ctx.closePath();

    ctx.fillStyle = body.color;
    ctx.font = "10px Arial";
    ctx.textAlign = "center";
    ctx.fillText(body.name, x, y - r - 7);
    // ctx.fillText(body.name, x, z - r - 7);

    ctx.restore();
  }

  drawOrbit(body) {
    const ctx = this.sim.ctx;

    const centerX = this.view.width / 2 - this.view.x;
    const centerY = this.view.height / 2 - this.view.y;

    const r = body.orbitalElements.semi_major_axis;
    const i = body.orbitalElements.eccentricity;
    const o = -body.orbitalElements.longitude_perihelion;
    
    // const ln = body.orbitalElements.longitude_ascending_node;
    // const I = body.orbitalElements.orbital_inclination;

    const S = Math.sqrt(1 - Math.pow(i, 2)) * r; // semi minor axis
    const A = calculateFoci(r, S); // Foci point

    const rotation = degreesToRadians(o);
    const x = centerX + -A * Math.cos(rotation) * this.view.scale;
    const y = centerY + -A * Math.sin(rotation) * this.view.scale;
    const radiusX = r * this.view.scale;
    const radiusY = S * this.view.scale;

    ctx.save();

    ctx.strokeStyle = "rgba(255, 255, 255, .3)";

    ctx.beginPath();
    ctx.ellipse(x, y, radiusX, radiusY, rotation, 0, 2 * Math.PI, true);
    ctx.stroke();
    ctx.closePath();

    ctx.restore();
  }
}

class Simulation {
  constructor() {
    this.ready = false;

    this.el = document.createElement("canvas");
    this.ctx = this.el.getContext("2d");

    this.width = 0;
    this.height = 0;
    this.view = new View(
      -(this.width / 2),
      -(this.height / 2),
      this.width,
      this.height
    );

    // Frame rate variables
    this.fps = 30;
    this.now = undefined;
    this.then = Date.now();
    this.interval = 1000 / this.fps;
    this.delta = undefined;
    this.TPS = new FPS();
    this.FPS = new FPS();

    this.julianCenturyInJulianDays = 36525;
    this.julianEpochJ2000 = 2451545;

    // Get Gregorian Date
    // this.current = new Date(); // Set as today
    // this.speed = 0; // paused
    // this.speed = 1 * 60 * 60 * 1000; // 1 hour per second
    this.speed = 24 * 1 * 60 * 60 * 1000; // 1 day per second
    this.current = new Date(Date.UTC(2000, 0, 2, 0, 0, 0)); // Set at Epoch (J2000)
    this.hours = this.current.getHours();
    this.day = this.current.getDate();
    this.month = this.current.getMonth() + 1; // January has index 0
    this.year = this.current.getFullYear();

    // Get Julian Date
    this.julianDate = this.getJulianDate(
      this.year,
      this.month,
      this.day,
      this.hours
    );

    // Get Julian Centuries since Epoch (J2000)
    this.t =
      (this.julianDate - this.julianEpochJ2000) /
      this.julianCenturyInJulianDays;

    this.newDate = undefined;

    this.system = new System(this);
  }

  start() {
    document.body.prepend(this.el);
    this.resize();

    // Initialise system
    this.system.init();

    window.addEventListener("resize", this.resize.bind(this));

    this.ready = true;

    this.update();
  }

  update() {
    this.view.update();

    this.now = Date.now();

    this.delta = this.now - this.then;

    // Only run this code based on desired frames per second
    if (this.delta > this.interval) {
      this.then = this.now - (this.delta % this.interval);

      //Keep track of FPS
      this.FPS.count++;
      this.FPS.now = Date.now();
      this.FPS.delta = this.FPS.now - this.FPS.then;
      if (this.FPS.delta > 1000) {
        //Update frame rate every second
        this.FPS.avgCount = this.FPS.count;
        this.FPS.count = 0;
        this.FPS.then = this.FPS.now - (this.FPS.delta % 1000);
      }

      // Increase the date by 1 hour each frame
      this.updateDate(this.speed);

      // Update system
      this.system.update(this.t);
    }

    this.TPS.count++;
    this.TPS.now = Date.now();
    this.TPS.delta = this.TPS.now - this.TPS.then;
    if (this.TPS.delta > 1000) {
      //Update frame rate every second
      this.TPS.avgCount = this.TPS.count;
      this.TPS.count = 0;
      this.TPS.then = this.TPS.now - (this.TPS.delta % 1000);
    }

    window.requestAnimationFrame(this.render.bind(this));

    setTimeout(this.update.bind(this), 0);
  }

  render() {
    this.ctx.clearRect(0, 0, this.width, this.height);

    this.ctx.save();
    this.ctx.fillStyle = "#090909";
    this.ctx.fillRect(0, 0, this.width, this.height);
    this.ctx.restore();

    // Render system
    this.system.render();
  }

  resize() {
    this.width = window.innerWidth;
    this.height = window.innerHeight;

    this.view.resize(this.width, this.height);

    this.el.style.width = `${this.width}px`;
    this.el.style.height = `${this.height}px`;

    const ratio = window.devicePixelRatio;
    this.el.width = this.width * ratio;
    this.el.height = this.height * ratio;

    this.ctx.scale(ratio, ratio);

    if (this.ready) {
      this.render();
    }
  }

  getJulianDate(year, month, day, hours) {
    var inputDate = new Date(year, month, Math.floor(day), hours);
    var switchDate = new Date(1582, 10, 15, 0);

    var isGregorianDate = inputDate >= switchDate;

    // Adjust if B.C.
    if (year < 0) {
      year++;
    }

    // Adjust if JAN or FEB
    if (month == 1 || month == 2) {
      year = year - 1;
      month = month + 12;
    }

    // Calculate A & B; ONLY if date is equal or after 1582-Oct-15
    var A = Math.floor(year / 100); // A
    var B = 2 - A + Math.floor(A / 4); // B

    // Ignore B if date is before 1582-Oct-15
    if (!isGregorianDate) {
      B = 0;
    }

    const years = Math.floor(365.25 * year);
    const monthDays = Math.floor(30.6001 * (month + 1));

    return years + monthDays + day + hours / 24 + 1720994.5 + B;
  }

  updateDate(increment) {
    this.newDate = this.current;
    this.newDate.setTime(this.newDate.getTime() + increment);
    this.current = this.newDate;

    const newHour = this.newDate.getHours();
    const newDay = this.newDate.getDate();
    const newMonth = this.newDate.getMonth() + 1; // January has index 0
    const newYear = this.newDate.getFullYear();

    this.year = newYear;
    this.month = newMonth;
    this.day = newDay;
    this.hours = newHour;

    this.julianDate = this.getJulianDate(
      this.year,
      this.month,
      this.day,
      this.hours
    );

    this.t =
      (this.julianDate - this.julianEpochJ2000) /
      this.julianCenturyInJulianDays;
  }
}

window.addEventListener("load", function () {
  const simulation = new Simulation();
  simulation.start();
});

              
            
!
999px

Console