Earth and Venus share an interesting orbital relationship: for every eight times our home planet goes around the sun, Venus circles 13 times. This coincidental orbital resonance creates a beautiful five-fold symmetry when plotted out, as shown in the SVG above; this article shows how to create this and similar effects in your own work.

Above The Plane of the Elliptic

The base markup consists of <circle> elements representing Earth, Venus and the sun, together with the planet’s respective orbits, seen from “above” the solar system, looking “down”:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 500 500" >
    <g id="orbits">
        <circle id="venusorbit" cx="250" cy="250" r="120" />
        <circle id="earthorbit" cx="250" cy="250" r="165" />
    </g>
    <g id="lineGroup"></g>
    <circle id="earth" cx="250" cy="85" r="8" />
    <circle id="venus" cx="250" cy="130" r="5" />
    <circle id="sol" cx="250" cy="250" r="16" />
</svg>

The lineGroup element in the middle will be used as the insertion point of the lines drawn during each small orbital increment.

The elements are styled using :

#orbits circle {
  fill: none;
  stroke: #fff;
  stroke-width: 3px;
}
#lineGroup line {
  stroke-width: 1px;
}
#earth {
  fill: blue;
}
#venus {
  fill: hsl(60,80%,80%)
}
#sol {
  fill: yellow;
}

Constants of the Spheres

The added to the SVG has a set of constants at the start:

const earthDeg = 5,
earthOrbits = 8,
venusOrbits = 13,
resonance = earthOrbits / venusOrbits,
centre = 250,
earthDist = centre - parseInt(earth.getAttribute("cy"), 10),
venusDist = centre - parseInt(venus.getAttribute("cy"), 10);

It would also be possible to determine the centre of the SVG by subtracting components of the viewBox to get the height and width of the element, then dividing each by 2; in this case, I’ve hard-coded the centre as a constant. The orbital radius of Earth and Venus is gained by using the centre of each planetary circle (derived using parseInt) subtracted from the centre of the SVG element.

The second half of the script orbits the planets and draws the lines. It starts with a variable set to 0 using let:

let i = 0,
orbitals = setInterval(function(){
  earth.setAttribute("transform", "rotate("+ i + " " + centre + " " + centre + ")");
  venus.setAttribute("transform", "rotate("+ i / resonance + " " + centre + " " + centre + ")");
  let earthX = Math.cos((i*Math.PI/180)) * earthDist + centre,
  earthY = Math.sin((i*Math.PI/180)) * earthDist + centre;
  venusX = Math.cos((i/(earthOrbits/13))*Math.PI/180) * venusDist + centre,
  venusY = Math.sin((i/(earthOrbits/13))*Math.PI/180) * venusDist + centre,
  resLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
  resLine.setAttribute("x1", earthX);
  resLine.setAttribute("y1", earthY);
  resLine.setAttribute("x2", venusX);
  resLine.setAttribute("y2", venusY);
  resLine.setAttribute("stroke", "hsla(" + i + ", 50%, 50%, 0.5)");
  lineGroup.appendChild(resLine);       
  i += earthDeg;
    if (i == (360 * earthOrbits) + earthDeg) {
      clearInterval(orbitals);
     }
}, 60);

Earth is rotated around the centre of the SVG by the value of i; Venus is the same, but with the value of i divided by the ratio mentioned at the start of this article.

That’s the planets rotated into place; next, we need to determine where they are in SVG coordinate space. That’s determined with a little trigonometry to bring out the X and Y coordinates of each planet.

Using these values, a new line element is created and appended to the lineGroup we created in the markup. I’ve also used the value of i to determine the color of the line using hsla. (Values over 360° will map back to the equivalent degree in 0 - 360 color space).

Finally, i is added to by the value of earthDeg. This entire portion of the script is repeated every 60 milliseconds, until i is equal to 360 times the value of earthOrbits + earthDeg, bringing the planets back into alignment.

Angle of Opposition

If you run the code as-is you’ll find that there’s a small problem: the lines between the orbits are drawn 90 degrees after the actual position of the planets, due to assumptions in the calculations that they start at a mathematical, horizontal degree of 0, rather than at the top of the circle.

Rather than trying to alter the calculations, I hacked the result by rotating lineGroup backwards 90 degrees in the original markup:

<g id="lineGroup" transform="rotate(-90 250 250)"></g>

This brings the projected lines to the actual positions of the planets.

SVG or Canvas?

Given that the lines themselves aren’t animated, and there’s 577 of them, it’s reasonable to ask why this was created in SVG, rather than HTML5 canvas. I had three reasons:

  1. While I started the original code in <canvas>, I found that the SVG approach was more straightforward, and made more sense to me.
  2. Taking the code all the way in <canvas> would have involved “sticking” some of the drawing (the orbits, Sol, and added lines) and “wiping” others (the planets) between each orbital update. Again, the SVG approach was more straightforward.
  3. I liked the precise vector result of the SVG, making it scalable to all sizes with no loss in quality.

Ultimately, the canvas approach wouldn’t be too terribly different from the SVG one, and this code would probably make a good start at such an attempt.

Conclusion

The same technique could be used to draw out all kinds of mandala-like designs; I’d encourage you to play with the associated Codepen, changing the orbital ratio, to see the results.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/pELERa