Generating SVG Solar Systems, Part 1: Setting the Scene

Written by Paul Hebert on

Lately I’ve been having lots of fun creating procedurally generated artwork. These art pieces are drawn by a computer following a series of predefined steps and making random choices along the way. My favorite piece so far generates solar systems:

Click “Refresh” to generate new solar systems

I learned a ton about JavaScript, SVGs, CSS (and space!) while making this. It’s way too much to fit into a single article so in this post will focus on generating and animating solar systems using SVGs, JavaScript, randomness, and CSS.

By the end of this section, we’ll have built the following generative art piece. (Part 2 will finish the solar system shown above.)

Click “Refresh” to generate new solar systems

All of my generative art pieces share three building blocks: JavaScript randomness functions, an SVG wrapper, and a draw() function.

JavaScript Randomness functions

The fun of procedurally generated artwork comes from mixing things up. By having the program make random choices we can make each art piece unique and different.

JavaScript exposes a Math.random() function that will return a pseudo-random number between 0 and 1. We can use this to build a few other randomness helper functions. None of these are perfectly random, but they’re close enough for our use case:

// Return a number between two values.
function random(min, max) {
  const difference = max - min;
  return min + difference * Math.random();
}

// Returns a random integer between two values
function randomInt(min, max) {
  return Math.round(random(min, max));
}

// Returns true or false. By default the chance is 50/50 but you 
// can pass in a custom probability between 0 and 1. (Higher 
// values are more likely to return true.)
function randomBool(probability = 0.5) {
  return Math.random() > probability;
}

// Returns a random item from an array
function randomItemInArray(array) {
  return array[randomInt(0, array.length - 1)];
}

With these helpers in place we’ll be able to introduce randomness so that each solar system we generate is unique. Next up, we need an SVG wrapper for our solar system. 1

SVG Wrapper

An SVG element will contain all of the graphics that make up our solar systems:

<svg
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  width="200"
  height="200"
  viewBox="0 0 1000 1000"
  style="background: #000;"
  role="img"
  aria-labelledby="solarSystemTitle"
  aria-describedby="solarSystemDescription"
>
  <title id="solarSystemTitle">
    A procedurally generated solar system
  </title>
  <desc id="solarSystemDescription">
    A 2D rendering of a solar system, 
    with planets orbiting a central star.
  </desc>

  <!-- This element will store our graphics -->
  <g class="js-svg-wrapper"></g>

  <!-- This stores any global CSS styles -->
  <style></style>
</svg>

There are a few things to note about this SVG:

  1. The viewBox attribute defines our SVG coordinate system. In this case we’re saying that all of our graphics will be placed relative to a 1000 pixel square grid.
  2. I’ve given the SVG the role="img" and an accessible name and description using aria-labelledby and aria-describedby. This will give screen reader users information about the graphic we’re creating. (Though writing good descriptions for generative art can be very tricky!)
  3. I’ve added a .js-svg-wrapper group element. This will be the element we insert content into.
  4. We’ve got an empty <style> tag. We’ll be adding some global CSS there later.

A draw() Function

Next we need to populate our SVG. To do so, we’ll create a draw() function which sets the innerHTML of our wrapper element. For now, we’ll place a randomly sized circle at the center of our SVG to represent our solar system’s star:

// Define a couple variables about our SVG grid
const width = 1000;
const height = 1000;

// Our draw function is where the ✨ magic ✨ happens
function draw() {
  let starSize = randomInt(70, 120);
  let markup = drawStar(starSize);

  document.querySelector(".js-svg-wrapper").innerHTML = markup;
}

// We create a separate drawStar function to make it easier 
// to change later
function drawStar(size) {
  // `cx` and `cy` represent the x and y coordinates for the 
  // center of our circle.
  // `r` sets the radius of the circle. We'll use our star size.
  // For now we'll make our star white (`#fff`). 
  // We'll change the color soon.
  return `
    <circle 
      cx="${width / 2}" 
      cy="${height / 2}" 
      r="${size}" 
      fill="#fff"
    />
  `;
}

