Generating SVG Solar Systems, Part 2: Filters, Gradients, and Clip Paths

Written by Paul Hebert on

In the first part of this series we created a program to generate unique solar systems by drawing and animating SVG circles:

But the solar systems are composed of solid colors and feel a little flat. We can make them feel more lifelike and fun by using some SVG magic. Here’s what we’re working towards.

Click “Refresh” to generate new solar systems

SVG Filters (a.k.a. Avoiding Complicated Math)

I spent some time researching how to generate different textures. Unfortunately, most of the solutions I found included doing a bunch of complicated math that I’d rather not do. Luckily, I found an excellent article by Sara Soueidan, showing how to use SVG filters to create textures. With a few lines of code, we can have the browser generate unique textures for us!

Styling Planets

We can use SVG filters to add textures to our planets. Filters are created as SVG elements, which you can then reference via ID to style other elements using the filter attribute:

<filter id="my-filter">
  <!-- 
    You can add multiple layers of filter effects here 
    to create an SVG filter 
  -->
</filter>

<circle filter="url(#my-filter)" />

A Turbulent World

First off we’ll create some “turbulence” using the <feTurbulence> filter. Turbulence in this context refers to randomly generated “noise” or textures. The filter has four different attributes we’ll be using. (Don’t worry, these will make a lot more sense when we get to the demo.)

  • baseFrequency: This attribute determines how tall and wide our noise is.
  • numOctaves: This attribute determines the level of detail in our noise. Higher numbers look more natural, but are more computationally expensive.
  • type: Setting the type to fractalNoise will produce a smoother noise with less sharp edges
  • seed: The seed is used as a starting point for the random noise. Using unique seeds will ensure you get unique noise.

By default our filters will extend outside of the planet we’re applying them to. Adding an <feComposite> filter layer will allow us to constrain the filters to our circle:

<filter id="my-filter">
  <feTurbulence baseFrequency="0.1" numOctaves="1" seed="0"/>
  <feComposite operator="in" in="SourceGraphic" />
</filter>

Here’s a demo showing turbulence applied to our planet circle. Adjust the turbulence properties to see how they affect our texture:

These textures are interesting, but they’re not quite what we’re looking for. In Sara’s article she showed how you can layer a lighting filter on top of turbulence to create more realistic textures.

Shining Some Light

Lighting filters treat two dimensional graphics as three dimensional graphics and simulate shining a light on them. We can add a lighting layer to our filter to create a more lifelike texture.

There are a few different types of lighting filters. For our use case we’re going to apply <feDiffuseLighting> on top of our turbulence. It has two attributes we’re interested in:

  • lighting-color: Determines the color of light to apply (and therefore the color of the generated texture)
  • surfaceScale: Determines the “height” of the surface the light is applied to. The higher the value, the more contrast will show in the generated texture.

Lighting filters are meant to wrap light sources. In our case we’ll be using <feDistantLight> which has a couple properties we’ll play with:

  • elevation: Determines the height the light shines from (how high in the sky the light is located.) This is represented as an angle between 0 and 360.
  • azimuth: Determines the direction the light shines from (0 to 360).

We’ll add these new filter layers to our existing filter. (Note that the lighting filters still come before the feComposite filter so that the lighting is limited to our planet’s shape.)

<filter id="my-filter">
  <feTurbulence baseFrequency="0.1" numOctaves="1" seed="0"/>
  <feDiffuseLighting lighting-color="DodgerBlue" surfaceScale="10">
    <feDistantLight azimuth="45" elevation="60"></feDistantLight>
  </feDiffuseLighting>
  <feComposite operator="in" in="SourceGraphic" />
</filter>

This is all a bit tricky to understand, but playing with a demo makes it clearer how the different filter attributes interact:

Now we’re getting somewhere! By tweaking the attribute values we’re able to generate some fun textures for our planets. Let’s update our solar system planets to use these effects.

Applying Our Texture Filters

To ensure our planets are unique and different we’ll use randomization to select values for our turbulence and lighting. But we want to make sure our values make pleasing textures.

In order to do so we’ll need to put some constraints on the values we generate. We can use our randomization functions to pick values from predefined ranges and update our drawPlanet() function from part 1:

