<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>
    /* 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%;
    }
  </style>
</svg>

<button class="js-refresh-button">Refresh</button>

/**
 * Code related to setting up the codepen, not the solar systems
 */

* {
  margin: 0;
}

body {
  display: flex;
  flex-direction: column;
  align-items: center;
}

svg {
  width: min(100vw, calc(100vh - 4.5em));
  height: auto;
}

button {
  margin-top: 1em;
  appearance: none;
}

:root {
  --hue: 200;
}

button {
  /* Text Colors */
  --text-saturation: 90%;
  --text-lightness: 40%;
  
  --text-saturation-hover: calc(var(--text-saturation) + 10%);
  --text-lightness-hover: calc(var(--text-lightness) - 5%); ;
  
  --text-saturation-active: var(--text-saturation-hover);
  --text-lightness-active: calc(var(--text-lightness) - 10%); ;
  
  --text-saturation-disabled: calc(var(--text-saturation) - 60%);
  --text-lightness-disabled: calc(var(--text-lightness) + 10%);
  
  /* Background Colors */
  --background-saturation: 0%;
  --background-lightness: 100%;
  
  --background-saturation-hover: calc(var(--background-saturation) + 80%);
  --background-lightness-hover: calc(var(--background-lightness) - 5%);
  
  --background-saturation-active: var(--background-saturation-hover);
  --background-lightness-active: calc(var(--background-lightness) - 10%);
  
  --background-saturation-disabled: calc(var(--background-saturation) + 30%);
  --background-lightness-disabled: calc(var(--background-lightness) - 10%);
  
  /* Border Colors */
  --border-saturation: 90%;
  --border-lightness: 60%;
  
  --border-saturation-hover: calc(var(--border-saturation) + 10%);
  --border-lightness-hover: calc(var(--border-lightness) - 10%);
  
  --border-saturation-active: var(--border-saturation-hover);
  --border-lightness-active: calc(var(--border-lightness) - 20%);
  
  --border-saturation-disabled: calc(var(--border-saturation) - 60%);
  --border-lightness-disabled: calc(var(--border-lightness) + 20%);
  
  /* Focus shadow styles */
  --shadow-saturation-focus: 100%;
  --shadow-lightness-focus: 85%;
  
  /* Color Styles */
  color: hsl(var(--hue), var(--text-saturation), var(--text-lightness));
  background-color: hsl(var(--hue), var(--background-saturation), var(--background-lightness)); 
  border:0.1em solid hsl(var(--hue), var(--border-saturation), var(--border-lightness)); 
  
  /* Misc. Styles */
  border-radius: 0.25em;
  cursor: pointer;
  display: inline-block;
  font-size: 1em;
  padding: 0.5em 1em;
  transition-property: box-shadow, background-color, border-color, color;
  transition-timing-function: ease-out;
  transition-duration: 0.2s;
}

button:hover {
  color: hsl(
    var(--hue), 
    var(--text-saturation-hover), 
    var(--text-lightness-hover)
  );
  
  background-color: hsl(
    var(--hue), 
    var(--background-saturation-hover), 
    var(--background-lightness-hover)
  );
  
  border-color: hsl(
    var(--hue), 
    var(--border-saturation-hover), 
    var(--border-lightness-hover)
  );
}

button:active {
  color: hsl(
    var(--hue), 
    var(--text-saturation-active), 
    var(--text-lightness-active)
  );
  
  background-color: hsl(
    var(--hue), 
    var(--background-saturation-active), 
    var(--background-lightness-active)
  );
  
  border-color: hsl(
    var(--hue), 
    var(--border-saturation-active), 
    var(--border-lightness-active)
  );
}

button:focus {
  outline: none;
  box-shadow: 0 0 0 0.25em hsl(
    var(--hue), 
    var(--shadow-saturation-focus),
    var(--shadow-lightness-focus)
  );
}

body {
  font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif; 
}
// Define a couple variables about our SVG grid
const width = 1000;
const height = 1000;

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

function draw() {
  let starSize = randomInt(70, 120);
  let markup = drawStar(starSize) + addPlanets(starSize);
  
  document.querySelector(".js-svg-wrapper").innerHTML = markup;
}

