3D Shapes animated with CSS

2023-05-12

Overview

Recently, I came across a blog post by Harold Cooper on Hacker News where he explains the magic behind the animations that he included in one of his earlier blog posts. I was quite impressed by the spinning animations and immediately wanted to try it out myself.

I don't have much experience with css animations, so I thought it would be a fun opportunity to learn something new. Also, it's been a while since I did any geometry, so I thought it would be a good time for a quick refresher.

CSS Animations

CSS animations are a great way to add a bit of flair to your website. No need for JavaScript or external libraries, so your code is lighter and easier to maintain. You can use them for anything from subtle hover effects to full-blown animated experiences.

In this post, I'm going to walk through recreating some of the animations from Harold's blog post. This should serve as a foundation for future posts. Generating the coordinates for the vertices of the shapes and getting them to rotate around the y axis was more difficult than anticipated, so I'll share some rough helper functions I used and I will hopefully improve them in future.

To start us off, here is the basic css that we will be using to create the spinning animations in this post.

One of the important css properties that we will be using is transform-style: preserve-3d. This property specifies whether or not an element should be positioned in 3D space using the transform property.

.shape {
  transform-style: preserve-3d;
  animation: spin 20s linear infinite;
}
.coord {
  position: absolute;
  transform-style: preserve-3d;
}
.vertex {
  animation: un-spin 20s linear infinite;
}

We will then use the spin animation to rotate the shape around the y-axis and slightly tilt it so that it is easier to see the 3D effect.

Then we will use the un-spin animation to rotate the vertices in the opposite direction so that they appear to be stationary.

These animations and properties are also explained in Harold's blog post, so check that out if you want some additional details.

@keyframes spin {
  from {
    transform: rotateX(-0.1turn) rotateY(0turn);
  }
  to {
    transform: rotateX(-0.1turn) rotateY(1turn);
  }
}
@keyframes un-spin {
  from {
    transform: rotateY(0turn);
  }
  to {
    transform: rotateY(-1turn);
  }
}

Geometry Refresher

Like I already mentioned, it has been quite I awhile since I took a geometry class. So I had to spend some time looking up the specifics for some of the basic 3D shapes and how to generate the coordinates for their vertices. I'll go over three shapes (cube, tetrahedron, and icosaedron) and share some of the helper functions that I used to generate the coordinates for the vertices down below.

Cube / Hexahedron

So our first shape is the cube, also known as a hexahedron.

To generate the coordinates for the vertices we can simply divide the side length of the cube by 2 and then use that value to generate the coordinates for each vertex.

Here is the resulting animated shape and the helper function that I used to generate the coordinates for the vertices.

A
B
C
D
E
F
G
H
function generateCubeVertices(s) {
  const vertices = []
  const halfSide = s / 2
  for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
      for (let k = 0; k < 2; k++) {
        vertices.push([
          i ? halfSide : -halfSide,
          j ? halfSide : -halfSide,
          k ? halfSide : -halfSide,
        ])
      }
    }
  }
  return vertices
}

Pyramid / Tetrahedron

Next up we have the pyramid, also known as a tetrahedron.

To generate the coordinates for the vertices we can start by creating a triangle with side length equal to the height of the pyramid. Then we can use the midpoint of each side of the triangle to create the coordinates for the vertices.

Here is the resulting animated shape and the helper function that I used to generate the coordinates for the vertices.

A
B
C
D
function createTetrahedronVertices(size) {
  const height = Math.sqrt(2 / 3) * size
  const vertices = [
    [0, height / 3, 0],
    [size / 2, -height / 3, -size / (2 * Math.sqrt(3))],
    [-size / 2, -height / 3, -size / (2 * Math.sqrt(3))],
    [0, -height / 3, size / Math.sqrt(3)],
  ]
  return vertices
}

Icosahedron

Finally, for the last shape, we'll use the icosahedron. This shape has 12 vertices and 20 faces. It's a bit more complex than the previous shapes, but the method to generate the coordinates for the vertices is still pretty simple thanks to the fact that it can be calculated using the golden ratio.

To calculate the coordinates for the vertices we can start by calculating the golden ratio and then use that value to calculate the coordinates for each vertex.

Here's the shape and the code to generate the vertices:

function generateIcosahedron(size) {
  const t = (1 + Math.sqrt(5)) / 2 // golden ratio
  const vertices = []
  vertices.push([-size, t * size, 0])
  vertices.push([size, t * size, 0])
  vertices.push([-size, -t * size, 0])
  vertices.push([size, -t * size, 0])
  vertices.push([0, -size, t * size])
  vertices.push([0, size, t * size])
  vertices.push([0, -size, -t * size])
  vertices.push([0, size, -t * size])
  vertices.push([t * size, 0, -size])
  vertices.push([t * size, 0, size])
  vertices.push([-t * size, 0, -size])
  vertices.push([-t * size, 0, size])
  return vertices
}

Conclusion

So that's it for this post.
I hope you enjoyed it and learned something about 3D animations using CSS.
I plan to follow up with more posts on this topic, so stay tuned! Until next time, happy coding!