Easily manage projects with monday.com

*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.

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.

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.

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.

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.

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.

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.

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:

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 `0°`

to `180°`

, the laterals of the bars go from purple to orange and, from `180°`

to `360°`

, they go from orange back to purple.

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 `0°`

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!

Very nice in depth post. Although I don’t think it’s the typing speed that determines how fast you can write this!

It seems like this exercise would be great recorded as a video while in professor mode. It essentially offers a practical explanation to all of the 3D possibilities that CSS can offer.

Am I the only one feeling like a newb right now?

Nope you are not alone. :p

This actually

isinsanely complicated (for normal people, at least)! XOI somehow barely managed to understand the various steps, but that’s it.

I guess that’s the difference between

understandingmath andknowingmath – using the cosine function like that is a proof of that. I know I could never achieve that expertise even in a million years.But still now I feel I understand way better how 3D works in CSS: for example, I thought the

`translateZ()`

function could only move things towards the front or the back – this article showed me it can be used together with`rotateX/Y`

to move them in other directions.And also it shed some light to the perspective properties, and the different methods to rotate a scene.

As always, infinite kudos to Ana. Thanks a billion.

Nope. Me too.

Ha, just take two years of electronics subjects and you don’t even need to pay much attention, it ends up drilled into your brain by repetition eventually. Not kidding. A lot of people bitched and moaned about how subjects like DSP were so heavy on the Maths problem solving side, but when you go through the same old, same old so many times, it just sticks…

Haha, no you aren’t. “Surprisingly simple” my butt. SMH.

Definitely not alone.

I can’t event call myself a web developer after reading this, great job and an excellent post!

Thanks so much for this article. It really explains things thoroughly and well. Now I just need to go learn more about SASS functions.

It’s an honour to have one of my old GIFs appear on CSS tricks! This is a really fascinating article, I had no idea that CSS could allow for this kind of complexity. Thanks for posting.

amazing >_<

This is so awesome! Shared it!

Thanks for sharing. This is inspirational!

This is purely epic, also very good and detailed tutorial.

keep up the good work.

Fascinating! I wish I was better at maths, and SASS, and CSS, and…

Keep up the awesome work!

Absolutely brilliant post! Thanks for taking the time to explain it all in such detail (which, I’m sure took more than 30 minutes to do :-) You really show off three great skills: your implementation of CSS, your understanding of math and 3D coordinate systems, and your ability to explain complex concepts clearly.

This is really neat! When you first mentioned 30 minutes, I was pretty skeptical, but once you broke it down, I could see how it’s doable in 30 minutes.

See, the most interesting part about doing 3D with HTML and CSS is that the DOM basically becomes your scene graph, and CSS becomes your means to transforming that scene graph, similar to how you’d typically be working with a scene graph in any other 3D framework. Granted, it’s a bit of a limited scene graph where every polygon must be represented by a node (an HTML element), but a scene graph none the less. (Well, I suppose you could represent 3 faces at once using a single element with :before and :after.)

And then you get all the cool, useful scene graph benefits, like parent-child coordinate systems. Need a bunch of moving cubes? Just build one cube in the local coordinate system (6 elements, or 2 with :before and :after), duplicate it into a bunch of parent elements, and move the parents to move the cubes. Need to rotate the entire assembly of moving cubes? Throw all the parents into another ancestor, then rotate that!

What’s funny to me, is that, chances are, if you took a 3D game developer and a front-end web developer, the two might not see any overlap in technologies whatsoever. And, yet, there is

so muchoverlap: JavaScript for in-game scripting, UI coding, even the idea of a 3D scene graph.All we need now is for someone to take a JavaScript 3D engine (e.g. cannon.js) and build a simple 3D web-based physics game without using WebGL! Here’s a quick example I found: http://davetayls.me/blog/2013/06/04/3d-css-and-physics-with-cannonjs/

Anyways, I digress. Really cool demo!

three.js is also a 3D Engine that can handle 3D with transform properties !

Check out this amazing demo : http://mrdoob.github.io/three.js/examples/css3d_periodictable.html

very impressive tutorial like usual :)

as I am lazy, if i had to do a gif animation i will do that with Blender “animation node adddon” https://www.youtube.com/watch?v=r-c7B0cBqEE&list=LLHvd290AgsBrWEUu_mYzM9g&index=19

Awesome demo. I don’t know if I’m more impressed with the final product or how clearly you explained everything.

I like how you simplified/abstracted the repetitive declarations. I’ve been trying to get better at it, and it’s nice to see other developer’s examples.

Wow! Amazing!

Really, truly nice. Following you on Codepen, posthaste.

This is amazing! So much knowledge packed into one great post. I love the fact you also gave simplified working examples for each concept. Way above my skillz but appreciated nonetheless.

You are a master!

Brilliant article, very humbling.

Took me longer than 30 minutes to just read it!

I think one closing curly brace is missing from the alog-ifying of the lateral faces positioning.

Also, I got a little lost during the colour shading part.

Still trying to decode and understand this terrific post.

Oops, true! Thanks, fixed now.

Wow, absolutely stunning. Thanks for the detailed explanation.

So impressive … moebius alive … so impressive

8-O

good job! I like it!

Cheera, that was brilliant ! Made me want to go more in depth in displaying DOM elements in 3D.

That’s really amazing, soon we will have semantically correct, rdf-compatible pages that will be able to be displayed in 3D when on desktop and in 2D when on mobile. Or the opposite. What a time to be alive ;)

Tremendo trabajo Ana!

this is why i love math!

Wow! That was awesome!