How I Live-Coded My Most-Hearted Pen

The following is a guest post by Ana Tudor. If you've seen Ana's work, perhaps you know that she uses mathematics and code together to make art. The finished pieces look like they take ages to make. But as I witness with my own eyes, Ana can think through what it takes to build something like this while doing it incredibly quickly. Here she'll explain to us the entire thought process in a step-by-step tutorial.

In mid-August, I decided to try to reproduce a nice looking GIF I found on 12gon.

The original GIF

I thought I'd code it live for people to watch using CodePen's Professor Mode. 30 minutes later, this was the result:

See the Pen Möbius 6hedrons (pure CSS) by Ana Tudor (@thebabydino) on CodePen.

Let's take a look at how the whole thing works. It's surprisingly simple!

3D coordinate system

We'll be working in 3D.

Three dimensions: x, y and z.

The x axis goes from the left (its -) to the right (its +). The y axis goes from the top (its -) to the bottom (its +). The z axis goes from the back of the screen (its -) to the front, towards us (its +). The intersection of these three axes is the origin of the coordinate system. The xy plane (represented in blue in the figure) is the vertical plane of the screen. The yz plane is the vertical plane (represented in green) that splits the screen into a left part and a right part. The zx plane is the horizontal plane (represented in red) that splits the screen into a top part and a bottom part.

Important concept: every HTML element has a local 3D coordinate system, whose origin is initially situated at the 50% 50% 0 point of the element (50% horizontally, 50% vertically and in the plane of the element because all HTML elements are flat, all their points are contained in the same plane). It can be changed with the transform-origin property, but don't worry, we won't need to do that here.

Basic setup

The position of an element in 3D is always going to be relative to the 3D coordinate system of its parent, so we make the <body> cover the entire viewport and absolutely position all its descendants at the 50% 50% point of their respective parents. We also set transform-style: preserve-3d on the body's descendants because we want to allow nesting of 3D transformed elements (the bars will be transformed in 3D and so will its children, the bar faces).

body {
  height: 100vh;
  perspective: 40em;
  background: #000;
}

body * {
  position: absolute;
  top: 50%; left: 50%;
  transform-style: preserve-3d;
}

Given that this is a 3D demo and the <body> covers the entire viewport, and therefore be our scene, we have also set a perspective on it. This makes everything that is closer to us appear larger than everything that is far away. The smaller the perspective value, the bigger the difference between what's in front and what's in back. The following demo shows how changing the perspective value on the scene makes objects in the scene, in this case two cubes, be rendered differently. For each of the cubes, the demo also shows the xy plane of its local system of coordinates (in blue).

See the Pen what changing perspective on the scene does by Ana Tudor (@thebabydino) on CodePen.

Initial Data

Now let's gather a bit of data from the image. Kind of difficult since it's moving and makes us dizzy, so let's split it into frames. I have an aversion towards stuff I need to install, so I use an online splitter for this kind of stuff, but you can use whatever you're most comfortable with. Splitting the GIF tells me that there are 43 frames and the delay between them is 0.04s, making the duration of the animation somewhere around 1.75s.

Most importantly, this is the first frame.

First frame of the inspiration gif

Yay, a static image that lets me count the bars (or square right prisms) without getting me dizzy! And yes, I counted them by putting my finger on the current one I was counting and remembering where I started from. If I counted right, there are 24 bars. And if I didn't, it doesn't matter, I like 24. I have a good reason to like it. The central points of the bars are distributed on a circle around the y axis in the horizontal zx plane.

The bars are distributed on a circle around the Y axis in the horizontal ZX plane

Around a circle, there are 360°, as the demo below illustrates:

See the Pen full circle - responsive SVG explanation by Ana Tudor (@thebabydino) on CodePen.

24 happens to be a divisor of 360°, so if we want to distribute the bars evenly on the circle, we distribute them at every 360°/24 = 15°, which is a nice round number and I like round numbers—it's why I approximated the duration of the animation to be 1.75s. Let's assume my counting skills are accurate and leave the number of bars at 24 and the base angle between them at 15° because integers are good...they just make our lives easier!

Next, let's pick the four main shades from the image. I believe I used the dev tools picker for this, but you can use whatever tool you wish.

Picking the four main shades

The four shades I picked were for the end face of the bar in the front (1 in the figure above), the end face of the bar in the back (2), the lateral face of the bar in the front (3) and the lateral face of the bar in the back (4).

