Particles & spiral patterns in CSS: part II

Particles & spiral patterns in CSS: part II

Imitating the spirograph with HTML and CSS (and their preprocessors, Pug and SCSS)

The week of Codepen's particle challenge is up - but I'm far from done obsessing over particles and serpentines. In Friday's Part I I merely scratched the surface, showing you my first demo for this week's challenge:

and based on that, showed you how to draw the most basic 2D serpentine pattern:

To reiterate: the dots are deposited in equal intervals by a line spinning around its center point and also moving in a straight line - like so:

For my second demo I decided to push it a little further in terms of complexity:

and then tweaked it a bit:

It was a headache to coordinate - and now might be a headache to wrap your head around ;)

Let me break down what's happening here - which is only a slight modification of my original linear serpentine. This time, however, instead of traveling along a straight line from one end to another, the pivot point is stationary. It is at the center of the viewport. Also, the line that spins around like the hand of a clock isn't the one drawing. It is merely a join for another line, and it is that one that deposits the dots as it spins while being spun itself:

As complex as that sounds, the HTML setup for it is almost as simple as for the linear serpentine:

<div class="spiroGraph"> <!-- the container for our serpentine -->
  <div class="arm"> <!-- the spinning joint anchored at the center of the viewport -->
    <div class="foreArm"></div> <!-- the joing that spins while also being spun -->
  </div>
</div>

We'll need to spawn a large number of the .arms - and like last time, the easiest way to do that is using Emmet - which is built into VS Code and Codepen. Instead of typying up the entire HTML markup above manually, just type:

.spiroGraph>.arm*180>.foreArm

and press Enter (VS Code) or Tab (Codepen) - and witness 180 .arms, each with a .foreArm, spawned instantly.

That does it for HTML - so let's go over to our style sheets. As before, let's use SCSS - which will allow us to write most of our code just as we would in plain CSS, but will let us use for-loops for quickly styling each of our 180 .arms and .foreArms, and also declare variables.

First, let's set up the scene:

$particles: 360; /* an SCSS variable defining the number of particles across the whole serpentine; not the same as a CSS variable */

body {
  margin: 0;
  background-color: #000; /* hex code for the color black */
  height: 100vh; /* so that the body takes up the entire viewport's height */
  display: grid; /* we'll want to center the content */
  align-content: center;
  overflow: hidden;
  font-size: .8vmin; /* setting font size to be .8 of 1% of the shorter viewport dimention (height on desktop, width on mobile  */
}

.spiroGraph {
  position: relative; /* thus the container will be the point of reference for its child, the .foreArm */
  display: flex;
  height: 100em; /* using font size as a unit for object size - making it 80% of the shorter vieport dimension */
  justify-content: center; /* we'll want the .arm to be placed at the center of the horizontal axis */
}

Now for all the moving bits:


.arm {
  --particles: #{$particles}; /* a proper CSS variable that takes its name and value after its SCSS counterpart */
  position: absolute; /* the container floats freely, independent of any other objects */
  bottom: 50em; /* the bottom end of the .arm is anchored at the center of the viewport */
  height: 30em; /* since there's got to be room for the .foreArm, we don't want it to be be 50em - but can't be less than 25em */
  transform-origin: bottom; /* its pivot point is located at its bottom end */
  rotate: calc(360deg/var(--particles)*var(--turnStep)); /* it does a full 360 over 360 particles - ergo each step is 1deg - but this formula will let you tweak that number  */
}

.foreArm {
  --loops: 9; /* we'll want our serpentine to have 9 loops */
  position: absolute;
  bottom: 100%; /* its bottom will be where normally its top would be; thus it'll be anchored at the tip of the .arm */
  height: 20em; /* 50em (half of the viewport's height) minus the height of the .arm */
  /* also note we didn't declare the width for either - which is why we do this: */
  display: flex;
  justify-content: center; /* the .foreArm's child WILL have width, and we need to cente it */
  transform-origin: bottom; /* its pivot point is located at its bottom end */
  rotate: calc(360deg/var(--particles)*var(--loops)*var(--turnStep)); /* it has to make 9 loops of full 360 rotations, ergo it has to spin 9 times faster than the foreArm: 1deg x 9 = 9deg */
}

.foreArm::before {
  content: ''; /* no text */
  position: absolute;
  width: 2em;
  aspect-ratio: 1; /* the height matches the width */
  border-radius: 50%; /* our particle will be round */
  background-color: red /* for now */
}

Since we haven't set up the variable of --turnStep for each .arm:nth-child, no rotation was applied - so what you're probably seeing right now is a single red dot in the middle at the top of the container;

Let's fix that - and quickly loop over the variable:

@for $i from 1 through 360 {
  .arm:nth-child(#{$i}) {
    --turnStep: #{$i};
  }
}

Now that's more like it. Are you seeing a spiral of 360 red dots?

We can just as easily add some more color to it - by applying the same degree-based formula that we did to rotation.

.foreArm::before {
  content: ''; /* no text */
  position: absolute;
  width: 2em;
  aspect-ratio: 1; /* the height matches the width */
  border-radius: 50%; /* our particle will be round */
  background-color: hsl(calc(360deg/var(--particles)*var(--turnStep)) 100% 50%); /* the color will cycle through the full spectrum of colors of the rainbow */
}

That should make our serpentine look like this:

And thus we get a proper spirograph-like serpentine. But again, this is really basic, and there's a whole lot more you can do with it. You can make more - or fewer - particles as you like (as long as you make sure your HTML and your SCSS have the same number of objects). Or experiment with dot sizes - or arm lengths and centers of rotation.

To make things easier, here's a live demo on Codepen that you can tinker with. For instance, see the madness that happens when you just reverse the direction of rotation on the .foreArm - like this:

rotate: calc(-360deg/var(--particles)*var(--loops)*var(--turnStep)); /* notice the minus before 360deg - that makes it assume the negative value, and this reverse the direction of rotation */

Have fun - and come back next time for another crazy loop done in (almost) pure HTML and CSS.