We’ll hook up this function to run when the script first loads, and re-run when a “Refresh” button is clicked:

draw();

const refreshButton = document.querySelector(".js-refresh-button")
refreshButton.addEventListener("click", draw);

We’ve now got a generative art piece! (Just not a very exciting one.) Click the refresh button to randomly resize the star in our SVG.

Expanding Our draw() Function

We’ve now got all of our boilerplate in place. From here on out, we’ll be making changes to our draw() function.

Adding Planets

First off, let’s add some planets. It’s not much of a solar system if it’s just one star.

Let’s add a couple functions to draw planets and their orbit paths. For now our planets are plain white, but we’ll add colors soon.

function drawPlanet(size, distance) {
  // We center the planet vertically, but we adjust the x
  // position by our orbit distance. The `planet` class
  // will be used to set up our planet orbit CSS
  return `
    <circle 
      cx="${width / 2 + distance}" 
      cy="${height / 2}" 
      r="${size}" 
      fill="#fff"
      class="planet"
    />
  `;
}

function drawOrbit(distance) {
  // The orbit is centered and has a radius equal to our
  // current distance
  return `
    <circle 
      cx="${width / 2}" 
      cy="${height / 2}" 
      r="${distance}" 
      stroke="#ccc"
      fill="none"
    />
  `;
}

Now we need to call these functions. We’ll create a new addPlanets() function and call it from our main draw() function:

let markup = drawStar() + addPlanets(starSize);

We’ll use a while loop in our addPlanets() function to keep adding planets until we’re getting to the edge of our canvas:

// Define some helper functions to randomize plant size
// and orbit distance
let randomPlanetSize = () => randomInt(10, 50);
let randomOrbitDistance = () => randomInt(100, 120);

function addPlanets(starSize) {
  let markup = "";

  // Set up our first planet
  let planetSize = randomPlanetSize();
  let orbitDistance = starSize + randomOrbitDistance();

  // Keep adding planets until a planet's orbital distance and
  // size would lead to it extending past our canvas
  while (orbitDistance + planetSize < 500) {
    // Add our new planet and its orbit path to our markup
    markup += drawOrbit(orbitDistance);
    markup += drawPlanet(planetSize, orbitDistance);

    // Prep our next planet so the while loop can check
    // whether it's in bounds
    planetSize = randomPlanetSize();
    orbitDistance += randomOrbitDistance();
  }

  return markup;
}

Now we’re getting somewhere! We’ve got a central star with planets placed on orbit paths around it! Go ahead and click that “Refresh” button a few times and watch the scene re-draw itself.

Animating our Orbits

This is still a little boring. It would be nice to get the planets to rotate around the star. Luckily, we can use CSS transforms and keyframe animations to get this working! Let’s go back and add some CSS to the <style> tag inside of our main SVG markup.

/* 
  Storing values as custom properties will make them 
  easier to change later 
*/
:root {
  --start-rotation: 0deg;
  --rotation-speed: 10s;
}

/* Set up an animation to rotate from 0 to 360 degrees */
@keyframes orbit {
  from {
    transform: rotate(var(--start-rotation));
  }
  to {
    transform: rotate(calc(var(--start-rotation) + 360deg));
  }
}

.planet {
  /* Apply our animation to the planets */
  animation: orbit var(--rotation-speed) infinite linear;
  /* 
    Within an SVG, the transform-origin is set relative 
    to the SVG. This ensures our orbit will rotate around 
    the center of our star 
  */
  transform-origin: 50% 50%;
}

This is closer but it’s still not quite right. All of the planets are orbiting around at the same speed and rotation. Ideally each planet would have its own speed and rotation. Since our CSS is already using custom properties, we can update our drawPlanet() function to set unique values for those custom properties. We can incorporate the distance into our random rotation speed to make further orbits take longer.