After visually approximating dimensions and distances (I'll admit I'm not good at that, but I guess these values work), we set the following variables in the Sass code:

$n-prisms: 24; // number of bars
$height: 6.25em;               // height of a bar
$base: 1em;                    // base of a bar
$base-c:                       // base shades
	#69f                   // base front (1)
	#7e4b4c;	       // base back (2)
$lat-c:                        // lateral shades
	#542252                // lateral front (3)
	#7e301a;               // lateral back (4)
$radius: 1.625*$height;        // radius of circle we distribute the bars on
$base-angle: 360deg/$n-prisms; // base angle between two bars
$t: 1.75s;                     // animation duration

Basic HTML structure

Next, let's decide on the HTML structure. Each bar has four lateral faces and two end (or base) faces, so that's six faces in total for each bar. The bars are all rotating around their fixed midpoints, which are located on a circle of a known $radius in the horizontal zx plane. This means that, inside the assembly of bars, we have 24 positioning elements that will move the bars on that circle and, inside each of these elements, we have a bar with six faces. This gives us the following HTML structure:

<div class="assembly">
  <div class="positioner">
    <div class="prism">
      <div class="prism__face"></div>
      <!-- 6 more faces just like above -->
    </div>
  </div>
  <!-- 23 more positioners just like above -->
</div>

But of course we won't copy paste the positioner bit so many times. There are smarter and more compact ways of writing this. For example, using Haml or Slim:

.assembly
  - 24.times do
    .positioner
      .prism
        - 6.times do
          .prism__face

The Bar Faces

