Frontend Challenge Accepted: CSS 3D Cube

Cover for Frontend Challenge Accepted: CSS 3D Cube

Topics

Share this post on


Translations

If you’re interested in translating or adapting this post, please contact us first.

Do you like challenges? Are you willing to take on a task that you’ve never come across before, and do it under a deadline? What if, in carrying out the task, you encounter a problem that appears unsolvable? I want to share my experience of using CSS 3D effects for the first time in a real project and to inspire you to take on challenges.

It was an ordinary day when Eugene (manager from CreativePeople) wrote to me. He sent me a video and explained that he was developing a concept for a new project and was wondering if it were possible to develop something like the thing on the video.

It was a 3D object (a cuboid, to be precise), which rotated around one of the axes. I had already had some experience of working with CSS 3D, and a solution started to appear in my mind. I googled keywords like “CSS 3D cube” to confirm my thoughts and answered Eugene that it was possible.

Eugene’s next question was whether I would accept this project? It is worth noting that I like tricky tasks so I couldn’t refuse. At the time, I didn’t realize what I was getting into, but I was beyond determined.

Sharpen the axes

Let me remind you about browser axes. Not the war axes, but the number lines. The same as axes in the three-dimensional Cartesian coordinate system, which we all studied at school.

The Cartesian coordinate system for a three-dimensional space is an ordered triplet of lines (axes) that are pair-wise perpendicular, have a single unit of length for all three axes and have an orientation for each axis.
Wikipedia: Cartesian coordinate system

The picture below shows how the axes are directed in a browser.

A right-handed three-dimensional Cartesian coordinate system with the Z-axis pointing towards the viewer.

A right-handed three-dimensional Cartesian coordinate system with the Z-axis pointing towards the viewer. (Image credit: Wikimedia Commons)

The X-axis is horizontal, the Y axis is vertical, and the Z-axis comes out from inside the screen. The Z-axis zero value is in the plane of the screen. Remember this fact.

Clear up the perspective

To create a 3D object, I needed an element (let’s call it “scene”) with a perspective. A perspective is the depth of the scene, and it depends on the sizes of the objects it contains.

.scene {
  perspective: 800px;
}

If the perspective is too small, objects can be distorted. And if it is too big, the 3D effect will be reduced to nothing.

Furthermore, there is only one angle of view for all objects in the scene. And the 3D effect depends on the viewpoint position.

So, how to calculate the perspective? I discovered that it depended on the axis of rotation. For the X-axis, it was the height value multiplied by 4 that would fit, and for the Y-axis—the width value multiplied by 4. Here is my magic formula:

  const perspective = dimension * 4;

Considered from all sides

After determining the perspective, I started to create a 3D object. I chose a cube because it is straightforward and predictable. A cube element is a regular div with the width and height defined (say, 200px) and relative positioning used. It transforms into a 3D object through the transform-style property with the value preserve-3d. It tells the browser to render all nested elements according to the rules of the 3D world.

In my case, the cube has six divs (“sides”) with absolute positioning. The class names correspond to the initial position of the sides (back, left, right, top, bottom, front). Here is the markup:

<div class="scene">
  <div class="cube">
    <div class="side back"></div>
    <div class="side left"></div>
    <div class="side right"></div>
    <div class="side top"></div>
    <div class="side bottom"></div>
    <div class="side front"></div>
  </div>
</div>

By default, all the sides are in one plane, so I needed to rearrange them. Here is how it looks:

The resulting CSS:

.cube {
  position:relative;
  width: 200px;
  height: 200px;
  transform-style: preserve-3d;
}
.side {
  position: absolute;
  width: 200px;
  height: 200px;
}
.back {
  transform: translateZ(-100px);
}
.left {
  transform: translateX(-100px) rotateY(90deg);
}
.right {
  transform: translateX(100px) rotateY(90deg);
}
.top {
  transform: translateY(-100px) rotateX(90deg);
}
.bottom {
  transform: translateY(100px) rotateX(90deg);
}
.front {
  transform: translateZ(100px);
}

To rotate the cube, I set the transform property of the cube element, with a value of rotation along the X-axis to the arbitrary angle:

.cube {
  transform: rotateX(42deg);
}

Overcome the shortcomings

According to the task, I was to rotate the cube only along the X-axis, so I didn’t need the left and the right sides. I added captions coincident with the initial positions of the remaining sides.

I started to rotate the cube and found that the captions on the bottom and back sides are displayed upside down:

To solve this problem, I rotated each of this sides along the X-axis by 180 degrees more:

.back {
  transform: translateZ(-100px) rotateX(180deg);
}
.bottom {
  transform: translateY(100px) rotateX(270deg);
}

Go beyond the screen

I started to fill the sides with real content and immediately faced another problem. I needed to have 1px dotted lines displayed, but they were blurry and looked bad.

Soon, I realized what the problem was. Do you remember that 3D TV ad when the image extends beyond the screen? It was something like that with my cube.

If you could look at the cube from the left or the right side, you would see that its center is on the screen plane (zero on the Z-axis) and the front side is out of the screen. Therefore, it increases visually and blurs.

