Rendering a Planetary System Diagram with PaperJS

This article contains a tutorial about how to create and render a simple animated planetary system view using PaperJS library


Pre-Requisites

  • Beginner level in Javascript and HTML5 canvas. An introduction to PaperJS would also be helpful.

  • Basic understanding of what is a planetary system (e.g. Solar system) and of orbital characteristics of a planet.


What We Will Create

We will develop a simplified version of an animated diagram like this:

Kepler-62 system diagram


Project Structure

It is very simple.

We'll have just one .html file and one .js file. We'll need the PaperJS lib, so our javascript files will be placed in a dedicated folder:


index.html
scripts
├── paper-full.js
└── planets.js

To follow the turorial, please create this files hierarchy and put the downloaded paper-full.js under scripts.


HTML Page Template

The index.html page template for this tutorial contains almost nothing:

CTYPE html>
l lang="en">
ead>
<meta charset="UTF-8">
<title>Planetary System with PaperJS</title>
<script src='scripts/paper-full.js' lang='javascript'></script> <!-- 1 -->
<script src='scripts/planets.js' lang='javascript' canvas="planetary_system_view"></script> <!-- 2 -->
<style> <!-- 3 -->
  body {
    background-color: grey;
  }
  canvas {
    background-color: #02002c;
    width: 450px;
    height: 450px;
  }
</style>
head>
ody>
<canvas id="planetary_system_view"/> <!-- 4 -->
body>
ml>

Notes:

  1. We declare the paperjs lib.

  2. We declare our javascript file, which we will fill below during this tutorial. Please note that this declaration contains a canvas attribute, which is required by PaperJS.

  3. A bit of self-explaining CSS styling in order to be in line with the style of the site.

  4. The canvas element where the things will be rendered.

PaperJS supports its own scripting dialect called paperscript, but also allows to code using traditional Javascript. We'll use the latter.

Once created with the contents above, the index.html file will not need to be changed any more.


planets.js Script

It contains all the javascript code necessary for rendering the planetary system animation.


Structure and Algorithm

In the code, we will try to be clean and to separate the responsibilities of different concepts, which will allow to make the application very flexible.

We will need the following elements:

  • Data structures describing the planetary systems: star and planets with their properties (sizes, orbits…). This usually corresponds to the data model part of an application and the respective values are stored somewhere in a database. For our simple example we'll just hardcode them.

  • An entity that contains the items drawn in the diagram, with their graphical properties.

  • An entity that will manage the contents of the view and define the animation to produce.

In brief, the algorithm will be as follows.

  1. Collect the data about the planetary system to draw

  2. Initialize the graphical elements drawn in the diagram

  3. Launch a cycling animation


The Data Model

A star has a rudimentary representation in our approach. It's enough for us to know its name and radius:

tion Star(name, radius) {
is.name = name;
is.shape = new Object();
is.shape.radius = radius;

A planet has some more characteristics. We need to know its name and size, but also the properties of its orbit: the semi-major axis, the eccentricity and the period. So the data structure will be longer:

tion Planet(name, sMajAx, eccentr, period, size) {
is.name = name;
is.shape = new Object();
is.shape.size = size;
is.orbit = new Object();
is.orbit.eccentricity = eccentr;
is.orbit.semiMajorAxis = sMajAx;
is.orbit.semiMinorAxis = calculateSemiMinorAxis(sMajAx, eccentr);
is.orbit.period = period;

The semi-minor axis is also necessary for rendering, but it can be calculated easily when the semi-major axis and the eccentricity are known, so we create a dedicated function:

tion calculateSemiMinorAxis(sMajAx, eccentricity) {
r eccSqrt = Math.sqrt(1 - eccentricity * eccentricity);
turn sMajAx * eccSqrt;

Now, we can initialize our model data when the HTML page is loaded:

he orbital characteristics of planets in this example are chosen almost randomly
ow.onload = function() {
per.install(window);
r planets = new Array();
anets.push(new Planet("B", 60, -0.3, 360, 4));
anets.push(new Planet("C", 120, 0.15, 480, 8));
anets.push(new Planet("D", 185, 0.02, 2200, 2));
r star = new Star("S", 16);
r canvas = document.getElementById("planetary_system_view");
 a few more lines will be added here later
per.view.update();

This function will be executed when the HTML page loads into the browser.

At this moment, nothing is drawn yet in our diagram.


The View

We need an entity that collects data about graphical properties of all rendered items.

The star will be drawn in the center of the view and all the orbits of the planets will be calculated relatively to the center.

So, let's call this object PlanetarySystemView and its stub is as follows:

tion PlanetarySystemView(canvas) {

r center = new paper.Point(canvas.width / 2, canvas.height / 2);

r star = null;
r orbits = new Array();
r planets = new Array();

 TODO


Each of the fields of this object will be initialized through a dedicated function. The shortest initialization is made for the star field:

.initStar = function(starModel) {
ar = new paper.Shape.Circle(center, starModel.shape.radius); // 1
ar.fillColor = '#FCFF05'; // 2

Notes:

  1. A circle is created in PaperJS context, placed in the center of the view and having the radius of the star model, in pixels.

  2. A color is assigned to the circle representing the star, thus making it visible.

Now, in order to test if the star is drawn properly, we can replace the comment line inside window.onload function with

r.setup(canvas);
PlanetarySystemView(canvas).initStar(star);

And our lonely star will start to shine:

Star rendered

For each existing planet, we need to define an orbit, that is why the orbits field is an array. An orbit will be defined in a dedicated function:

.initOrbit = function(planetModel) {
r orbit = new Object();
bit.maxRadius = planetModel.orbit.semiMajorAxis; // 1
bit.minRadius = planetModel.orbit.semiMinorAxis; // 1
bit.center = new paper.Point(view.center.x,
                             view.center.y - orbit.maxRadius * planetModel.orbit.eccentricity);  // 2
bit.outline = new paper.Shape.Ellipse({  // 3
                              center: orbit.center,
                              size: [orbit.minRadius * 2, orbit.maxRadius * 2],  // 4
                              dashArray: [1, 5],
                              strokeWidth: 0.5
                            });
bit.outline.strokeColor = '#FFFFFF'
bits.push(orbit); // 5

Notes:

  1. We pass to our view orbit object the values of semi- major and minor axes of the orbit, in order to have them accessible directly, without keeping further the reference to the model. It is suitable here because we do not intend to change the properties of the models dynamically.

  2. The semi-major axis of the orbit will be parallel to the Y axis of the diagram, so we calculate the eccentricity distance offset in pixels and set the center Y coordinate respectively. The semi-major axis will be directed to the top of the diagram if the eccentricity is positive.

  3. We create the outline of the orbit as a PaperJS Ellipse object.

  4. The size of the ellipse equals the size of its bounding rectangle, which corresponds to the major and minor diameters.

  5. The orbit object containing the rendered shape is pushed into the array of orbits known to the view.

Now let's replace the comment line inside the initial window.onload function with

r.setup(canvas);
view = new PlanetarySystemView(canvas);
.initStar(star);
(var i = 0; i < planets.length; i++) {
ew.initOrbit(planets[i]);

The trajectories of the orbits should become visible, like this:

Star and orbits rendered

There are now only the planets left to initialize.

Each planet will be represented visually with a painted circle, placed in a point on the orbital trajectory. The initial position will be chosen randomly.

Since the trajectory of one full orbital revolution is an ellipse, or even a circle, the whole orbital path may be counted as a 360-degree rotation around the centre of the system, starting at a reference point on the orbit. This assumption gives us a possibility to simplify the representation of the current planet's position on the orbit with just a single number, showing the angle of the path already covered in a single revolution.

Javascript maths functions like to operate with radians instead of degrees, so we'll need to make a bit of angular conversions. To facilitate it, let's introduce a constant correponding to 360 degrees measured in radians:

DOUBLE_PI = 2 * Math.PI;

A planet view can be initialized with the following function:

initPlanet = function(orbitView, planetModel) {
r planet = new Object();
anet.orbit = orbitView; // 1
anet.position = DOUBLE_PI * Math.random(); // 2
anet.step = DOUBLE_PI / planetModel.orbit.period; // 3
anet.shape = new paper.Shape.Circle(new paper.Point(-100, -100), planetModel.shape.size); // 4
anet.shape.fillColor = '#AAAADD';
anets.push(planet); // 5

Notes:

  1. We link the planet view object with the one of its orbit, because we'll need to access the values like the axes to make calculations for the animation.

  2. We define the starting position of the planet in orbit. It's simply a random angle between 0 and 360 degrees, converted to radians.

  3. We define the step in radians between two neighboring positions of a planet on its path. Each next position will be obtained by adding the step to the current position value.

  4. The initialization of the rendered shape. It is placed outside the canvas at -100,-100, because the coordinates on the orbital path will be calculated in the animation sequence and they are not yet necessary here.

  5. The planet object is added to the array of planet views.

You probably noticed that this function is declared as var, but not with this.: it was done intentionally. This function does not need to be called from outside of PlanetarySystemView object and can be invoked just after the initialization of the respective orbit. We can add the call as the last line inside initOrbit function:

Planet(orbit, planetModel);

Last thing about the view object. We'll need to access the properties of animated items, so there is a standard accessor method for planets field:

.getPlanets = function() {
turn planets;

Please also note that our model entities passed to the PlanetarySystemView object are never modified, but instead they are used in order to produce other objects that are responsible for view rendering only.


The Animation

The animation is managed by a dedicated object called PlanetarySystemRenderer, which has the following stub:

tion PlanetarySystemRenderer(canvas) {
per.setup(canvas); // 1

r view = new PlanetarySystemView(canvas); // 2

 TODO


Notes:

  1. Setting up the context for PaperJS.

  2. Creation of the view. PlanetarySystemRenderer will be the only holder of the view object.

To calculate the position of a planet on its orbit, we need two functions that implement the well-known equation of ellipse:

calculatePlanetPosX = function(planetView) {
r orbit = planetView.orbit;
turn orbit.center.x + orbit.minRadius * Math.sin(planetView.position); // 1


calculatePlanetPosY = function(planetView) {
r orbit = planetView.orbit;
turn orbit.center.y + orbit.maxRadius * Math.cos(planetView.position); // 1

Note:

  1. Compared with the equation, we inverted cos and sin function calls in order to change the direction of orbital revolution. If we took sin for Y-axis and cos for X-axis, the revolution would be clockwise.

PlanetarySystemRenderer has a publicly accessible setScenery function that expects model data to be rendered:

.setScenery = function(star, planets) {
ew.initStar(star); // 1
r (var i = 0; i < planets.length; i++) {
view.initOrbit(planets[i]); // 1


Note:

  1. Each received data model object is redirected to the respective initialization function of the view.

There is also a function that will be called before rendering of each animation frame:

.updateView = function() {
r (var i = 0; i < view.getPlanets().length; i++) { // 1
var planet = view.getPlanets()[i];
planet.position = (planet.position + planet.step) % DOUBLE_PI; // 2
planet.shape.position.x = calculatePlanetPosX(planet); // 3
planet.shape.position.y = calculatePlanetPosY(planet); // 3


Notes:

  1. Loop on all planet views. Only planets are moving in the view, so we do not need to update the other objects.

  2. The calculation of the next position by adding the step delta value to the current position angle. The modulo division is done in order to keep the position value between 0 and 2Pi.

  3. The calculation of X and Y coordinates of the circle representing the planet.

Finally, to plug the animation into the loop, we need to fill the missing lines in our window.onload function, that we already used for tests before:

renderer = new PlanetarySystemRenderer(canvas); // 1
erer.setScenery(star, planets); // 2
r.view.attach('frame', renderer.updateView); // 3

Notes:

  1. Initialization of the renderer object.

  2. Passing the model data to the renderer.

  3. Triggering the animation function in PaperJS.


Result

Assembled together, all the pieces produce this animation:


Source Code

The complete source files for the html page and the javascript file are available.

 
 
Tags:  programming javascript paperjs animation modeling planetary system
Level:  intermediate