Now that we have an HTML structure, let's move on to styling. We start with the faces of the bars because they're the only elements with a background, and we always want to see something on the screen as soon as possible (especially when live coding!). We give all faces the dimensions of the lateral faces (because we have more lateral faces than base faces, so we'll treat base faces as a particular case later on). We then set the margins to be minus half the dimensions of the faces, so that their 50% 50% points stay in the middle of their containers. Of course, we need to also give them a background so that we can see them. We can choose any of the two shades we have picked for lateral faces; which one doesn't matter, we'll overwrite with the proper mix between the two when we distribute the bars on that circle in the horizontal zx plane.

.prism__face {
  margin: -.5*$height (-.5*$base);
  width: $base; height: $height;
  backface-visibility: hidden;
  background: nth($lat-c, 1);
}

We have also given the faces backface-visibility: hidden, so that we only see them when we look at them from the front and they're invisible to us when we look at them from the back.

See the Pen what `backface-visibility` does by Ana Tudor (@thebabydino) on CodePen.

This is really useful when we want to check they're facing the right direction. It also prevents wrong 3D ordering issues and flickering in Firefox.

Next, we handle the particular case of the base faces. If we take the lateral faces to be the first four faces (so faces 1, 2, 3 and 4), then the base faces will be faces 5 and 6, so all faces whose 1-based index is greater than or equal to 5. We express this with the help of the nth-child pseudo-class.

.prism__face:nth-child(n + 5) {
  margin-top: -.5*$base;
  height: $base;
  background: nth($base-c, 1);
}

You can see what we have so far in the Pen below. Not much yet, but it's a start!

See the Pen Möbius 6hedrons - step 1 by Ana Tudor (@thebabydino) on CodePen.

Creating the Bars

The following step is to position the faces so that they actually form a bar in 3D. In order to do this, we need to understand how rotations and translations work.

Rotating an element around an axis by a positive angle value means a clockwise rotation, as seen from the + of the axis we rotate around. A positive rotation around the z axis is a clockwise rotation as seen from the + of this axis—the normal position of our eyes in front of the screen.

See the Pen rotation around the z axis by Ana Tudor (@thebabydino) on CodePen.

A positive rotation around the y axis means a clockwise rotation as seen from the + of this axis, which is at the bottom. In this case, we see the left part of our element coming forward and its right part going towards the back of the screen. For example, if we rotate an element by 90° around the y axis, its front face looks right, while a -90° rotation around the same axis makes it look towards the left.

See the Pen rotation around the y axis by Ana Tudor (@thebabydino) on CodePen.

A positive rotation around the x axis means a clockwise rotation as seen from the + of the very same axis—right of the screen in this case. So we see the bottom part of the element coming up and forward, while its top part goes down and towards the back. For example, after a 90° rotation around the x axis, our element is face up, while a -90° rotation around the same axis makes its face look down.

See the Pen rotation around the x axis by Ana Tudor (@thebabydino) on CodePen.

A very important thing we need to remember is that every transform we apply on an element is also applied on its local system of coordinates.

For example, if we rotate an element around its y axis by 90° degrees, not only is this element facing right after the rotation, but its z axis — the one that was pointing towards us from the screen before the rotation — is now pointing right. If we rotate an element by 90° around the x axis, not only is the element face up after the rotation, but its z axis points up as well. If we were to rotate it by -90°, the element would be face down and its z axis would point down as well.

So we need to keep in mind that, no matter how we transform the element, the z axis always points out from the front face of the element, the y axis always points towards the bottom of the element, while its x axis always points towards the right of the element.

See the Pen rotating an element also rotates its system of coordinates v2 by Ana Tudor (@thebabydino) on CodePen.

That was about rotations, now let's see translations. A translation of a positive value along an axis moves the face towards the + of that axis.

For example, a translation of a positive value along the z axis (which is pointing towards us) brings the element forward, closer to us, while a translation of a negative value along the same axis moves the element backwards, away from us.

See the Pen translating an element by Ana Tudor (@thebabydino) on CodePen.

Just like in the case of rotations, translations also affect the element's local system of coordinates — you can see it moving along with the element in the demo above.

The fact that any transform affects the element's system of coordinates means that it also affects the effect of any subsequent transforms we apply on that element.

For example, if we translate an element in the positive direction of the z axis, without having applied any other transform before, this moves our element forward. But if we rotate our element by 90° around the y axis and then translate it along the z axis in the positive direction, this translation moves the element towards the right of the screen (from our point of view in front of the screen), because the z axis now points towards the right after the rotation. In the same way, if we first rotate the element by 90° around the x axis, and then we translate it along the z axis in the positive direction, this translation moves the element up because the z axis points up after the rotation. If we rotate it by -90° around the x axis and then we translate it along the z axis in the positive direction, this translation moves the element down because the z axis points down after the rotation.

See the Pen transforms by Ana Tudor (@thebabydino) on CodePen.

Now let's see where the faces are initially located, and where we want to move them so that they actually form a bar.

Initial vs. Final

Since we have started by positioning everything absolutely in the plane of the screen (xy), dead in the middle of the screen, the initial 50% 50% point of the faces (before moving them so they form the bar) coincides with the point right in the middle of the bar (first panel in the figure above). Half the bar is behind the xy plane (blue, second panel), the other half the bar is in front. Half the bar is to the left of the yz plane (green, third panel), and the other half is on the right. Half the bar is above the zx plane (red, fourth panel), and the other half is below. The distance from that point to the faces in the front and in the back is half the base. The distance to the faces on the right and on the left is also half the base, while the distance to the faces at the top and at the bottom is half the height.

We have taken the first four faces to be the lateral ones and the last two to be the base (end) ones. So, we position the first face in the front of the bar and go around with the next three — the second goes on the right, third goes in the back, fourth of the left. Then, we position the fifth face at the top and the sixth one at the bottom.

Face numbering

In order to position the first face in front, all we have to do is translate it forward by half the base. This means a translation of .5*$base along the z axis:

.prism__face:nth-child(1) {
  transform: translateZ(.5*$base);
}

To position the second face on the right, we first need to rotate it by 90° (a right angle) around the y axis so that its z axis points to the right. Then we translate it by half the base in the positive direction of the post-rotation z axis (to the right of the screen):

.prism__face:nth-child(2) {
  transform: rotateY(90deg) translateZ(.5*$base);
}

To position the third face in the back, we rotate it by 180° (90° more than the previous face) around the y axis so that its z axis now points towards the back. Then we translate it by half the base in this new positive direction of the z axis (away from us):

.prism__face:nth-child(3) {
  transform: rotateY(180deg) translateZ(.5*$base);
}

You may wonder why we aren't simply translating this face back by half the base — a translateZ(-.5*$base). Well, we could do that, but then we'd have a number of problems:

  • the code wouldn't follow a pattern
  • the front of the face would be on the inside of the bar
  • the face would be invisible from the outside of the bar, because its back is on the outside since we have set backface-visibility: hidden on it

To position the fourth face on the left, we rotate it by 270° (90° more than the previous face) around the y axis, this way making its z axis point left now. And then we translate it left (in the new positive direction of the z axis) by half the base:

.prism__face:nth-child(4) {
  transform: rotateY(270deg) translateZ(.5*$base);
}

To position the fifth face at the top, we rotate it by 90° around the x axis so that its z axis points up, then translate it in the new positive direction of the z axis (up) by half the height:

.prism__face:nth-child(5) {
  transform: rotateX(90deg) translateZ(.5*$height);
}

To position the sixth face at the bottom, we first rotate it by -90° around its x axis, making its z axis point down, then translate it down, towards the + of the rotated z axis, by half the height:

.prism__face:nth-child(6) {
  transform: rotateX(-90deg) translateZ(.5*$height);
}

The demo below illustrates how this works:

See the Pen position faces v#2 by Ana Tudor (@thebabydino) on CodePen.

All right, now we have a prism. But, that code for the faces doesn't look good; it's way too repetitive. And, if we change a bit what we have for the first face, we can see a pattern for the first four faces:

.prism__face:nth-child(1) { /* 1 = 0 + 1 */
  transform: rotateY(  0deg) translateZ(.5*$base);  /*   0deg = 0*90deg */
}
.prism__face:nth-child(2) { /* 2 = 1 + 1 */
  transform: rotateY( 90deg) translateZ(.5*$base);  /*  90deg = 1*90deg */
}
.prism__face:nth-child(3) { /* 3 = 2 + 1 */
  transform: rotateY(180deg) translateZ(.5*$base);  /* 180deg = 2*90deg */
}
.prism__face:nth-child(4) { /* 4 = 3 + 1 */
  transform: rotateY(270deg) translateZ(.5*$base);  /* 270deg = 3*90deg */
}

In general, we can write the code for these first faces (the lateral ones) as:

.prism__face:nth-child(#{$i + 1}) {
  transform: rotateY($i*90deg) translateZ(.5*$base);
}

... where $i is 0, 1, 2 or 3.

But what about the last two faces? Well, we can notice a pattern for these too:

.prism__face:nth-child(5) { /* 5 = 4 + 1 */
  transform: 
    rotateX( 90deg) translateZ(.5*$height); /*  90deg =  1*90deg = pow(-1, 4)*90deg */
}
.prism__face:nth-child(6) { /* 6 = 5 + 1 */
  transform: 
    rotateX(-90deg) translateZ(.5*$height); /* -90deg = -1*90deg = pow(-1, 5)*90deg */
}

In general, we can write the code for the last faces (the base ones) as:

.prism__face:nth-child(#{$i + 1}) {
  transform: rotateX(pow(-1, $i)*90deg) translateZ(.5*$height);
}

... where $i is 4 or 5.

Now we can combine these two variations (the one for the lateral faces and the one for the base faces) with the Sass if() function:

.prism__face:nth-child(#{$i + 1}) {
  transform: 
    if($i < 4, rotateY($i*90deg), rotateX(pow(-1, $i)*90deg) 
    translateZ(.5*if($i < 4, $base, $height));
}

Or, if we don't want to have two ternaries with the same condition:

$j: if($i < 4, 1, 0); // $j is 1 if $i is less than 4 and 0 otherwise
$k: 1 - $j; // $k is 0 if $j is 1 and 1 otherwise

.prism__face:nth-child(#{$i + 1}) {
  transform: 
    rotate3d($j, $k, 0, ($j*$i + $k*pow(-1, $i))*90deg) 
    translateZ(.5*($j*$base + $k*$height));
}

Now all we need to do is put one of these two versions for the generic face into a loop which will generate the code for all six of them:

.prism__face {
  @for $i from 0 to 6 {
    &:nth-child(#{$i + 1}) {
      transform: 
        if($i < 4, rotateY($i*90deg), rotateX(pow(-1, $i)*90deg) 
        translateZ(.5*if($i < 4, $base, $height));
    }
  }
}

All right, we're done with creating the bars! We can see the result in the following Pen:

See the Pen Möbius 6hedrons - step2 by Ana Tudor (@thebabydino) on CodePen.

Hmmm, this doesn't look much different from what we had before. This is because of our point of view. Here, if we were to connect it to the middle of the screen, this line would be perpendicular onto the front face, the one that we see the biggest (so it obstructs all other faces), while in the demos above explaining the distribution, the point of view is a bit higher up and to the right, which is why we could also see the top and right faces there. However, what we have here is 3D. This becomes obvious if we rotate the bar (which can be done by dragging in the demo above), or if we change the point of view.

Changing the point of view with CSS is done via the perspective-origin property. Just like the perspective property, this is set on the scene element (the <body> in our case). Its default value is 50% 50% (dead in the middle of the scene element). Dragging up and down in the demo below changes the second value (the y) value of perspective-origin and therefore, our point of view, so the top or the bottom value become visible.

See the Pen Möbius 6hedrons - step2b (perspective-origin) by Ana Tudor (@thebabydino) on CodePen.

Distributing the Bars

This is actually really similar to distributing the lateral faces on the bar. We rotate the bar positioners around the y axis and then translate them along the z axis in the positive direction. Except now we don't take 90° steps. Instead, our step is $base-angle (which we have earlier computed to have a nice round value of 15°), and our z translation distance is the radius of the circle we distribute the bars on. So the code is:

.positioner {
  @for $i from 0 to $n-prisms {
    &:nth-child(#{$i + 1}) {
      transform: rotateY($i*$base-angle) translateZ($radius);
    }
  }
}

This demo illustrates how the bar positioning works:

See the Pen position prisms by Ana Tudor (@thebabydino) on CodePen.

This pen shows where we are with the code we have written so far — it's starting to look like something!

See the Pen Möbius 6hedrons - step 3 by Ana Tudor (@thebabydino) on CodePen.

However, there are a couple of problems here. First of all, the bars aren't vertical in the original GIF. In order to get them into the right position, we need to rotate the positioners around the x axis — let's say by 70°. Our generic transform chain becomes:

transform: rotateY($i*$base-angle) translateZ($radius) rotateX(70deg);

We can see things look now better:

See the Pen Möbius 6hedrons - step 3b by Ana Tudor (@thebabydino) on CodePen.

But in the original GIF, we see the bars a bit from above. We have two options for achieving that effect.

The first one would be to simply rotate the entire assembly such that we bring its front half a bit down and its back part a bit up. This would mean a rotation of a negative angle around the x axis — let's say -30°:

.assembly { transform: rotateX(-30deg); }

Our second option would be to add a perspective-origin on the <body>. Seeing the assembly slightly from above would mean decreasing the y component of the perspective-origin value — we need to make it lower than 50%. However, % values, or any other values measured from the top, aren't a good idea because when we use them, how we see the bars depends on the height of the scene. To illustrate this better, take a look at the following figure:

Everything is identical for these three cases except the height of the scene (you can check it out live in this pen. But, since the y component of the perspective-origin is measured from the top and each cube is positioned right in the middle of its scene, our point of view is different for different scene heights. Therefore, the way we see a cube depends on the height of the scene it is in.

In our case, the scene height isn't fixed but rather it's the height of the viewport. If we resize the viewport, then we see the bars differently depending on the perspective-origin value that's relative to the top of the scene.

The solution I often use in such situations is subtracting a fixed number of px or em from the initial 50% value inside a calc() function — something like this:

perspective-origin: 50% calc(50% - 32em);

The image below illustrates how things look with such a solution (and you can check it out live in this pen).

Note that if the cubes in the example above or the assembly in the demo we're working on weren't positioned at 50% from the top, but at 30vmax, then we would have something like calc(30vmax - 32em). The key takeaway here is that we need to set a perspective-origin that's relative to the central point of the object we wish to see in the same way regardless of the dimensions of the scene.

Now let's look at the first frame of the original GIF again:

First frame of the inspiration gif

We'll now try to tweak each of these two options to see which can get closer to the image above.

Is it the assembly rotation method?

See the Pen Möbius 6hedrons - step 3c by Ana Tudor (@thebabydino) on CodePen.

Or the one changing the point of view? Sadly, this demo doesn't work properly in Firefox.

See the Pen Möbius 6hedrons - step 3d by Ana Tudor (@thebabydino) on CodePen.

It seems like the first option —rotating the entire assembly— manages to get closer, so we'll go for that.

See the Pen Möbius 6hedrons - step 3e by Ana Tudor (@thebabydino) on CodePen.

Note that there are more things that could be tweaked in either of the two cases and it is possible that a certain combination of bar dimensions, perspective and perspective-origin on the scene gets closer to the original than simply rotating the assembly. My main objective during live coding was to be quick so I went for whatever looked better in that moment and the one that required the fewest number of changes to the values previously set.

Shading the bars

There are a few other things that don't look right yet.

First, the right and left faces should be darker than the front and back one. This should be easy to fix with the proper nth-child selectors by decreasing the brightness of the selected faces with a brightness() filter:

.prism__face:nth-child(-n+4):nth-child(even) {
  filter: brightness(.7); /* value < 1 decreases brightness */
}

Second, bars around the circle should have different shades, the laterals going from purple in the front to some kind of orange in the back. So, going around the circle from to 180°, the laterals of the bars go from purple to orange and, from 180° to 360°, they go from orange back to purple.

Shading around the circle

This sounds like a job for the Sass mix() function! In our case, the weight would go from 100% (100% the first shade, purple, the rest up to 100%, so 100% - 100% = 0% the other shade, orange) to 0% (0% purple, 100% orange) in the [0°, 180°] interval and then it would grow from 0% to 100% in the [180°, 360°] interval. Well, this is the cosine function! Sort of...

We can see below the graph of the cosine function for the [0°, 360°] interval. As the angle goes from to 180°, the value of the cosine goes from 1 to -1. For an angle going from 180° to 360°, the value of the cosine goes from -1 to 1.

See the Pen cos(θ) graph by Ana Tudor (@thebabydino) on CodePen.

If we add 1, the entire graph shifts up by one unit, its maximum being 2 and its minimum 0.

See the Pen 1 + cos(θ) graph by Ana Tudor (@thebabydino) on CodePen.

Next, if we multiply it all by 50%, the graph goes from 100% to 0% on the [0°, 180°] interval and back to 100% again on the [180°, 360°] interval, which is exactly what we wanted.

See the Pen (1 + cos(θ))*50% graph by Ana Tudor (@thebabydino) on CodePen.

So the weight in the mix function for bar $i is (1 + cos($i*$base-angle))*50%. We create a simple mixin to make things easier:

@mixin mix-me($c, $k) {
  background: mix(nth($c, 1), nth($c, 2), $k);
}

And then we use it inside the positioner loop:

.positioner {
  @for $i from 0 to $n-prisms {
    $curr-angle: $i*$base-angle; // save this so we don't compute it twice
    $k: (1 + cos($curr-angle))*50%;
    
    &:nth-child(#{$i + 1}) {
      transform: rotateY($curr-angle) translateZ($radius) rotateX(70deg);
      
      .prism__face {
        @include mix-me($lat-c, $k);
        
        &:nth-child(n + 5) { @include mix-me($base-c, $k); }
      }
    }
  }
}

The result of the changes we have made in this section can be seen in this pen:

See the Pen Möbius 6hedrons - step 4 (shading) by Ana Tudor (@thebabydino) on CodePen.

Animating the bars

This is probably the simplest part of all. Every bar rotates counter-clockwise by half a turn around the x axis. Then, it's stationary for a little while, then the whole thing repeats. It takes a bit of tweaking to get a percentage that feels right — we settle for 75% here. For the timing function, we could use the plain old ease-in-out, or we could try a symmetrical one from easings.net. My first choice for symmetrical easing is easeInOutCubic because it seems to me it makes the animation feel closest to natural motion in most cases.

@keyframes rot {
  75%, 100% { transform: rotateX(-.5turn); }
}

.prism {
  animation: rot $t ease-in-out infinite;
}

This animation can be seen in the following Pen:

See the Pen Möbius 6hedrons - step 5 (animation) by Ana Tudor (@thebabydino) on CodePen.

However, there's a problem here: all bars rotate at the same time. This means we need to set a different animation-delay for each bar. We use negative delays so that all animations are already started at the 0 moment.

.positioner {
  @for $i from 0 to $n-prisms {
    &:nth-child(#{$i + 1}) {
      .prism { animation-delay: -$i*$t/$n-prisms; }
    }
  }
}

And this gives us the final result!

See the Pen Möbius 6hedrons (pure CSS) by Ana Tudor (@thebabydino) on CodePen.

Final words

Now you know how I coded something that looks insanely complicated in just 30 minutes. You would probably be faster, because I have never been able to learn how to type with more than one finger!