function drawPlanet(size, distance) {
  return `
    <circle 
      cx="${width / 2 + distance}" 
      cy="${height / 2}" 
      r="${size}" 
      fill="#fff"
      class="planet"
      style="
        --start-rotation:${randomInt(0, 360)}deg;
        --rotation-speed:${distance * randomInt(40, 70)}ms;
      "
    />
  `;
}

Now each planet has a randomized starting rotation and rotation speed:

But these planets still feel a little plain. Let’s add some colors!

Adding colors

For generative art I really like using HSL colors. Since they allow us to separately set the hue, saturation, and lightness of a color, they make it easy to randomly generate colors within specific parameters. Let’s update our drawPlanet() function to use a randomized color for its fill:

function drawPlanet(size, distance) {
  const hue = randomInt(0, 360);
  const saturation = randomInt(70, 100);
  const lightness = randomInt(50, 70);
  const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;

  return `
    <circle 
      cx="${width / 2 + distance}" 
      cy="${height / 2}" 
      r="${size}" 
      fill="${color}"
      class="planet"
      style="
        --start-rotation:${randomInt(0, 360)}deg;
        --rotation-speed:${distance * randomInt(40, 70)}ms;
      "
    />
  `;
}

Note that hue is a value between 0 and 360, while saturation and lightness are both percentages (and require percentage signs.) We’re picking a random hue from 0 to 360, while limiting saturation and lightness to predefined ranges. Here’s what that looks like:

Next up, let’s give our star a color. This is going to use a similar technique but is a little trickier. According to my super-scientific method of googling “what color are stars” it turns out that humans don’t see green or purple light emitted from stars, so we shouldn’t allow any of our stars to be green or purple!

We’ll need to adjust how we calculate our hue to omit green and purple hues. To do so, we’ll need to take a look at the how HSL hue values map to colors:

A rectangular gradient showing how HSL hues range from 0 to 360. 0 and 360 are both red. 60 is yellow. 120 is green. 180 is teal. 240 is blue, and 300 is pink.

For our use case we’ll allow the following hue ranges:

  • Red: 0 to 30, or 330 to 360.
  • Yellow: 40 to 60
  • Blue: 190 to 240

With a bit of math, we can choose a random hue in one of those ranges:

function drawStar(size) {
  // Note upper range of red exceeds 360
  const hueRange = randomItemInArray([
    [330, 390],
    [40, 60],
    [190, 240],
  ]);

  // Pass along chosen array as arguments
  let hue = randomInt(...hueRange);

  // If red is greater than 360, use the remainder
  if (hue > 360) {
    hue = hue - 360;
  }

  // We'll use higher saturation and lightness values for our 
  // star than our planets.
  const saturation = randomInt(90, 100);
  const lightness = randomInt(60, 80);
  const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;

  return `
    <circle 
      cx="${width / 2}" 
      cy="${height / 2}" 
      r="${size}" 
      fill="${color}"
    />
  `;
}

Now our star should be red, yellow or blue:

Next Steps

Awesome! We’ve built a program for procedurally generating solar system! But it’s still feeling a bit flat… It would be great if we could make this a little more lifelike with textures and realistic lighting. To do so, we’ll need to add a few more tools to our toolbox.

In part two of this series we’ll use SVG filters, gradients, and clipping paths to turn our flat solar system into something a little more lifelike:

Click “Refresh” to generate new solar systems

Continue the journey with part two of this series: SVG Filters, Gradients, and Clip Paths


  1. (If you’re not familiar with SVGs, check out Tyler Sticka’s excellent presentation on SVGs). 
Paul Hebert

Paul Hebert is a hybrid designer and developer at Cloud Four. When he's not designing and developing websites he enjoys bouldering, drawing, cooking, gardening, and eating too much cheese.

Never miss an article!

Get Weekly Digests


Leave a Comment

Please be kind, courteous and constructive. You may use simple HTML or Markdown in your comments. All fields are required.


Let’s discuss your project! Email Us