function addPlanets(starSize) {
  let count = 0;
  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) + drawPlanet(planetSize, orbitDistance, count);

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

  return markup;
}


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);
  let hueDiff = hueRange[1] - hueRange[0];
  let hue2 = (hueRange[1] - hue > hue - hueRange[0]) ? randomInt(hue + hueDiff/2, hueRange[1]) : randomInt(hue - hueDiff/2, hueRange[0]);
  // If red is greater than 360,
  // use remainder
  if (hue > 360) {
    hue = hue - 360;
  }
  if (hue2 > 360) {
    hue2 = hue2 - 360;
  }
  const starColor = `hsl(${hue}, ${random(90, 100)}%, ${random(60, 70)}%)`;
  const starColorSaturatedAndLight = `hsl(${hue}, 100%, 60%)`;
  const backGlowColor = `hsl(${hue2}, ${random(80, 90)}%, ${random(40, 50)}%)`;
  const frontGlowColor = `hsl(${hue2}, 100%, ${random(50, 60)}%)`;
  
  return `
      ${starFilters({starColor, starColorSaturatedAndLight, frontGlowColor, backGlowColor, size})}
      ${starCircles({starColor, starColorSaturatedAndLight, frontGlowColor, backGlowColor, size})}
  `;
}

function starFilters({starColor, starColorSaturatedAndLight, backGlowColor, frontGlowColor, size}) {
  const blurFilterSize = 300;
  return `
      <defs>
        <filter id="star-main">
          <feTurbulence type="fractalNoise" baseFrequency="${size / 5000}" numOctaves="5" seed="${random(0, 100)}"/>
          <feDiffuseLighting lighting-color="${starColorSaturatedAndLight}" surfaceScale="${size / 20}">
            <feDistantLight azimuth="45" elevation="60" />
          </feDiffuseLighting>
          <feComposite operator="in" in2="SourceGraphic"/>
          <feGaussianBlur stdDeviation="${size / 100}"/>
        </filter>

        <filter
          id="star-glow" 
          filterUnits="userSpaceOnUse"
          x="0" 
          y="0" 
          height="${height}" 
          width="${width}"
        >
          <feGaussianBlur stdDeviation="${size / 5}"/>
        </filter>

        <filter
          id="star-secondary-glow" 
          filterUnits="userSpaceOnUse"
          x="${width/2 - size * blurFilterSize}" 
          y="${height/2 - size * blurFilterSize}" 
          height="${size * 2 * blurFilterSize}" 
          width="${size * 2 * blurFilterSize}"
        >
          <feGaussianBlur stdDeviation="${size / 5}"/>
        </filter>

        <filter
          id="star-turbulent-glow" 
        >
          <feTurbulence baseFrequency="${0.75 / size * 10}" seed="${random(0, 100)}"/>
          <feDiffuseLighting lighting-color="${starColorSaturatedAndLight}" surfaceScale="${1 * size / 20}">
            <feDistantLight azimuth="45" elevation="60" />
          </feDiffuseLighting>
          <feComposite operator="in" in2="SourceGraphic"/>
          <feGaussianBlur stdDeviation="${size / 30}"/>
        </filter>
      </defs>
  `;
}

function starCircles({starColor, starColorSaturatedAndLight, backGlowColor, frontGlowColor, size, isMain}) {
  const cx = width/2;
  const cy = height/2;
  
  return `
      <circle r="${size}" cx="${cx}" cy="${cy}" filter="url(#star-glow)" fill="${backGlowColor}" opacity="0.7"/ class="star-glow"/>
      <circle r="${size * 0.85}" cx="${cx}" cy="${cy}" filter="url(#star-turbulent-glow)" fill="${backGlowColor}" opacity="0.7" class="turbulent-glow"/>
      <circle r="${size * 0.8}" cx="${cx}" cy="${cy}" fill="${starColorSaturatedAndLight}"  class="flat"/>
      <circle r="${size * 0.75}" cx="${cx}" cy="${cy}" filter="url(#star-main)" opacity="0.9" class="main-turbulence"/>
      <circle r="${size * 0.74}" cx="${cx}" cy="${cy}" filter="url(#star-secondary-glow)" fill="${frontGlowColor}" opacity="0.7" class="inner-glow"/>
  `;
}

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';
  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 valus to create our filter:
  const defs = `
      <clipPath id="${id}-shadow-clip-path">
      <circle cx="${cx}" cy="${cy}" r="${size + 2}" />
    </clipPath>
    <radialGradient id="${id}-shadow">
      <stop offset="0%" stop-color="hsla(0, 0%, 0%, 0)"></stop>
      <stop offset="90%" stop-color="hsla(0, 0%, 0%, 1)"></stop>
    </radialGradient>
    <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 = `
    <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 + 2}" fill="url(#${id}-shadow)" clip-path="url(#${id}-shadow-clip-path)"/>
    </g>
  `;

  return defs + 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"
    />
  `;
}


/**
 * Initialize our art piece
 */
draw();

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

//
// Randomization Functions
//

// 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)];
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.