// We added a new count parameter we'll use below.
function drawPlanet(size, distance, count) {
  const hue = randomInt(0, 360);
  const saturation = randomInt(70, 100);
  const lightness = randomInt(50, 70);
  const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
  const cx =  width/2 + distance;
  const cy =  height/2;

  // We'll use the current planet number to create unique IDs 
  // for our filters
  const id = `planet-${count}`;

  // We'll generate some random values for our turbulence
  const turbulenceType = randomBool() ? 'fractalNoise' : 'turbulence';
  // We intentionally make the y value larger than the x value
  // to create horizontal striping patterns
  const baseFrequencyX = random(0.5, 2) / size;
  const baseFrequencyY = random(2, 4) / size;
  const numOctaves = randomInt(3, 10);
  const seed = Math.random();

  // And some random values for our lighting
  const elevation = randomInt(30, 100);
  const surfaceScale = randomInt(5, 10);

  // We'll use those random values to create our filter:
  const filter = `
    <filter id="${id}-texture">
      <feTurbulence
        type="${turbulenceType}"
        baseFrequency="${baseFrequencyX} ${baseFrequencyY}"
        seed="${seed}"
        numOctaves="${numOctaves}"
      />
      <feDiffuseLighting lighting-color="${color}" surfaceScale="${surfaceScale}">
        <feDistantLight elevation="${elevation}" />
      </feDiffuseLighting>
      <feComposite operator="in" in2="SourceGraphic"/>
    </filter>
  `;

  // And apply the filter to our planet:
  const planet = `
    <circle
      class="planet"
      style="
        --start-rotation:${randomInt(0, 360)}deg;
        --rotation-speed:${distance * randomInt(40, 70)}ms;
      "
      r="${size}" 
      cx="${cx}" 
      cy="${cy}"
      fill="#000"
      filter="url(#${id}-texture)"
    />
  `;

  return filter + planet;
}

Let’s see it in action:

Click “Refresh” to generate new solar systems and planets.

It’s fun to see these textures on our planets, but they still don’t really feel three dimensional. Let’s add a shadow to them to simulate the “dark side” of planets facing away from the central star.

The Dark Side

To add our shadows we’ll be using two more SVG tools: radial gradients and clip paths.

We can use a <radialGradient> to create a smooth transition between two colors. For our shadow we’ll transition from complete transparency (hsla(0, 0%, 0%, 0)) to complete black (hsla(0, 0%, 0%, 1)):

<radialGradient id="shadow-gradient">
  <!-- Each stop represents a color -->
  <stop offset="0%" stop-color="hsla(0, 0%, 0%, 0)"></stop>
  <!-- 
    The offset says where on the gradient this color starts.
    We hit black 90% of the way to the edge of our gradient
  -->
  <stop offset="90%" stop-color="hsla(0, 0%, 0%, 1)"></stop>
</radialGradient>

<!-- We keep our existing planet -->
<circle cx="500" cy="500" r="400" filter="url(#texture)" />
<!-- 
  We add a new circle for our shadow and 
  reference our gradient as a fill.
  Our shadow is twice as wide as our planet, 
  but offset to one side.
-->
<circle cx="100" cy="500" r="800" fill="url(#shadow)" />

This is close to what we want, but our shadow is extending past the edge of our planet. We can add a <clipPath> to clip the shadow to our planet. Similar to filters and gradients, we’ll first define our <clipPath> and then reference it by ID:

<clipPath id="shadow-clip-path">
  <!-- 
    The shape of a clipPath is determined by its inner content.
    This circle should match our planet.
  -->
  <circle cx="500" cy="500" r="400" />
</clipPath>
<!-- 
  We add a `clip-path` attribute to our shadow
-->
<circle 
  cx="100" 
  cy="500" 
  r="800" 
  fill="url(#shadow)" 
  clip-path="url(#shadow-clip-path)"/>

We can take it a step further, and give our planets a highlight by adding a white circle shifted one pixel to the left:

<!-- White circle -->
<circle cx="499" cy="500" r="400" fill="white" />
<!-- Turbulent Texture -->
<circle cx="500" cy="500" r="400" filter="url(#texture)" />
<!-- Clipped gradient shadow -->
<circle 
  cx="100" 
  cy="500" 
  r="800" 
  fill="url(#shadow)" 
  clip-path="url(#shadow-clip-path)"/>
This looks a little extreme, but will work better in the context of our solar system

These shadows are looking pretty good! Let’s update our drawPlanet() function. Since we’re now showing multiple circles, we’ll need to put them into a group and move our planet class and custom properties to the group so they all orbit together:

const planet = `
  <g
    class="planet"
    style="
      --start-rotation:${randomInt(0, 360)}deg;
      --rotation-speed:${distance * randomInt(40, 70)}ms;
    "
  >
    <circle
      r="${size}" 
      cx="${cx - 1}" 
      cy="${cy}"
      fill="#fff"
    />
    <circle
      r="${size}" 
      cx="${cx}" 
      cy="${cy}"
      filter="url(#${id}-texture)"
    />
    <circle 
      cx="${cx - size}" 
      cy="${cy}" 
      r="${size * 2}" 
      fill="url(#${id}-shadow)" 
      clip-path="url(#${id}-shadow-clip-path)"
    />
  </g>
`;

Here we can see it in the context of our larger solar system:

These planets are looking great! Let’s apply some similar effects to our stars!

Styling Stars

A lot of the effects we’ll apply to stars use the same techniques we used for planets, so I won’t do a deep dive, but I want to highlight one more SVG filter that will come in handy: <feGaussianBlur>.

This aptly named filter allows you to blur a graphic. The level of blurriness is set using the stdDeviation attribute. This will come in really handy for giving our stars “glowing” effects.

<filter id="blur">
  <feGaussianBlur stdDeviation="10">
</filter>

<circle cx="500" cy="500" r="250" filter="url(#blur)" fill="Crimson"/>
Use the slider to adjust the blur level

In order to build our stars we’ll use a combination of blurs, lighting and turbulence. In total we’ll use 5 different layers of circles:

  1. A glowing blur meant to show the star’s light shining outwards
  2. A glowing blur using turbulence meant to represent the jagged random flares of light around a star
  3. A flat, unfiltered circle blocking the parts of the two blurs that are behind the star itself
  4. A turbulent circle meant to give the star some random texture
  5. A smaller, radial gradient that makes the texture less intense and gives the star the illusion of roundness

I’m not going to go through the entire filter code here because it got quite lengthy, but you can check it out, and view how the different layers interact in the CodePen below:

On the left you can see each layer individually. On the right you can see them combined into a finished star. Click “Refresh” to generate new stars.

We’ll update our drawStar() function from part 1 to output these circles in our solar systems:

Setting up Star Fields

Now we’re getting somewhere! We’ve got generative stars and planets styled with some awesome SVG filters. But the background’s still a little plain. Let’s add a starry sky!

Again, we’ll be stacking multiple layers of graphics to get the effect we’re going for:

  1. A gradient to add a little color to the background
  2. Another gradient to add different colors to the background
  3. Some blurred, white turbulence
  4. A star texture that I copied from an excellent article by Bence Szabó (who is an SVG filter wizard!)

Here are the layers we’ll be rendering for our background. Again the individual layers are on the left, and they’re stacked on the right:

Now we can plug this in to our draw() function and complete our generative solar system!

function draw() {
  let starSize = randomInt(70, 120);
  let markup = drawStarField() + drawStar(starSize) + addPlanets(starSize);

  document.querySelector(".js-svg-wrapper").innerHTML = markup;
}
Awesome, we've made a generate solar system art piece! Spam that “Refresh” button to generate new solar systems!

Next Steps

I’m really pleased with the generative solar systems we’ve built but there are still lots of opportunities for improvement. We could add moons or rings to our planets, improve our filters, or add asteroid belts. (If you make an improvement, I’d love to see it! Please share it in the comments.)

We’ve also learned a lot of new skills. We’ve got a framework for creating generative art, and we’ve learned a ton about SVGs, CSS and JavaScript which we can apply to other areas of web design and development.

If you’re curious, you can check out more of my generative art at squigglesanddots.art.

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