To solve this problem, I shifted the cube along the Z-axis to place the front side into the plane of the screen:

.cube {
  transform:translateZ(-100px);
}

Here is the almost ready cube:

Use the magic numbers

I guess you’ve noticed I used the magic number 100 to shift sides along the axis. 100 is exactly half the height of my test cube. Why half the height? Because, this is the radius of the circle inscribed in a side of the cube (which is a square, apparently).

const offset = dimension / 2;

If I needed to rotate a triangular prism, the circle would be inscribed on a triangle. In this case, the calculation formula of the offset would be as follows:

const offset = dimension / (2 * Math.sqrt(3));

Blow away the cube

To consider the task complete, I had to test the result in different browsers.

The picture I saw in the Internet Explorer plunged me into depression. To get an idea of what I’m talking about, have a look at the example below in your favorite browser. I changed one property that had resulted in the cube displayed wrong in the Internet Explorer. However, don’t peek into the source code until you have read the paragraph under the example.

The fact is that Internet Explorer doesn’t support the property transform-style with the value preserve-3d. I learned about this, looking at my favorite caniuse.com (see Notes #1). In the example above, I replaced preserve-3d to flat. Already know that? Hey, I told you not to peek.

So, I was upset but wasn’t going to give up. A problem is a chance to learn something new. Besides, I had accepted the challenge.

Search for the fulcrum

I was looking for a way to create a 3D object without using transform-style: preserve-3d and discovered a useful property: transform-origin. It is the central point of element transformation. I have created an interactive example which will help you understand how it works:

The 3D rotation of an element in the example is very similar to the front side of a cube, isn’t it? That’s what I used.

By the way, didn’t you try to select the checkbox backface-visibility: hidden during 3D rotation?
Note, this property is used to hide the back of the element during 3D transformation.

Start all over again

I started to redo the cube. I didn’t have to interact with the scene as a whole, so I removed the perspective property of the scene element. And… added it into each 3D transformation, because now every element transforms independently. Also, I set new properties for each side—transform-origin with a value equal to the position of the cube center and backface-visibility: hidden. That is how the styles have changed:

.scene {

}
.cube {
  position: relative;
  width: 200px;
  height: 200px;
  transform: perspective(800px) translateZ(-100px);
}
.side {
  position: absolute;
  transform-origin: 50% 50% -100px;
  backface-visibility: hidden;
}

I had to put the sides to the right places. Because of the transform-origin property, I didn’t need to shift them, only to rotate around the axes. It’s like magic! Let’s see how it works:

Here is the CSS for the sides placement:

.back {
  transform: perspective(800px) rotateY(180deg);
}
.top {
  transform: perspective(800px) rotateX(90deg);
}
.bottom {
  transform: perspective(800px) rotateX(-90deg);
}
.front {
  transform: perspective(800px);
}

And here you can see the new cube in action:

Give back to Caesar what is Caesar’s

The second cube looks and spins the same as the first. But in this case, you need to transform each side individually. It might not be too easy. Especially if you want to control the intermediate angle of rotation.

Furthermore, if you opened the example in Chrome, you could see that the sides flashed during the rotation. And it’s very frustrating.

In the end, I applied both approaches using the simple test of the transform-style: preserve-3d. The first cube is the default one. And the second cube is for Internet Explorer and browsers that do not support the preserve-3d.

Use the power of Math

Finally, I had to implement Parallax. Usually, it reacts to the user’s actions. It can be the position of the mouse cursor or the scrollbar. But this time, the effect depended on the angle of rotation.

So, what data I had. First, the starting and ending points of the caption position or, to put it simple, its offset up and down from the center of a side. Second, the angle of rotation of the cube.

I spent hours trying to develop a formula, and then it dawned on me. That’s what I remembered:

Graphs of the sine and cosine functions

Graphs of the sine and cosine functions. (Image credit: Wikimedia Commons)

With the help of sines and cosines, I easily calculated the offset of each caption depending on the angle. Here are the formulas I got:

const front_offset = offset * sin(angle) * -1;
const bottom_offset = offset * cos(angle);
const back_offset = offset * sin(angle);
const top_offset = offset * cos(angle) * -1;

Sum it up

The task is complete, and now I can enjoy the result and share it with you. Click this link to see how it works. Use the scroll or arrow keys to rotate the promo block. Also, try to pull up and down the black triangle on the right to control the angle of rotation manually (unfortunately, this feature does not work in Internet Explorer). Looks pretty good, doesn’t it? Besides, the performance is rather high (about 60fps).

I am very glad to have participated in the development of this site. I have gained the useful experience of using CSS 3D and have discovered many new interesting properties. But most importantly, I have realized one should never give up. Most likely, you can find another way to accomplish the task.

I hope you have enjoyed my story and are now ready for new challenges.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.

How can we help you?

Martians at a glance
17
years in business

We transform growth-stage startups into unicorns, build developer tools, and create open source products.

If you prefer email, write to us at surrender@evilmartians.com