- CSS in 3D: Learning to Think in Cubes Instead of Boxes
- By Jhey Tompkins
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: Hoarfroster
- Reviewer:
My path to CSS was a bit unorthodox. I didn’t start out as a front-end developer, but as a Java developer. In fact, my earliest memories of CSS are of picking colors in Visual Studio.
It wasn’t until later that I started to work out and find my love for the front end. Exploring CSS will come later. By the time it’s fully in my world, it’s probably time for CSS3 to flourish. 3D and animation are the cool kids on the block, and they almost shaped my learning of CSS. They caught my eye and shaped (pun intended) me, making understanding CSS more important than other things like layout, color, etc.
I mean I’m working on CSS 3D every minute of the day. As with anything you spend a lot of time on, you’ll end up perfecting your processing power over the years as you hone this skill. This article describes how I’m currently working with CSS 3D and introduces some tips and tricks that might help you!
Codepen jh3y/mLaXRe
Everything is rectangular
For most cases in 3D, we can use cuboids. We can certainly create more complex shapes, but they usually require more consideration. The rounded and curved parts are particularly difficult, and there are some tricks for dealing with them (we’ll get into that later).
We’re not going to show you how to make cuboids in CSS. You can refer to Ana Tudor’s post or check out this screenshot I made of making cuboids.
The core here is that we need to wrap our cuboid with one element, and then transform six elements in it, so that each element acts as one side of our cuboid. Applying transform-style: Preserve-3D is important, and it’s not a bad idea to apply it everywhere. When shapes become more complex, we will most likely work with nested cuboids. Switching between browsers to try to debug a missing transform-style can be painful.
* {
transform-style: preserve-3d;
}
Copy the code
Codepen jh3y/QWELPQg
For our 3D creation, it’s much more than just the usual shapes. Try to imagine an entire scene made of cuboids. For example, let me make a real example. Consider rendering a 3D book on a page with four cuboids. One front and one back cover, one spine and one page. Use background-image to do the rest for us.
Codepen jh3y/ZEOzNbm
Set the scene
We’re going to use rectangles like Legos. However, we can make our lives a little easier by setting up scenes and creating planes. That plane is where our creation sits, making it easier to rotate and move the whole creation.
Codepen jh3y/pobzmNx
For me, when I create a scene, I like to rotate it on the X and Y axes first. And then I’m going to rotateX it flat. This way, when I want to add a new cuboid to the scene, I can add the new element directly to the flat element. The other thing I’m going to do here is set position: absolute on all cuboids.
.plane {
transform: rotateX(calc(var(--rotate-x, -24) * 1deg)) rotateY(calc(var(--rotate-y, -24) * 1deg)) rotateX(90deg) translate3d(0.0.0);
}
Copy the code
Start with sample
Creating rectangles of various sizes on a flat surface results in a lot of repetitive code for each creation. For this reason, I use Pug to create my cuboids through mixins. If you’re not familiar with Pug, I’ve written a 5 minute introduction.
A typical scenario would look like this:
//- Front
//- Back
//- Right
//- Left
//- Top
//- Bottom
mixin cuboid(className)
.cuboid(class=className)
- let s = 0
while s < 6
.cuboid__side
- s++
.scene
//- Plane that all the 3D stuff sits on
.plane
+cuboid('first-cuboid')
Copy the code
As for CSS, the style code for my cuboid class currently looks like this:
.cuboid {
/* Default style */
--width: 15;
--height: 10;
--depth: 4;
height: calc(var(--depth) * 1vmin);
width: calc(var(--width) * 1vmin);
transform-style: preserve-3d;
position: absolute;
font-size: 1rem;
transform: translate3d(0.0.5vmin);
}
.cuboid > div:nth-of-type(1) {
height: calc(var(--height) * 1vmin);
width: 100%;
transform-origin: 50% 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotateX(-90deg) translate3d(0.0.calc((var(--depth) / 2) * 1vmin));
}
.cuboid > div:nth-of-type(2) {
height: calc(var(--height) * 1vmin);
width: 100%;
transform-origin: 50% 50%;
transform: translate(-50%, -50%) rotateX(-90deg) rotateY(180deg) translate3d(0.0.calc((var(--depth) / 2) * 1vmin));
position: absolute;
top: 50%;
left: 50%;
}
.cuboid > div:nth-of-type(3) {
height: calc(var(--height) * 1vmin);
width: calc(var(--depth) * 1vmin);
transform: translate(-50%, -50%) rotateX(-90deg) rotateY(90deg) translate3d(0.0.calc((var(--width) / 2) * 1vmin));
position: absolute;
top: 50%;
left: 50%;
}
.cuboid > div:nth-of-type(4) {
height: calc(var(--height) * 1vmin);
width: calc(var(--depth) * 1vmin);
transform: translate(-50%, -50%) rotateX(-90deg) rotateY(-90deg) translate3d(0.0.calc((var(--width) / 2) * 1vmin));
position: absolute;
top: 50%;
left: 50%;
}
.cuboid > div:nth-of-type(5) {
height: calc(var(--depth) * 1vmin);
width: calc(var(--width) * 1vmin);
transform: translate(-50%, -50%) translate3d(0.0.calc((var(--height) / 2) * 1vmin));
position: absolute;
top: 50%;
left: 50%;
}
.cuboid > div:nth-of-type(6) {
height: calc(var(--depth) * 1vmin);
width: calc(var(--width) * 1vmin);
transform: translate(-50%, -50%) translate3d(0.0.calc((var(--height) / 2) * -1vmin)) rotateX(180deg);
position: absolute;
top: 50%;
left: 50%;
}
Copy the code
By default, it gives me something like this:
Codepen jh3y/abZorVz
Supported by CSS variables
You may have noticed that I use some CSS variables (also known as custom properties) in my CSS code. It saves me a lot of time. I used a lot of CSS variables to help build my cuboids.
--width
: Width of a cuboid on a plane--height
: Height of cuboid in plane--depth
: Depth of a cuboid on a plane--x
: X position on the plane--y
: Y position on the plane
I mainly use vmin as my sizing unit to keep the layout responsive. If I’m creating something that needs to be scaled, I might create a responsive unit. We mentioned this technique in our last article. I flattened the shape again and now I can call my cuboid a shape with height, width, and depth. The following illustration shows how we can change the size of a cuboid by moving it on a flat surface.
Codepen jh3y/BaKqQLJ
Use dat.GUI for debugging
You may have noticed the small panel in the upper right corner of some of our demos. It is dat. GUI. This is a lightweight JavaScript controller library that is useful for debugging CSS 3D. Without too much code, we can set up a panel that allows us to change CSS variables at run time. One thing I like to do is use panels to rotate planes on the X and Y axes. This way, we can see how things line up or work on parts we might not see at first.
const {
dat: {GUI},
} = window
const CONTROLLER = new GUI()
const CONFIG = {
'cuboid-height': 10.'cuboid-width': 10.'cuboid-depth': 10.x: 5.y: 5.z: 5.'rotate-cuboid-x': 0.'rotate-cuboid-y': 0.'rotate-cuboid-z': 0,}const UPDATE = () = > {
Object.entries(CONFIG).forEach(([key, value]) = > {
document.documentElement.style.setProperty(`--${key}`, value)
})
}
const CUBOID_FOLDER = CONTROLLER.addFolder('Cuboid')
CUBOID_FOLDER.add(CONFIG, 'cuboid-height'.1.20.0.1)
.name('Height (vmin)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'cuboid-width'.1.20.0.1)
.name('Width (vmin)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'cuboid-depth'.1.20.0.1)
.name('Depth (vmin)')
.onChange(UPDATE)
/ / you have a choice here, you can use the x | | y
// Or use a standard transform with vmin
CUBOID_FOLDER.add(CONFIG, 'x'.0.40.0.1)
.name('X (vmin)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'y'.0.40.0.1)
.name('Y (vmin)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'z', -25.25.0.1)
.name('Z (vmin)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'rotate-cuboid-x'.0.360.1)
.name('Rotate X (deg)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'rotate-cuboid-y'.0.360.1)
.name('Rotate Y (deg)')
.onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'rotate-cuboid-z'.0.360.1)
.name('Rotate Z (deg)')
.onChange(UPDATE)
UPDATE()
Copy the code
If you watch the time-lapse video in this tweet. You’ll notice that I rotate the plane a lot while building the scene.
A tweet from @jH3YY
Dat.GUI code is a bit repetitive. We can create a function that accepts the configuration and generates the controller, but it needs to be modified to suit your needs. I started with a dynamically generated controller in this demo.
In the middle
You may have noticed that, by default, each cuboid is located in the lower and upper half of the plane. This is something I’ve deliberately set up and something I’ve only recently started doing. Why is that? Because we want to use the inclusion elements of the cuboid as the center of the cuboid, this will make animation easier. In particular, if we think about rotation around the z-axis. I discovered this when I created the CSS is Cake. After making the cake, I decided to make each slice interactive, and then I had to go back and change my implementation to fix the rotation center of the flipped slice.
Codepen jh3y/KKVGoGJ
Here, I split the presentation center and the offset center to show how these affect the effect.
Codepen jh3y/XWKrLwe
positioning
If we’re dealing with a more complex scenario — we might break it up into different parts. This is where the concept of subplanes comes in handy. Consider this demo, in which I recreate my personal workspace.
A tweet from @jH3YY.
There’s so much going on here, it’s hard to keep track of all the cuboids. To do that, we can introduce subplanes. Let’s break down that demo. The chair has its own subplane. This makes it much easier to move and rotate it around the scene — among other things — without affecting anything else. In fact, we can even rotate the top of the chair without moving the feet!
Codepen jh3y/QWELerg
aesthetic
Once we have a structure, it’s time to look at aesthetics. It all depends on what you’re doing. But you can get some quick wins by using certain techniques. I tend to start by making things “ugly” and then go back and create CSS variables for all colors and apply them. The three shadows of a particular object allow us to visually distinguish the sides of cuboids. Considering this toaster example, let’s cover the side of the toaster with three tones:
Codepen jh3y/KKVjLrx
Our previous Pug Mixin allowed us to define class names for cuboids. Applying color to one side usually looks like this:
/* Use linear gradient to apply shimmer effect */
.toaster__body > div:nth-of-type(1) {
background: linear-gradient(120deg, transparent 10%.var(--shine) 10% 20%, transparent 20% 25%.var(--shine) 25% 30%, transparent 30%), var(--shade-one);
}
.toaster__body > div:nth-of-type(2) {
background: var(--shade-one);
}
.toaster__body > div:nth-of-type(3),
.toaster__body > div:nth-of-type(4) {
background: var(--shade-three);
}
.toaster__body > div:nth-of-type(5),
.toaster__body > div:nth-of-type(6) {
background: var(--shade-two);
}
Copy the code
It’s a little tricky to include extra elements in our Pug mixin. But let’s not forget that each side of our cuboid provides two pseudo-elements. We can use these for all kinds of details. For example, the toaster slot and the side handle slot are pseudo-elements.
Another trick is to use background-image to add detail. For example, consider a 3D workspace. We can use the background layer to create shadows. We can use actual images to create textured surfaces. The floor and carpet are duplicated background-images. In fact, using pseudo-elements for textures is great because we can transform them as needed, such as rotating tiled images. I also noticed that in some cases there was a flicker in the render when working directly with the cuboid sides.
Codepen jh3y/XWdQBRx
One of the problems with using images for textures is how we create different shadows. We need tone to distinguish between the different sides, and this is where the filter property helps. Let’s add the Brightness () filter to the different sides of the cuboid to make them bright or dark. Consider this CSS flip table where all surfaces use textured images. But to distinguish the sides, we applied a brightness filter on it.
Codepen jh3y/xJXvjP
How do we create shapes — or seemingly impossible features we want to create — with a finite set of elements? Sometimes we can trick the eye with a little smoke and mirror effect. We can provide a fake 3D feel. The Z Dog Library is a good example of this.
Consider that we now have a bundle of balloons, the ropes holding them in place use the right Angle, and each rope has its own spin, tilt, and so on. But the balloon itself is flat. If we rotate the plane, the balloon will keep rotating against the plane. This has a “fake” 3D feel to it. Try it out and fix it.
Codepen jh3y/NWNVgJw
Sometimes it takes a little out-of-the-box thinking. I was building a 3D workspace when someone suggested some houseplants. I did add some plants in my room, but my initial thought was, “No, I can make a square pot, how do I make all the leaves?” In fact, we can use some visual tricks on this as well. We can take some pictures of leaves or plants, remove the background using a tool like remove.bg, and then place many images in the same position, but rotate them each by a certain amount. Now, as they rotate, we can get the feel of a 3D plant.
Codepen jh3y/oNLNZMR
Deal with awkward shapes
Awkward shapes are difficult to cover in a generic way, and each creation has its own obstacles. However, there are several examples that can help you provide ideas for solving problems. I recently read an article about UX for the Lego interface panel. In fact, it’s not a bad idea to treat CSS 3D like lego. But the Lego interface panel is a shape we can make with CSS (minus the studs — I recently learned that’s the name of the bolts). This is a cuboid, and then we can cut the top surface, make the end transparent, and rotate a pseudo-element to connect it. We can use pseudo-elements to add some detail to the background layer. Try opening and closing wireframes in the demo below. If we want the exact height and Angle of the shape, we can use some mathematics to construct hypotenuse etc.
Codepen jh3y/PozojYe
Another awkward thing is curves – sphericity is not supported by CSS. At this point, we have several options, one is to accept this fact and create polygons with a finite number of sides, while the other is to create circles and use the rotation method we mentioned in plants. Each of these options will work. But again, it’s based on use cases, and each has its pros and cons. With polygons, we can almost get a curve if we use a lot of elements, and the latter can cause performance problems. Using perspective techniques, we might also end up with performance problems. We also gave up the ability to design the “sides” of shapes because there was no suitable method.
Codepen jh3y/wvWvqqM
Fight the Z-axis
Last but not least, fight the Z-axis. This is where some elements on the plane may overlap or cause an unwanted flicker in the rendering. It’s hard to give good examples, and there’s no universal solution. This is a matter to be resolved on a case-by-case basis. The main strategy is to sort things in the DOM as needed, but sometimes that’s not the only problem.
Being accurate can sometimes lead to problems, so let’s look again at the 3D workspace example. Think of the canvas on the wall, where the shadow is a false element. If we place the canvas right against the wall, we have a problem. If we do this, shadows and walls will compete for hierarchy on the render. To solve this problem, we can use a little translate. This will solve the problem and can effectively declare what should be put in front of it.
Codepen jh3y/PozoYWK
Try resizing this demo with Canvas Offset turned on and off. Notice how the shadows flicker without the offset? That’s because shadows and walls are competing for layers. The offset sets –x to a small length of 1vmin we’ll name –cm, the response unit used for this authoring.
This is
We can now take our CSS to another dimension — 3D. Use some of my tips, find your own tips, share them, and share your 3D creations! Yes, making 3D things in CSS can be difficult, but it’s definitely an ability we can improve as we go along. Different methods work for different people, and patience is an essential ingredient. I’d love to know what your method is!
The most important thing? Actually have fun, hahaha!
Codepen jh3y/MWeWvGO
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.