<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: 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)];
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.