3d – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 16 Dec 2022 14:58:17 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 3d – CSS-Tricks https://css-tricks.com 32 32 45537868 CSS Infinite 3D Sliders https://css-tricks.com/css-infinite-3d-sliders/ https://css-tricks.com/css-infinite-3d-sliders/#comments Fri, 16 Dec 2022 14:58:08 +0000 https://css-tricks.com/?p=375621 In this series, we’ve been making image sliders with nothing but HTML and CSS. The idea is that we can use the same markup but different CSS to get wildly different results, no matter how many images we toss …


CSS Infinite 3D Sliders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In this series, we’ve been making image sliders with nothing but HTML and CSS. The idea is that we can use the same markup but different CSS to get wildly different results, no matter how many images we toss in. We started with a circular slider that rotates infinitely, sort of like a fidget spinner that holds images. Then we made one that flips through a stack of photos.

This time around, we’re diving into the third dimension. It’s going to look tough at first, but lots of the code we’re looking at is exactly what we used in the first two articles in this series, with some modifications. So, if you’re just now getting into the series, I’d suggest checking out the others for context on the concepts we’re using here.

CSS Sliders series

This is what we’re aiming for:

At first glance, it looks like we have a rotating cube with four images. But in reality, we’re dealing with six images in total. Here is the slider from a different angle:

Now that we have a good visual for how the images are arranged, let’s dissect the code to see how we get there.

The basic setup

Same HTML as the rest of the sliders we’ve used for the other sliders:

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

And once again, we’re using CSS Grid to place the images in a stack, one on top of another:

.gallery {
  display: grid;
}
.gallery > img {
  grid-area: 1 / 1;
  width: 160px;
  aspect-ratio: 1;
  object-fit: cover;
}

The animation

The logic for this slider is very similar to the circular slider from the first article. In fact, if you check the video above again, you can see that the images are placed in a way that creates a polygon. After a full rotation, it returns to the first image.

We relied on the CSS transform-origin and animation-delay properties for that first slider. The same animation is applied to all of the image elements, which rotate around the same point. Then, by using different delays, we correctly place all the images around a big circle.

The implementation will be a bit different for our 3D slider. Using transform-origin won’t work here because we’re working in 3D, so we will use transform instead to correctly place all the images, then rotate the container.

We’re reaching for Sass again so we can loop through the number of images and apply our transforms:

@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
     transform: 
       rotate(#{360*($i - 1) / $n}deg) /* 1 */
       translateY(50% / math.tan(180deg / $n)) /* 2 */ 
       rotateX(90deg); /* 3 */
  }
}

You might be wondering why we’re jumping straight into Sass. We started with a fixed number of images using vanilla CSS in the other articles before generalizing the code with Sass to account for any number (N) of images. Well, I think you get the idea now and we can cut out all that discovery work to get to the real implementation.

The transform property is taking three values, which I’ve illustrated here:

Showing the three phases of the image slider layout.

We first rotate all the images above each other. The angle of rotation depends on the number of images. For N images, we have an increment equal to 360deg/N. Then we translate all of the images by the same amount in a way that makes their center points meet on the sides.

Showing the stack of images arranged flat in a circle with a red line running through the center point of the images.

There’s some boring geometry that helps explain how all this works, but the distance is equal to 50%/tan(180deg/N). We dealt with a similar equation when making the circular slider ( transform-origin: 50% 50%/sin(180deg/N) ).

Finally, we rotate the images around the x-axis by 90deg to get the arrangement we want. Here is a video that illustrates what the last rotation is doing:

Now all we have to do is to rotate the whole container to create our infinite slider.

.gallery {
  transform-style: preserve-3d;
  --_t: perspective(280px) rotateX(-90deg);
  animation: r 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;
}
@keyframes r {
  0%, 3% {transform: var(--_t) rotate(0deg); }
  @for $i from 1 to $n {
    #{($i/$n)*100 - 2}%, 
    #{($i/$n)*100 + 3}% {
      transform: var(--_t) rotate(#{($i / $n) * -360}deg);
    }  
  }
  98%, 100% { transform: var(--_t) rotate(-360deg); }
}

That code might be hard to understand, so let’s actually step back a moment and revisit the animation we made for the circular slider. This is what we wrote in that first article:

.gallery {
  animation: m 12s cubic-bezier(.5, -0.2, .5, 1.2) infinite;
}
@keyframes m {
  0%, 3% { transform: rotate(0); }
  @for $i from 1 to $n {
    #{($i / $n) * 100 - 2}%,
    #{($i / $n) * 100 + 3}% { 
      transform: rotate(#{($i / $n) * -360}deg);
    }  
  }
  98%, 100% { transform: rotate(-360deg); }
}

The keyframes are almost identical. We have the same percentage values, the same loop, and the same rotation.

Why are both the same? Because their logic is the same. In both cases, the images are arranged around a circular shape and we need to rotate the whole thing to show each image. That’s how I was able to copy the keyframes from the circular slider and use that same code for our 3D slider. The only difference is that we need to rotate the container by -90deg along the x-axis to see the images since we have already rotated them by 90deg on the same axis. Then we add a touch of perspective to get the 3D effect.

That’s it! Our slider is done. Here is the full demo again. All you have to do is to add as many images as you want and update one variable to get it going.

Vertical 3D slider

Since we are playing in the 3D space, why not make a vertical version of the previous slider? The last one rotates along the z-axis, but we can also move along the x-axis if we want.

If you compare the code for both versions of this slider, you might not immediately spot the difference because it’s only one character! I replaced rotate() with rotateX() inside the keyframes and the image transform. That’s it!

It should be noted that rotate() is equivalent to rotateZ(), so by changing the axis from Z to X we transform the slider from the horizontal version into the vertical one.

Cube slider

We cannot talk about 3D in CSS without talking about cubes. And yes, that means we are going to make another version of the slider.

The idea behind this version of the slider is to create an actual cube shape with the images and rotate the full thing in around the different axis. Since it’s a cube, we’re dealing with six faces. We’ll use six images, one for each face of the cube. So, no Sass but back to vanilla CSS.

That animation is a little overwhelming, right? Where do you even start?

We have six faces, so we need to perform at least six rotations so that each image gets a turn. Well, actually, we need five rotations — the last one brings us back to the first image face. If you go grab a Rubik’s Cube — or some other cube-shaped object like dice — and rotate it with your hand, you’ll have a good idea of what we’re doing.

.gallery {
  --s: 250px; /* the size */

  transform-style: preserve-3d;
  --_p: perspective(calc(2.5*var(--s)));
  animation: r 9s infinite cubic-bezier(.5, -0.5, .5, 1.5);
}

@keyframes r {
  0%, 3%   { transform: var(--_p); }
  14%, 19% { transform: var(--_p) rotateX(90deg); }
  31%, 36% { transform: var(--_p) rotateX(90deg) rotateZ(90deg); }
  47%, 52% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }
  64%, 69% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg); }
  81%, 86% { transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg); }
  97%, 100%{ transform: var(--_p) rotateX(90deg) rotateZ(90deg) rotateY(-90deg) rotateX(90deg) rotateZ(90deg) rotateY(-90deg); }
}

The transform property starts with zero rotations and, on each state, we append a new rotation on a specific axis until we reach six rotations. Then we are back to the first image.

Let’s not forget the placement of our images. Each one is applied to a face of the cube using transform:

.gallery img {
  grid-area: 1 / 1;
  width: var(--s);
  aspect-ratio: 1;
  object-fit: cover;
  transform: var(--_t,) translateZ(calc(var(--s) / 2));
}
.gallery img:nth-child(2) { --_t: rotateX(-90deg); }
.gallery img:nth-child(3) { --_t: rotateY( 90deg) rotate(-90deg); }
.gallery img:nth-child(4) { --_t: rotateX(180deg) rotate( 90deg); }
.gallery img:nth-child(5) { --_t: rotateX( 90deg) rotate( 90deg); }
.gallery img:nth-child(6) { --_t: rotateY(-90deg); }

You are probably thinking there is weird complex logic behind the values I’m using there, right? Well, no. All I did was open DevTools and play with different rotation values for each image until I got it right. It may sound stupid but, hey, it works — especially since we have a fixed number of images and we are not looking for something that supports N images.

In fact, forget the values I’m using and try to do the placement on your own as an exercise. Start with all the images stacked on top of each other, open the DevTools, and go! You will probably end up with different code and that’s totally fine. There can be different ways to position the images.

What’s the trick with the comma inside the var()? Is it a typo?

It’s not a typo so don’t remove it! If you do remove it, you will notice that it affects the placement of the first image. You can see that in my code I defined --_t for all the images except the first one because I only need a translation for it. That comma makes the variable fall back to a null value. Without the comma, we won’t have a fallback and the whole value will be invalid.

From the specification:

Note: That is, var(--a,) is a valid function, specifying that if the --a custom property is invalid or missing, the var()` should be replaced with nothing.

Random cube slider

A little bit of randomness can be a nice enhancement for this sort of animation. So, rather than rotate the cube in sequential order, we can roll the dice so to speak, and let the cube roll however it will.

Cool right? I don’t know about you, but I like this version better! It’s more interesting and the transitions are satisfying to watch. And guess what? You can play with the values to create your own random cube slider!

The logic is actual not random at all — it just appears that way. You define a transform on each keyframe that allows you to show one face and… well, that’s really it! You can pick any order you want.

@keyframes r {
  0%, 3%   { transform: var(--_p) rotate3d( 0, 0, 0,  0deg); }
  14%,19%  { transform: var(--_p) rotate3d(-1, 1, 0,180deg); }
  31%,36%  { transform: var(--_p) rotate3d( 0,-1, 0, 90deg); }
  47%,52%  { transform: var(--_p) rotate3d( 1, 0, 0, 90deg); }
  64%,69%  { transform: var(--_p) rotate3d( 1, 0, 0,-90deg); }
  81%,86%  { transform: var(--_p) rotate3d( 0, 1, 0, 90deg); }
  97%,100% { transform: var(--_p) rotate3d( 0, 0, 0,  0deg); }
}

I am using rotate3d() this time but am still relying on DevTools to find the values that feel “right” to me. Don’t try to find a relationship between the keyframes because there simply isn’t one. I’m defining separate transforms and then watching the “random” result. Make sure the first image is the first and last frames, respectively, and show a different image on each of the other frames.

You are not obligated to use a rotate3d() transform as I did. You can also chain different rotations like we did in the previous example. Play around and see what you can come up with! I will be waiting for you to share your version with me in the comments section!

Wrapping up

I hope you enjoyed this little series. We built some fun (and funny) sliders while learning a lot about all kinds of CSS concepts along the way — from grid placement and stacking order, to animation delays and transforms. We even got to play with a dash of Sass to loop through an array of elements.

And we did it all with the exact same HTML for each and every slider we made. How cool is that? CSS is dang powerful and capable of accomplishing so much without the aid of JavaScript.


CSS Infinite 3D Sliders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-infinite-3d-sliders/feed/ 2 375621
Single Element Loaders: Going 3D! https://css-tricks.com/single-element-loaders-going-3d/ https://css-tricks.com/single-element-loaders-going-3d/#comments Fri, 01 Jul 2022 13:24:09 +0000 https://css-tricks.com/?p=366544 For this fourth and final article of our little series on single-element loaders, we are going to explore 3D patterns. When creating a 3D element, it’s hard to imagine that just one HTML element is enough to simulate something like all six faces of a cube. But  maybe we can get away …


Single Element Loaders: Going 3D! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
For this fourth and final article of our little series on single-element loaders, we are going to explore 3D patterns. When creating a 3D element, it’s hard to imagine that just one HTML element is enough to simulate something like all six faces of a cube. But  maybe we can get away with something more cube-like instead by showing only the front three sides of the shape — it’s totally possible and that’s what we’re going to do together.

The split cube loader

Here is a 3D loader where a cube is split into two parts, but is only made with only a single element:

Each half of the cube is made using a pseudo-element:

Cool, right?! We can use a conic gradient with CSS clip-path on the element’s ::before and ::after pseudos to simulate the three visible faces of a 3D cube. Negative margin is what pulls the two pseudos together to overlap and simulate a full cube. The rest of our work is mostly animating those two halves to get neat-looking loaders!

Let’s check out a visual that explains the math behind the clip-path points used to create this cube-like element:

We have our variables and an equation, so let’s put those to work. First, we’ll establish our variables and set the sizing for the main .loader element:

.loader {
  --s: 150px; /* control the size */
  --_d: calc(0.353 * var(--s)); /* 0.353 = sin(45deg)/2 */

  width: calc(var(--s) + var(--_d)); 
  aspect-ratio: 1;
  display: flex;
}

Nothing too crazy so far. We have a 150px square that’s set up as a flexible container. Now we establish our pseudos:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
}

Those are two halves in the .loader container. We need to paint them in, so that’s where our conic gradient kicks in:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
    #fff 135deg, #666 0 270deg, #aaa 0);
}

The gradient is there, but it looks weird. We need to clip it to the element:

.loader::before,
.loader::after {
  content: "";
  flex: 1;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
    #fff 135deg, #666 0 270deg, #aaa 0);
  clip-path:
    polygon(var(--_d) 0, 100% 0, 100% calc(100% - var(--_d)), calc(100% - var(--_d)) 100%, 0 100%, 0 var(--_d));
}

Let’s make sure the two halves overlap with a negative margin:

.loader::before {
  margin-right: calc(var(--_d) / -2);
}

.loader::after {
  margin-left: calc(var(--_d) / -2);
}

Now let’s make ‘em move!

.loader::before,
.loader::after {
  /* same as before */
  animation: load 1.5s infinite cubic-bezier(0, .5, .5, 1.8) alternate;
}

.loader::after {
  /* same as before */
  animation-delay: -.75s
}

@keyframes load{
  0%, 40%   { transform: translateY(calc(var(--s) / -4)) }
  60%, 100% { transform: translateY(calc(var(--s) / 4)) }
}

Here’s the final demo once again:

The progress cube loader

Let’s use the same technique to create a 3D progress loader. Yes, still only one element!

We’re not changing a thing as far as simulating the cube the same way we did before, other than changing the loader’s height and aspect ratio. The animation we’re making relies on a surprisingly easy technique where we update the width of the left side while the right side fills the remaining space, thanks to flex-grow: 1.

The first step is to add some transparency to the right side using opacity:

This simulates the effect that one side of the cube is filled in while the other is empty. Then we update the color of the left side. To do that, we either update the three colors inside the conic gradient or we do it by adding a background color with a background-blend-mode:

.loader::before {
  background-color: #CC333F; /* control the color here */
  background-blend-mode: multiply;
}

This trick only allows us to update the color only once. The right side of the loader blends in with the three shades of white from the conic gradient to create three new shades of our color, even though we’re only using one color value. Color trickery!

Let’s animate the width of the loader’s left side:

Oops, the animation is a bit strange at the beginning! Notice how it sort of starts outside of the cube? This is because we’re starting the animation at the 0% width. But due to the clip-path and negative margin we’re using, what we need to do instead is start from our --_d variable, which we used to define the clip-path points and the negative margin:

@keyframes load {
  0%,
  5% {width: var(--_d); }
  95%,
  100% {width: 100%; }
}

That’s a little better:

But we can make this animation even smoother. Did you notice we’re missing a little something? Let me show you a screenshot to compare what the final demo should look like with that last demo:

It’s the bottom face of the cube! Since the second element is transparent, we need to see the bottom face of that rectangle as you can see in the left example. It’s subtle, but should be there!

We can add a gradient to the main element and clip it like we did with the pseudos:

background: linear-gradient(#fff1 0 0) bottom / 100% var(--_d) no-repeat;

Here’s the full code once everything is pulled together:

.loader {
  --s: 100px; /* control the size */
  --_d: calc(0.353*var(--s)); /* 0.353 = sin(45deg) / 2 */

  height: var(--s); 
  aspect-ratio: 3;
  display: flex;
  background: linear-gradient(#fff1 0 0) bottom / 100% var(--_d) no-repeat;
  clip-path: polygon(var(--_d) 0, 100% 0, 100% calc(100% - var(--_d)), calc(100% - var(--_d)) 100%, 0 100%, 0 var(--_d));
}
.loader::before,
.loader::after {
  content: "";
  clip-path: inherit;
  background:
    conic-gradient(from -90deg at calc(100% - var(--_d)) var(--_d),
     #fff 135deg, #666 0 270deg, #aaa 0);
}
.loader::before {
  background-color: #CC333F; /* control the color here */
  background-blend-mode: multiply;
  margin-right: calc(var(--_d) / -2);
  animation: load 2.5s infinite linear;
}
.loader:after {
  flex: 1;
  margin-left: calc(var(--_d) / -2);
  opacity: 0.4;
}

@keyframes load {
  0%,
  5% { width: var(--_d); }
  95%,
  100% { width: 100%; }
}

That’s it! We just used a clever technique that uses pseudo-elements, conic gradients, clipping, background blending, and negative margins to get, not one, but two sweet-looking 3D loaders with nothing more than a single element in the markup.

More 3D

We can still go further and simulate an infinite number of 3D cubes using one element — yes, it’s possible! Here’s a grid of cubes:

This demo and the following demos are unsupported in Safari at the time of writing.

Crazy, right? Now we’re creating a repeated pattern of cubes made using a single element… and no pseudos either! I won’t go into fine detail about the math we are using (there are very specific numbers in there) but here is a figure to visualize how we got here:

We first use a conic-gradient to create the repeating cube pattern. The repetition of the pattern is controlled by three variables:

  • --size: True to its name, this controls the size of each cube.
  • --m: This represents the number of columns.
  • --n: This is the number of rows.
  • --gap: this the gap or distance between the cubes
.cube {
  --size: 40px; 
  --m: 4; 
  --n: 5;
  --gap :10px;

  aspect-ratio: var(--m) / var(--n);
  width: calc(var(--m) * (1.353 * var(--size) + var(--gap)));
  background:
    conic-gradient(from -90deg at var(--size) calc(0.353 * var(--size)),
      #249FAB 135deg, #81C5A3 0 270deg, #26609D 0) /* update the colors here */
    0 0 / calc(100% / var(--m)) calc(100% / var(--n));
}

Then we apply a mask layer using another pattern having the same size. This is the trickiest part of this idea. Using a combination of a linear-gradient and a conic-gradient we will cut a few parts of our element to keep only the cube shapes visible.

.cube {
  /* etc. */
  mask: 
    linear-gradient(to bottom right,
       #0000 calc(0.25 * var(--size)),
       #000 0 calc(100% - calc(0.25 * var(--size)) - 1.414 * var(--gap)),
       #0000 0),
    conic-gradient(from -90deg at right var(--gap) bottom var(--gap), #000 90deg, #0000 0);  
  mask-size: calc(100% / var(--m)) calc(100% / var(--n));
  mask-composite: intersect;
}

The code may look a bit complex but thanks to CSS variables all we need to do is to update a few values to control our matrix of cubes. Need a 10⨉10 grid? Update the --m and --n variables to 10. Need a wider gap between cubes? Update the --gap value. The color values are only used once, so update those for a new color palette!

Now that we have another 3D technique, let’s use it to build variations of the loader by playing around with different animations. For example, how about a repeating pattern of cubes sliding infinitely from left to right?

This loader defines four cubes in a single row. That means our --n value is 4 and --m is equal to 1 . In other words, we no longer need these!

Instead, we can work with the --size and --gap variables in a grid container:

.loader {
  --size: 70px;
  --gap: 15px;  

  width: calc(3 * (1.353 * var(--size) + var(--gap)));
  display: grid;
  aspect-ratio: 3;
}

This is our container. We have four cubes, but only want to show three in the container at a time so that we always have one sliding in as one is sliding out. That’s why we are factoring the width by 3 and have the aspect ratio set to 3 as well.

Let’s make sure that our cube pattern is set up for the width of four cubes. We’re going to do this on the container’s ::before pseudo-element:

.loader::before { 
  content: "";
  width: calc(4 * 100% / 3);
  /*
     Code to create four cubes
  */
}

Now that we have four cubes in a three-cube container, we can justify the cube pattern to the end of the grid container to overflow it, showing the last three cubes:

.loader {
  /* same as before */
  justify-content: end;
}

Here’s what we have so far, with a red outline to show the bounds of the grid container:

Now all we have to do is to move the pseudo-element to the right by adding our animation:

@keyframes load {
  to { transform: translate(calc(100% / 4)); }
}

Did you get the trick of the animation? Let’s finish this off by hiding the overflowing cube pattern and by adding a touch of masking to create that fading effect that the start and the end:

.loader {
  --size: 70px;
  --gap: 15px;  
  
  width: calc(3*(1.353*var(--s) + var(--g)));
  display: grid;
  justify-items: end;
  aspect-ratio: 3;
  overflow: hidden;
  mask: linear-gradient(90deg, #0000, #000 30px calc(100% - 30px), #0000);
}

We can make this a lot more flexible by introducing a variable, --n, to set how many cubes are displayed in the container at once. And since the total number of cubes in the pattern should be one more than --n, we can express that as calc(var(--n) + 1).

Here’s the full thing:

OK, one more 3D loader that’s similar but has the cubes changing color in succession instead of sliding:

We’re going to rely on an animated background with background-blend-mode for this one:

.loader {
  /* ... */
  background:
    linear-gradient(#ff1818 0 0) 0% / calc(100% / 3) 100% no-repeat,
    /* ... */;
  background-blend-mode: multiply;
  /* ... */
  animation: load steps(3) 1.5s infinite;
}
@keyframes load {
  to { background-position: 150%; }
}

I’ve removed the superfluous code used to create the same layout as the last example, but with three cubes instead of four. What I am adding here is a gradient defined with a specific color that blends with the conic gradient, just as we did earlier for the progress bar 3D loader.

From there, it’s animating the background gradient’s background-position as a three-step animation to make the cubes blink colors one at a time.

If you are not familiar with the values I am using for background-position and the background syntax, I highly recommend one of my previous articles and one of my Stack Overflow answers. You will find a very detailed explanation there.

Can we update the number of cubes to make it variables?

Yes, I do have a solution for that, but I’d like you to take a crack at it rather than embedding it here. Take what we have learned from the previous example and try to do the same with this one — then share your work in the comments!

Variations galore!

Like the other three articles in this series, I’d like to leave you with some inspiration to go forth and create your own loaders. Here is a collection that includes the 3D loaders we made together, plus a few others to get your imagination going:

That’s a wrap

I sure do hope you enjoyed spending time making single element loaders with me these past few weeks. It’s crazy that we started with seemingly simple spinner and then gradually added new pieces to work ourselves all the way up to 3D techniques that still only use a single element in the markup. This is exactly what CSS looks like when we harness its powers: scalable, flexible, and reusable.

Thanks again for reading this little series! I’ll sign off by reminding you that I have a collection of more than 500 loaders if you’re looking for more ideas and inspiration.


Single Element Loaders: Going 3D! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/single-element-loaders-going-3d/feed/ 2 366544
Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D https://css-tricks.com/css-hover-effects-background-masks-3d/ https://css-tricks.com/css-hover-effects-background-masks-3d/#comments Thu, 26 May 2022 14:13:01 +0000 https://css-tricks.com/?p=365946 We’ve walked through a series of posts now about interesting approaches to CSS hover effects. We started with a bunch of examples that use CSS background properties, then moved on to the text-shadow property where we technically didn’t use


Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’ve walked through a series of posts now about interesting approaches to CSS hover effects. We started with a bunch of examples that use CSS background properties, then moved on to the text-shadow property where we technically didn’t use any shadows. We also combined them with CSS variables and calc() to optimize the code and make it easy to manage.

In this article, we will build off those two articles to create even more complex CSS hover animations. We’re talking about background clipping, CSS masks, and even getting our feet wet with 3D perspectives. In other words, we are going to explore advanced techniques this time around and push the limits of what CSS can do with hover effects!

Cool Hover Effects series:

  1. Cool Hover Effects That Use Background Properties
  2. Cool Hover Effects That Use CSS Text Shadow
  3. Cool Hover Effects That Use Background Clipping, Masks, and 3D (you are here!)

Here’s just a taste of what we’re making:

Hover effects using background-clip

Let’s talk about background-clip. This CSS property accepts a text keyword value that allows us to apply gradients to the text of an element instead of the actual background.

So, for example, we can change the color of the text on hover as we would using the color property, but this way we animate the color change:

All I did was add background-clip: text to the element and transition the background-position. Doesn’t have to be more complicated than that!

But we can do better if we combine multiple gradients with different background clipping values.

In that example, I use two different gradients and two values with background-clip. The first background gradient is clipped to the text (thanks to the text value) to set the color on hover, while the second background gradient creates the bottom underline (thanks to the padding-box value). Everything else is straight up copied from the work we did in the first article of this series.

How about a hover effect where the bar slides from top to bottom in a way that looks like the text is scanned, then colored in:

This time I changed the size of the first gradient to create the line. Then I slide it with the other gradient that update the text color to create the illusion! You can visualize what’s happening in this pen:

We’ve only scratched the surface of what we can do with our background-clipping powers! However, this technique is likely something you’d want to avoid using in production, as Firefox is known to have a lot of reported bugs related to background-clip. Safari has support issues as well. That leaves only Chrome with solid support for this stuff, so maybe have it open as we continue.

Let’s move on to another hover effect using background-clip:

You’re probably thinking this one looks super easy compared to what we’ve just covered — and you are right, there’s nothing fancy here. All I am doing is sliding one gradient while increasing the size of another one.

But we’re here to look at advanced hover effects, right? Let’s change it up a bit so the animation is different when the mouse cursor leaves the element. Same hover effect, but a different ending to the animation:

Cool right? let’s dissect the code:

.hover {
  --c: #1095c1; /* the color */

  color: #0000;
  background: 
    linear-gradient(90deg, #fff 50%, var(--c) 0) calc(100% - var(--_p, 0%)) / 200%, 
    linear-gradient(var(--c) 0 0) 0% 100% / var(--_p, 0%) no-repeat,
    var(--_c, #0000);
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  transition: 0s, color .5s, background-color .5s;
}
.hover:hover {
  color: #fff;
  --_c: var(--c);
  --_p: 100%;
  transition: 0.5s, color 0s .5s, background-color 0s .5s;
}

We have three background layers — two gradients and the background-color defined using --_c variable which is initially set to transparent (#0000). On hover, we change the color to white and the --_c variable to the main color (--c).

Here’s what is happening on that transition: First, we apply a transition to everything but we delay the color and background-color by 0.5s to create the sliding effect. Right after that, we change the color and the background-color. You might notice no visual changes because the text is already white (thanks to the first gradient) and the background is already set to the main color (thanks to the second gradient).

Then, on mouse out, we apply an instant change to everything (notice the 0s delay), except for the color and background-color that have a transition. This means that we put all the gradients back to their initial states. Again, you will probably see no visual changes because the text color and background-color already changed on hover.

Lastly, we apply the fading to color and a background-color to create the mouse-out part of the animation. I know, it may be tricky to grasp but you can better visualize the trick by using different colors:

Hover the above a lot of times and you will see the properties that are animating on hover and the ones animating on mouse out. You can then understand how we reached two different animations for the same hover effect.

Let’s not forget the DRY switching technique we used in the previous articles of this series to help reduce the amount of code by using only one variable for the switch:

.hover {
  --c: 16 149 193; /* the color using the RGB format */

  color: rgb(255 255 255 / var(--_i, 0));
  background:
    /* Gradient #1 */
    linear-gradient(90deg, #fff 50%, rgb(var(--c)) 0) calc(100% - var(--_i, 0) * 100%) / 200%,
    /* Gradient #2 */
    linear-gradient(rgb(var(--c)) 0 0) 0% 100% / calc(var(--_i, 0) * 100%) no-repeat,
    /* Background Color */
    rgb(var(--c)/ var(--_i, 0));
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  --_t: calc(var(--_i,0)*.5s);
  transition: 
    var(--_t),
    color calc(.5s - var(--_t)) var(--_t),
    background-color calc(.5s - var(--_t)) var(--_t);
}
.hover:hover {
  --_i: 1;
}

If you’re wondering why I reached for the RGB syntax for the main color, it’s because I needed to play with the alpha transparency. I am also using the variable --_t to reduce a redundant calculation used in the transition property.

Before we move to the next part here are more examples of hover effects I did a while ago that rely on background-clip. It would be too long to detail each one but with what we have learned so far you can easily understand the code. It can be a good inspiration to try some of them alone without looking at the code.

I know, I know. These are crazy and uncommon hover effects and I realize they are too much in most situations. But this is how to practice and learn CSS. Remember, we pushing the limits of CSS hover effects. The hover effect may be a novelty, but we’re learning new techniques along the way that can most certainly be used for other things.

Hover effects using CSS mask

Guess what? The CSS mask property uses gradients the same way the background property does, so you will see that what we’re making next is pretty straightforward.

Let’s start by building a fancy underline.

I’m using background to create a zig-zag bottom border in that demo. If I wanted to apply an animation to that underline, it would be tedious to do it using background properties alone.

Enter CSS mask.

The code may look strange but the logic is still the same as we did with all the previous background animations. The mask is composed of two gradients. The first gradient is defined with an opaque color that covers the content area (thanks to the content-box value). That first gradient makes the text visible and hides the bottom zig-zag border. content-box is the mask-clip value which behaves the same as background-clip

linear-gradient(#000 0 0) content-box

The second gradient will cover the whole area (thanks to padding-box). This one has a width that’s defined using the --_p variable, and it will be placed on the left side of the element.

linear-gradient(#000 0 0) 0 / var(--_p, 0%) padding-box

Now, all we have to do is to change the value of --_p on hover to create a sliding effect for the second gradient and reveal the underline.

.hover:hover {
  --_p: 100%;
  color: var(--c);
}

The following demo uses with the mask layers as backgrounds to better see the trick taking place. Imagine that the green and red parts are the visible parts of the element while everything else is transparent. That’s what the mask will do if we use the same gradients with it.

With such a trick, we can easily create a lot of variation by simply using a different gradient configuration with the mask property:

Each example in that demo uses a slightly different gradient configuration for the mask. Notice, too, the separation in the code between the background configuration and the mask configuration. They can be managed and maintained independently.

Let’s change the background configuration by replacing the zig-zag underline with a wavy underline instead:

Another collection of hover effects! I kept all the mask configurations and changed the background to create a different shape. Now, you can understand how I was able to reach 400 hover effects without pseudo-elements — and we can still have more!

Like, why not something like this:

Here’s a challenge for you: The border in that last demo is a gradient using the mask property to reveal it. Can you figure out the logic behind the animation? It may look complex at first glance, but it’s super similar to the logic we’ve looked at for most of the other hover effects that rely on gradients. Post your explanation in the comments!

Hover effects in 3D

You may think it’s impossible to create a 3D effect with a single element (and without resorting to pseudo-elements!) but CSS has a way to make it happen.

What you’re seeing there isn’t a real 3D effect, but rather a perfect illusion of 3D in the 2D space that combines the CSS background, clip-path, and transform properties.

Breakdown of the CSS hover effect in four stages.
The trick may look like we’re interacting with a 3D element, but we’re merely using 2D tactics to draw a 3D box

The first thing we do is to define our variables:

--c: #1095c1; /* color */
--b: .1em; /* border length */
--d: 20px; /* cube depth */

Then we create a transparent border with widths that use the above variables:

--_s: calc(var(--d) + var(--b));
color: var(--c);
border: solid #0000; /* fourth value sets the color's alpha */
border-width: var(--b) var(--b) var(--_s) var(--_s);

The top and right sides of the element both need to equal the --b value while the bottom and left sides need to equal to the sum of --b and --d (which is the --_s variable).

For the second part of the trick, we need to define one gradient that covers all the border areas we previously defined. A conic-gradient will work for that:

background: conic-gradient(
  at left var(--_s) bottom var(--_s),
  #0000 90deg,var(--c) 0
 ) 
 0 100% / calc(100% - var(--b)) calc(100% - var(--b)) border-box;
Diagram of the sizing used for the hover effect.

We add another gradient for the third part of the trick. This one will use two semi-transparent white color values that overlap the first previous gradient to create different shades of the main color, giving us the illusion of shading and depth.

conic-gradient(
  at left var(--d) bottom var(--d),
  #0000 90deg,
  rgb(255 255 255 / 0.3) 0 225deg,
  rgb(255 255 255 / 0.6) 0
) border-box
Showing the angles used to create the hover effect.

The last step is to apply a CSS clip-path to cut the corners for that long shadow sorta feel:

clip-path: polygon(
  0% var(--d), 
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, 
  0% 100%
)
Showing the coordinate points of the three-dimensional cube used in the CSS hover effect.

That’s all! We just made a 3D rectangle with nothing but two gradients and a clip-path that we can easily adjust using CSS variables. Now, all we have to do is to animate it!

Notice the coordinates from the previous figure (indicated in red). Let’s update those to create the animation:

clip-path: polygon(
  0% var(--d), /* reverses var(--d) 0% */
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, /* reverses 100% calc(100% - var(--d)) */ 
  0% 100% /* reverses var(--d) calc(100% - var(--d)) */
)

The trick is to hide the bottom and left parts of the element so all that’s left is a rectangular element with no depth whatsoever.

This pen isolates the clip-path portion of the animation to see what it’s doing:

The final touch is to move the element in the opposite direction using translate — and the illusion is perfect! Here’s the effect using different custom property values for varying depths:

The second hover effect follows the same structure. All I did is to update a few values to create a top left movement instead of a top right one.

Combining effects!

The awesome thing about everything we’ve covered is that they all complement each other. Here is an example where I am adding the text-shadow effect from the second article in the series to the background animation technique from the first article while using the 3D trick we just covered:

The actual code might be confusing at first, but go ahead and dissect it a little further — you’ll notice that it’s merely a combination of those three different effects, pretty much smushed together.

Let me finish this article with a last hover effect where I am combining background, clip-path, and a dash of perspective to simulate another 3D effect:

I applied the same effect to images and the result was quite good for simulating 3D with a single element:

Want a closer look at how that last demo works? I wrote something up on it.

Wrapping up

Oof, we are done! I know, it’s a lot of tricky CSS but (1) we’re on the right website for that kind of thing, and (2) the goal is to push our understanding of different CSS properties to new levels by allowing them to interact with one another.

You may be asking what the next step is from here now that we’re closing out this little series of advanced CSS hover effects. I’d say the next step is to take all that we learned and apply them to other elements, like buttons, menu items, links, etc. We kept things rather simple as far as limiting our tricks to a heading element for that exact reason; the actual element doesn’t matter. Take the concepts and run with them to create, experiment with, and learn new things!


Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-hover-effects-background-masks-3d/feed/ 5 365946
A CSS Slinky in 3D? Challenge Accepted! https://css-tricks.com/a-css-slinky-in-3d/ https://css-tricks.com/a-css-slinky-in-3d/#comments Thu, 12 May 2022 14:27:50 +0000 https://css-tricks.com/?p=365706 Braydon Coyer recently launched a monthly CSS art challenge. He actually had reached out to me about donating a copy of my book Move Things with CSS to use as a prize for the winner of the challenge — …


A CSS Slinky in 3D? Challenge Accepted! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Braydon Coyer recently launched a monthly CSS art challenge. He actually had reached out to me about donating a copy of my book Move Things with CSS to use as a prize for the winner of the challenge — which I was more than happy to do!

The first month’s challenge? Spring. And when thinking of what to make for the challenge, Slinkys immediately came to mind. You know Slinkys, right? That classic toy you knock down the stairs and it travels with its own momentum.

Animated Gif of a Slinky toy going down stairs.
A slinking Slinky

Can we create a Slinky walking down stairs like that in CSS? That’s exactly the sort of challenge I like, so I thought we could tackle that together in this article. Ready to roll? (Pun intended.)

Setting up the Slinky HTML

Let’s make this flexible. (No pun intended.) What I mean by that is we want to be able to control the Slinky’s behavior through CSS custom properties, giving us the flexibility of swapping values when we need to.

Here’s how I’m setting the scene, written in Pug for brevity:

- const RING_COUNT = 10;
.container
  .scene
    .plane(style=`--ring-count: ${RING_COUNT}`)
      - let rings = 0;
      while rings < RING_COUNT
        .ring(style=`--index: ${rings};`)
        - rings++;

Those inline custom properties are an easy way for us to update the number of rings and will come in handy as we get deeper into this challenge. The code above gives us 10 rings with HTML that looks something like this when compiled:

<div class="container">
  <div class="scene">
    <div class="plane" style="--ring-count: 10">
      <div class="ring" style="--index: 0;"></div>
      <div class="ring" style="--index: 1;"></div>
      <div class="ring" style="--index: 2;"></div>
      <div class="ring" style="--index: 3;"></div>
      <div class="ring" style="--index: 4;"></div>
      <div class="ring" style="--index: 5;"></div>
      <div class="ring" style="--index: 6;"></div>
      <div class="ring" style="--index: 7;"></div>
      <div class="ring" style="--index: 8;"></div>
      <div class="ring" style="--index: 9;"></div>
    </div>
  </div>
</div>

The initial Slinky CSS

We’re going to need some styles! What we want is a three-dimensional scene. I’m mindful of some things we may want to do later, so that’s the thinking behind having an extra wrapper component with a .scene class.

Let’s start by defining some properties for our “infini-slinky” scene:

:root {
  --border-width: 1.2vmin;
  --depth: 20vmin;
  --stack-height: 6vmin;
  --scene-size: 20vmin;
  --ring-size: calc(var(--scene-size) * 0.6);
  --plane: radial-gradient(rgb(0 0 0 / 0.1) 50%, transparent 65%);
  --ring-shadow: rgb(0 0 0 / 0.5);
  --hue-one: 320;
  --hue-two: 210;
  --blur: 10px;
  --speed: 1.2s;
  --bg: #fafafa;
  --ring-filter: brightness(1) drop-shadow(0 0 0 var(--accent));
}

These properties define the characteristics of our Slinky and the scene. With the majority of 3D CSS scenes, we’re going to set transform-style across the board:

* {
  box-sizing: border-box;
  transform-style: preserve-3d;
}

Now we need styles for our .scene. The trick is to translate the .plane so it looks like our CSS Slinky is moving infinitely down a flight of stairs. I had to play around to get things exactly the way I want, so bear with the magic number for now, as they’ll make sense later.

.container {
  /* Define the scene's dimensions */
  height: var(--scene-size);
  width: var(--scene-size);
  /* Add depth to the scene */
  transform:
    translate3d(0, 0, 100vmin)
    rotateX(-24deg) rotateY(32deg)
    rotateX(90deg)
    translateZ(calc((var(--depth) + var(--stack-height)) * -1))
    rotate(0deg);
}
.scene,
.plane {
  /* Ensure our container take up the full .container */
  height: 100%;
  width: 100%;
  position: relative;
}
.scene {
  /* Color is arbitrary */
  background: rgb(162 25 230 / 0.25);
}
.plane {
  /* Color is arbitrary */
  background: rgb(25 161 230 / 0.25);
  /* Overrides the previous selector */
  transform: translateZ(var(--depth));
}

There is a fair bit going on here with the .container transformation. Specifically:

  • translate3d(0, 0, 100vmin): This brings the .container forward and stops our 3D work from getting cut off by the body. We aren’t using perspective at this level, so we can get away with it.
  • rotateX(-24deg) rotateY(32deg): This rotates the scene based on our preferences.
  • rotateX(90deg): This rotates the .container by a quarter turn, which flattens the .scene and .plane by default, Otherwise, the two layers would look like the top and bottom of a 3D cube.
  • translate3d(0, 0, calc((var(--depth) + var(--stack-height)) * -1)): We can use this to move the scene and center it on the y-axis (well, actually the z-axis). This is in the eye of the designer. Here, we are using the --depth and --stack-height to center things.
  • rotate(0deg): Although, not in use at the moment, we may want to rotate the scene or animate the rotation of the scene later.

To visualize what’s happening with the .container, check this demo and tap anywhere to see the transform applied (sorry, Chromium only. 😭):

We now have a styled scene! 💪

Styling the Slinky’s rings

This is where those CSS custom properties are going to play their part. We have the inlined properties --index and --ring-count from our HTML. We also have the predefined properties in the CSS that we saw earlier on the :root.

The inline properties will play a part in positioning each ring:

.ring {
  --origin-z: 
    calc(
      var(--stack-height) - (var(--stack-height) / var(--ring-count)) 
      * var(--index)
    );
  --hue: var(--hue-one);
  --accent: hsl(var(--hue) 100% 55%);
  height: var(--ring-size);
  width: var(--ring-size);
  border-radius: 50%;
  border: var(--border-width) solid var(--accent);
  position: absolute;
  top: 50%;
  left: 50%;
  transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
  transform:
    translate3d(-50%, -50%, var(--origin-z))
    translateZ(0)
    rotateY(0deg);
}
.ring:nth-of-type(odd) {
  --hue: var(--hue-two);
}

Take note of how we are calculating the --origin-z value as well as how we position each ring with the transform property. That comes after positioning each ring with position: absolute .

It is also worth noting how we’re alternating the color of each ring in that last ruleset. When I first implemented this, I wanted to create a rainbow slinky where the rings went through the hues. But that adds a bit of complexity to the effect.

Now we’ve got some rings on our raised .plane:

Transforming the Slinky rings

It’s time to get things moving! You may have noticed that we set a transform-origin on each .ring like this:

.ring {
  transform-origin: calc(100% + (var(--scene-size) * 0.2)) 50%;
}

This is based on the .scene size. That 0.2 value is half the remaining available size of the .scene after the .ring is positioned.

We could tidy this up a bit for sure!

:root {
  --ring-percentage: 0.6;
  --ring-size: calc(var(--scene-size) * var(--ring-percentage));
  --ring-transform:
    calc(
      100% 
      + (var(--scene-size) * ((1 - var(--ring-percentage)) * 0.5))
    ) 50%;
}

.ring {
  transform-origin: var(--ring-transform);
}

Why that transform-origin? Well, we need the ring to look like is moving off-center. Playing with the transform of an individual ring is a good way to work out the transform we want to apply. Move the slider on this demo to see the ring flip:

Add all the rings back and we can flip the whole stack!

Hmm, but they aren’t falling to the next stair. How can we make each ring fall to the right position?

Well, we have a calculated --origin-z, so let’s calculate --destination-z so the depth changes as the rings transform. If we have a ring on top of the stack, it should wind up at the bottom after it falls. We can use our custom properties to scope a destination for each ring:

ring {
  --destination-z: calc(
    (
      (var(--depth) + var(--origin-z))
      - (var(--stack-height) - var(--origin-z))
    ) * -1
  );
  transform-origin: var(--ring-transform);
  transform:
    translate3d(-50%, -50%, var(--origin-z))
    translateZ(calc(var(--destination-z) * var(--flipped, 0)))
    rotateY(calc(var(--flipped, 0) * 180deg));
}

Now try moving the stack! We’re getting there. 🙌

Animating the rings

We want our ring to flip and then fall. A first attempt might look something like this:

.ring {
  animation-name: slink;
  animation-duration: 2s;
  animation-fill-mode: both;
  animation-iteration-count: infinite;
}

@keyframes slink {
  0%, 5% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  25% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  45%, 100% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(var(--destination-z))
      rotateY(180deg);
  }
}

Oof, that’s not right at all!

But that’s only because we aren’t using animation-delay. All the rings are, um, slinking at the same time. Let’s introduce an animation-delay based on the --index of the ring so they slink in succession.

.ring {
  animation-delay: calc(var(--index) * 0.1s);
}

OK, that is indeed “better.” But the timing is still off. What sticks out more, though, is the shortcoming of animation-delay. It is only applied on the first animation iteration. After that, we lose the effect.

At this point, let’s color the rings so they progress through the hue wheel. This is going to make it easier to see what’s going on.

.ring {
  --hue: calc((360 / var(--ring-count)) * var(--index));
}

That’s better! ✨

Back to the issue. Because we are unable to specify a delay that’s applied to every iteration, we are also unable to get the effect we want. For our Slinky, if we were able to have a consistent animation-delay, we might be able to achieve the effect we want. And we could use one keyframe while relying on our scoped custom properties. Even an animation-repeat-delay could be an interesting addition.

This functionality is available in JavaScript animation solutions. For example, GreenSock allows you to specify a delay and a repeatDelay.

But, our Slinky example isn’t the easiest thing to illustrate this problem. Let’s break this down into a basic example. Consider two boxes. And you want them to alternate spinning.

How do we do this with CSS and no “tricks”? One idea is to add a delay to one of the boxes:

.box {
  animation: spin 1s var(--delay, 0s) infinite;
}
.box:nth-of-type(2) {
  --delay: 1s;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

But, that won’t work because the red box will keep spinning. And so will the blue one after its initial animation-delay.

With something like GreenSock, though, we can achieve the effect we want with relative ease:

import gsap from 'https://cdn.skypack.dev/gsap'

gsap.to('.box', {
  rotate: 360,
  /**
   * A function based value, means that the first box has a delay of 0 and
   * the second has a delay of 1
  */
  delay: (index) > index,
  repeatDelay: 1,
  repeat: -1,
  ease: 'power1.inOut',
})

And there it is!

But how can we do this without JavaScript?

Well, we have to “hack” our @keyframes and completely do away with animation-delay. Instead, we will pad out the @keyframes with empty space. This comes with various quirks, but let’s go ahead and build a new keyframe first. This will fully rotate the element twice:

@keyframes spin {
  50%, 100% {
    transform: rotate(360deg);
  }
}

It’s like we’ve cut the keyframe in half. And now we’ll have to double the animation-duration to get the same speed. Without using animation-delay, we could try setting animation-direction: reverse on the second box:

.box {
  animation: spin 2s infinite;
}

.box:nth-of-type(2) {
  animation-direction: reverse;
}

Almost.

The rotation is the wrong way round. We could use a wrapper element and rotate that, but that could get tricky as there are more things to balance. The other approach is to create two keyframes instead of one:

@keyframes box-one {
  50%, 100% {
    transform: rotate(360deg);
  }
}
@keyframes box-two {
  0%, 50% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

And there we have it:

This would’ve been a lot easier if we had a way to specify the repeat delay with something like this:

/* Hypothetical! */
animation: spin 1s 0s 1s infinite;

Or if the repeated delay matched the initial delay, we could possibly have a combinator for it:

/* Hypothetical! */
animation: spin 1s 1s+ infinite;

It would make for an interesting addition for sure!

So, we need keyframes for all those rings?

Yes, that is, if we want a consistent delay. And we need to do that based on what we are going to use as the animation window. All the rings need to have “slinked” and settled before the keyframes repeat.

This would be horrible to write out by hand. But this is why we have CSS preprocessors, right? Well, at least until we get loops and some extra custom property features on the web. 😉

Today’s weapon of choice will be Stylus. It’s my favorite CSS preprocessor and has been for some time. Habit means I haven’t moved to Sass. Plus, I like Stylus’s lack of required grammar and flexibility.

Good thing we only need to write this once:

// STYLUS GENERATED KEYFRAMES BE HERE...
$ring-count = 10
$animation-window = 50
$animation-step = $animation-window / $ring-count

for $ring in (0..$ring-count)
  // Generate a set of keyframes based on the ring index
  // index is the ring
  $start = $animation-step * ($ring + 1)
  @keyframes slink-{$ring} {
    // In here is where we need to generate the keyframe steps based on ring count and window.
    0%, {$start * 1%} {
      transform
        translate3d(-50%, -50%, var(--origin-z))
        translateZ(0)
        rotateY(0deg)
    }
    // Flip without falling
    {($start + ($animation-window * 0.75)) * 1%} {
      transform
        translate3d(-50%, -50%, var(--origin-z))
        translateZ(0)
        rotateY(180deg)
    }
    // Fall until the cut-off point
    {($start + $animation-window) * 1%}, 100% {
      transform
        translate3d(-50%, -50%, var(--origin-z))
        translateZ(var(--destination-z))
        rotateY(180deg)
    }
  }

Here’s what those variables mean:

  • $ring-count: The number of rings in our slinky.
  • $animation-window: This is the percentage of the keyframe that we can slink in. In our example, we’re saying we want to slink over 50% of the keyframes. The remaining 50% should get used for delays.
  • $animation-step: This is the calculated stagger for each ring. We can use this to calculate the unique keyframe percentages for each ring.

Here’s how it compiles to CSS, at least for the first couple of iterations:

View full code
@keyframes slink-0 {
  0%, 4.5% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  38.25% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  49.5%, 100% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(var(--destination-z))
      rotateY(180deg);
  }
}
@keyframes slink-1 {
  0%, 9% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(0deg);
  }
  42.75% {
    transform:
      translate3d(-50%, -50%, var(--origin-z))
      translateZ(0)
      rotateY(180deg);
  }
  54%, 100% {
    transform:
       translate3d(-50%, -50%, var(--origin-z))
       translateZ(var(--destination-z))
       rotateY(180deg);
  }
}

The last thing to do is apply each set of keyframes to each ring. We can do this using our markup if we want by updating it to define both an --index and a --name:

- const RING_COUNT = 10;
.container
  .scene
    .plane(style=`--ring-count: ${RING_COUNT}`)
      - let rings = 0;
      while rings < RING_COUNT
        .ring(style=`--index: ${rings}; --name: slink-${rings};`)
        - rings++;

Which gives us this when compiled:

<div class="container">
  <div class="scene">
    <div class="plane" style="--ring-count: 10">
      <div class="ring" style="--index: 0; --name: slink-0;"></div>
      <div class="ring" style="--index: 1; --name: slink-1;"></div>
      <div class="ring" style="--index: 2; --name: slink-2;"></div>
      <div class="ring" style="--index: 3; --name: slink-3;"></div>
      <div class="ring" style="--index: 4; --name: slink-4;"></div>
      <div class="ring" style="--index: 5; --name: slink-5;"></div>
      <div class="ring" style="--index: 6; --name: slink-6;"></div>
      <div class="ring" style="--index: 7; --name: slink-7;"></div>
      <div class="ring" style="--index: 8; --name: slink-8;"></div>
      <div class="ring" style="--index: 9; --name: slink-9;"></div>
    </div>
  </div>
</div>

And then our styling can be updated accordingly:

.ring {
  animation: var(--name) var(--speed) both infinite cubic-bezier(0.25, 0, 1, 1);
}

Timing is everything. So we’ve ditched the default animation-timing-function and we’re using a cubic-bezier. We’re also making use of the --speed custom property we defined at the start.

Aw yeah. Now we have a slinking CSS Slinky! Have a play with some of the variables in the code and see what different behavior you can yield.

Creating an infinite animation

Now that we have the hardest part out of the way, we can make get this to where the animation repeats infinitely. To do this, we’re going to translate the scene as our Slinky slinks so it looks like it is slinking back into its original position.

.scene {
  animation: step-up var(--speed) infinite linear both;
}

@keyframes step-up {
  to {
    transform: translate3d(-100%, 0, var(--depth));
  }
}

Wow, that took very little effort!

We can remove the platform colors from .scene and .plane to prevent the animation from being too jarring:

Almost done! The last thing to address is that the stack of rings flips before it slinks again. This is where we mentioned earlier that the use of color would come in handy. Change the number of rings to an odd number, like 11, and switch back to alternating the ring color:

Boom! We have a working CSS slinky! It’s configurable, too!

Fun variations

How about a “flip flop” effect? By that, I mean getting the Slink to slink alternate ways. If we add an extra wrapper element to the scene, we could rotate the scene by 180deg on each slink.

- const RING_COUNT = 11;
.container
  .flipper
    .scene
      .plane(style=`--ring-count: ${RING_COUNT}`)
        - let rings = 0;
        while rings < RING_COUNT
          .ring(style=`--index: ${rings}; --name: slink-${rings};`)
          - rings++;

As far as animation goes, we can make use of the steps() timing function and use twice the --speed:

.flipper {
  animation: flip-flop calc(var(--speed) * 2) infinite steps(1);
  height: 100%;
  width: 100%;
}

@keyframes flip-flop {
  0% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

Last, but not least, let’s change the way the .scene element’s step-up animation works. It no longer needs to move on the x-axis.

@keyframes step-up {
  0% {
    transform: translate3d(-50%, 0, 0);
  }
  100% {
    transform: translate3d(-50%, 0, var(--depth));
  }
}

Note the animation-timing-function that we use. That use of steps(1) is what makes it possible.

If you want another fun use of steps(), check out this #SpeedyCSSTip!

For an extra touch, we could rotate the whole scene slow:

.container {
  animation: rotate calc(var(--speed) * 40) infinite linear;
}
@keyframes rotate {
  to {
    transform:
      translate3d(0, 0, 100vmin)
      rotateX(-24deg)
      rotateY(-32deg)
      rotateX(90deg)
      translateZ(calc((var(--depth) + var(--stack-height)) * -1))
      rotate(360deg);
  }
}

I like it! Of course, styling is subjective… so, I made a little app you can use configure your Slinky:

And here are the “Original” and “Flip-Flop” versions I took a little further with shadows and theming.

Final demos

That’s it!

That’s at least one way to make a pure CSS Slinky that’s both 3D and configurable. Sure, you might not reach for something like this every day, but it brings up interesting CSS animation techniques. It also raises the question of whether having a animation-repeat-delay property in CSS would be useful. What do you think? Do you think there would be some good use cases for it? I’d love to know.

Be sure to have a play with the code — all of it is available in this CodePen Collection!


A CSS Slinky in 3D? Challenge Accepted! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-css-slinky-in-3d/feed/ 5 365706
Creating the DigitalOcean Logo in 3D With CSS https://css-tricks.com/creating-the-digitalocean-logo-in-3d-with-css/ https://css-tricks.com/creating-the-digitalocean-logo-in-3d-with-css/#comments Fri, 29 Apr 2022 14:37:10 +0000 https://css-tricks.com/?p=365447 Howdy y’all! Unless you’ve been living under a rock (and maybe even then), you’ve undoubtedly heard the news that CSS-Tricks, was acquired by DigitalOcean. Congratulations to everyone! 🥳

As a little hurrah to commemorate the occasion, I wanted to …


Creating the DigitalOcean Logo in 3D With CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Howdy y’all! Unless you’ve been living under a rock (and maybe even then), you’ve undoubtedly heard the news that CSS-Tricks, was acquired by DigitalOcean. Congratulations to everyone! 🥳

As a little hurrah to commemorate the occasion, I wanted to create the DigitalOcean logo in CSS. I did that, but then took it a little further with some 3D and parallax. This also makes for quite a good article because the way I made the logo uses various pieces from previous articles I’ve written. This cool little demo brings many of those concepts together.

So, let’s dive right in!

We are going to “trace” the DigitalOcean logo by grabbing an SVG version of it from simpleicons.org.

<svg role="img" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <title>DigitalOcean</title>
  <path d="M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z"></path>
</svg>

Being mindful that we’re taking this 3D, we can wrap our SVG in a .scene element. Then we can use the tracing technique from my “Advice for Advanced CSS Illustrations” article. We are using Pug so we can leverage its mixins and reduce the amount of markup we need to write for the 3D part.

- const SIZE = 40
.scene
  svg(role='img' viewbox='0 0 24 24' xmlns='http://www.w3.org/2000/svg')
    title DigitalOcean
    path(d='M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z')
  .logo(style=`--size: ${SIZE}`)
    .logo__arc.logo__arc--inner
    .logo__arc.logo__arc--outer
    .logo__square.logo__square--one
    .logo__square.logo__square--two
    .logo__square.logo__square--three

The idea is to style these elements so that they overlap our logo. We don’t need to create the “arc” portion of the logo as we’re thinking ahead because we are going to make this logo in 3D and can create the arc with two cylinder shapes. That means for now all we need is the containing elements for each cylinder, the inner arc, and the outer arc.

Check out this demo that lays out the different pieces of the DigitalOcean logo. If you toggle the “Explode” and hover elements, you can what the logo consists of.

If we wanted a flat DigitalOcean logo, we could use a CSS mask with a conic gradient. Then we would only need one “arc” element that uses a solid border.

.logo__arc--outer {
  border: calc(var(--size) * 0.1925vmin) solid #006aff;
  mask: conic-gradient(transparent 0deg 90deg, #000 90deg);
  transform: translate(-50%, -50%) rotate(180deg);
}

That would give us the logo. The “reveal” transitions a clip-path that shows the traced SVG image underneath.

Check out my “Advice for Complex CSS Illustrations” article for tips on working with advanced illustrations in CSS.

Extruding for the 3D

We have the blueprint for our DigitalOcean logo, so it’s time to make this 3D. Why didn’t we create 3D blocks from the start? Creating containing elements, makes it easier to create 3D via extrusion.

We covered creating 3D scenes in CSS in my “Learning to Think in Cubes Instead of Boxes” article. We are going to use some of those techniques for what we’re making here. Let’s start with the squares in the logo. Each square is a cuboid. And using Pug, we are going to create and use a cuboid mixin to help generate all of them.

mixin cuboid()
  .cuboid(class!=attributes.class)
    if block
      block
    - let s = 0
    while s < 6
      .cuboid__side
      - s++

Then we can use this in our markup:

.scene
  .logo(style=`--size: ${SIZE}`)
    .logo__arc.logo__arc--inner
    .logo__arc.logo__arc--outer
    .logo__square.logo__square--one
      +cuboid().square-cuboid.square-cuboid--one
    .logo__square.logo__square--two
      +cuboid().square-cuboid.square-cuboid--two
    .logo__square.logo__square--three
      +cuboid().square-cuboid.square-cuboid--three

Next, we need the styles to display our cuboids. Note that cuboids have six sides, so we’re styling those with the nth-of-type() pseudo selector while leveraging the vmin length unit to keep things responsive.

.cuboid {
  width: 100%;
  height: 100%;
  position: relative;
}
.cuboid__side {
  filter: brightness(var(--b, 1));
  position: absolute;
}
.cuboid__side:nth-of-type(1) {
  --b: 1.1;
  height: calc(var(--depth, 20) * 1vmin);
  width: 100%;
  top: 0;
  transform: translate(0, -50%) rotateX(90deg);
}
.cuboid__side:nth-of-type(2) {
  --b: 0.9;
  height: 100%;
  width: calc(var(--depth, 20) * 1vmin);
  top: 50%;
  right: 0;
  transform: translate(50%, -50%) rotateY(90deg);
}
.cuboid__side:nth-of-type(3) {
  --b: 0.5;
  width: 100%;
  height: calc(var(--depth, 20) * 1vmin);
  bottom: 0;
  transform: translate(0%, 50%) rotateX(90deg);
}
.cuboid__side:nth-of-type(4) {
  --b: 1;
  height: 100%;
  width: calc(var(--depth, 20) * 1vmin);
  left: 0;
  top: 50%;
  transform: translate(-50%, -50%) rotateY(90deg);
}
.cuboid__side:nth-of-type(5) {
  --b: 0.8;
  height: 100%;
  width: 100%;
  transform: translate3d(0, 0, calc(var(--depth, 20) * 0.5vmin));
  top: 0;
  left: 0;
}
.cuboid__side:nth-of-type(6) {
  --b: 1.2;
  height: 100%;
  width: 100%;
  transform: translate3d(0, 0, calc(var(--depth, 20) * -0.5vmin)) rotateY(180deg);
  top: 0;
  left: 0;
}

We are approaching this in a different way from how we have done it in past articles. Instead of applying height, width, and depth to a cuboid, we are only concerned with its depth. And instead of trying to color each side, we can make use of filter: brightness to handle that for us.

If you need to have cuboids or other 3D elements as a child of a side using filter, you may need to shuffle things. A filtered side will flatten any 3D children.

The DigitalOcean logo has three cuboids, so we have a class for each one and are styling them like this:

.square-cuboid .cuboid__side {
  background: hsl(var(--hue), 100%, 50%);
}
.square-cuboid--one {
  /* 0.1925? It's a percentage of the --size for that square */
  --depth: calc((var(--size) * 0.1925) * var(--depth-multiplier));
}
.square-cuboid--two {
  --depth: calc((var(--size) * 0.1475) * var(--depth-multiplier));
}
.square-cuboid--three {
  --depth: calc((var(--size) * 0.125) * var(--depth-multiplier));
}

…which gives us something like this:

You can play with the depth slider to extrude the cuboids as you wish! For our demo, we’ve chosen to make the cuboids true cubes with equal height, width, and depth. The depth of the arc will match the largest cuboid.

Now for the cylinders. The idea is to create two ends that use border-radius: 50%. Then, we can use many elements as the sides of the cylinder to create the effect. The trick is positioning all the sides.

There are various approaches we can take to create the cylinders in CSS. But, for me, if this is something I can foresee using many times, I’ll try and future-proof it. That means making a mixin and some styles I can reuse for other demos. And those styles should try and cater to scenarios I could see popping up. For a cylinder, there is some configuration we may want to consider:

  • radius
  • sides
  • how many of those sides are displayed
  • whether to show one or both ends of the cylinder

Putting that together, we can create a Pug mixin that caters to those needs:

mixin cylinder(radius = 10, sides = 10, cut = [5, 10], top = true, bottom = true)
  - const innerAngle = (((sides - 2) * 180) / sides) * 0.5
  - const cosAngle = Math.cos(innerAngle * (Math.PI / 180))
  - const side =  2 * radius * Math.cos(innerAngle * (Math.PI / 180))
  //- Use the cut to determine how many sides get rendered and from what point
  .cylinder(style=`--side: ${side}; --sides: ${sides}; --radius: ${radius};` class!=attributes.class)
    if top
      .cylinder__end.cylinder__segment.cylinder__end--top
    if bottom
      .cylinder__end.cylinder__segment.cylinder__end--bottom
    - const [start, end] = cut
    - let i = start
    while i < end
      .cylinder__side.cylinder__segment(style=`--index: ${i};`)
      - i++

See how //- is prepended to the comment in the code? That tells Pug to ignore the comment and leave it out from the compiled HTML markup.

Why do we need to pass the radius into the cylinder? Well, unfortunately, we can’t quite handle trigonometry with CSS calc() just yet (but it is coming). And we need to work out things like the width of the cylinder sides and how far out from the center they should project. The great thing is that we have a nice way to pass that information to our styles via inline custom properties.

.cylinder(
  style=`
    --side: ${side};
    --sides: ${sides};
    --radius: ${radius};`
  class!=attributes.class
)

An example use for our mixin would be as follows:

+cylinder(20, 30, [10, 30])

This would create a cylinder with a radius of 20, 30 sides, where only sides 10 to 30 are rendered.

Then we need some styling. Styling the cylinders for the DigitalOcean logo is pretty straightforward, thankfully:

.cylinder {
  --bg: hsl(var(--hue), 100%, 50%);
  background: rgba(255,43,0,0.5);
  height: 100%;
  width: 100%;
  position: relative;
}
.cylinder__segment {
  filter: brightness(var(--b, 1));
  background: var(--bg, #e61919);
  position: absolute;
  top: 50%;
  left: 50%;
}
.cylinder__end {
  --b: 1.2;
  --end-coefficient: 0.5;
  height: 100%;
  width: 100%;
  border-radius: 50%;
  transform: translate3d(-50%, -50%, calc((var(--depth, 0) * var(--end-coefficient)) * 1vmin));
}
.cylinder__end--bottom {
  --b: 0.8;
  --end-coefficient: -0.5;
}
.cylinder__side {
  --b: 0.9;
  height: calc(var(--depth, 30) * 1vmin);
  width: calc(var(--side) * 1vmin);
  transform: translate(-50%, -50%) rotateX(90deg) rotateY(calc((var(--index, 0) * 360 / var(--sides)) * 1deg)) translate3d(50%, 0, calc(var(--radius) * 1vmin));
}

The idea is that we create all the sides of the cylinder and put them in the middle of the cylinder. Then we rotate them on the Y-axis and project them out by roughly the distance of the radius.

There’s no need to show the ends of the cylinder in the inner part since they’re already obscured. But we do need to show them for the outer portion. Our two-cylinder mixin use look like this:

.logo(style=`--size: ${SIZE}`)
  .logo__arc.logo__arc--inner
    +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner
  .logo__arc.logo__arc--outer
    +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer

We know the radius from the diameter we used when tracing the logo earlier. Plus, we can use the outer cylinder ends to create the faces of the DigitalOcean logo. A combination of border-width and clip-path comes in handy here.

.cylinder-arc--outer .cylinder__end--top,
.cylinder-arc--outer .cylinder__end--bottom {
  /* Based on the percentage of the size needed to cap the arc */
  border-width: calc(var(--size) * 0.1975vmin);
  border-style: solid;
  border-color: hsl(var(--hue), 100%, 50%);
  --clip: polygon(50% 0, 50% 50%, 0 50%, 0 100%, 100% 100%, 100% 0);
  clip-path: var(--clip);
}

We’re pretty close to where we want to be!

There is one thing missing though: capping the arc. We need to create some ends for the arc, which requires two elements that we can position and rotate on the X or Y-axis:

.scene
  .logo(style=`--size: ${SIZE}`)
    .logo__arc.logo__arc--inner
      +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner
    .logo__arc.logo__arc--outer
      +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer
    .logo__square.logo__square--one
      +cuboid().square-cuboid.square-cuboid--one
    .logo__square.logo__square--two
      +cuboid().square-cuboid.square-cuboid--two
    .logo__square.logo__square--three
      +cuboid().square-cuboid.square-cuboid--three
    .logo__cap.logo__cap--top
    .logo__cap.logo__cap--bottom

The arc’s capped ends will assume the height and width based on the end’s border-width value as well as the depth of the arc.

.logo__cap {
  --hue: 10;
  position: absolute;
  height: calc(var(--size) * 0.1925vmin);
  width: calc(var(--size) * 0.1975vmin);
  background: hsl(var(--hue), 100%, 50%);
}
.logo__cap--top {
  top: 50%;
  left: 0;
  transform: translate(0, -50%) rotateX(90deg);
}
.logo__cap--bottom {
  bottom: 0;
  right: 50%;
  transform: translate(50%, 0) rotateY(90deg);
  height: calc(var(--size) * 0.1975vmin);
  width: calc(var(--size) * 0.1925vmin);
}

We’ve capped the arc!

Throwing everything together, we have our DigitalOcean logo. This demo allows you to rotate it in different directions.

But there’s still one more trick up our sleeve!

We’ve got our 3D DigitalOcean logo but it would be neat if it was interactive in some way. Back in November 2021, we covered how to create a parallax effect with CSS custom properties. Let’s use that same technique here, the idea being that the logo rotates and moves by following a user’s mouse cursor.

We do need a dash of JavaScript so that we can update the custom properties we need for a coefficient that sets the logo’s movement along the X and Y-axes in the CSS. Those coefficients are calculated from a user’s pointer position. I’ll often use GreenSock so I can use gsap.utils.mapRange. But, here is a vanilla JavaScript version of it that implements mapRange:

const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => {
  const INPUT_RANGE = inputUpper - inputLower
  const OUTPUT_RANGE = outputUpper - outputLower
  return value => outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
}

const BOUNDS = 100      
const update = ({ x, y }) => {
  const POS_X = mapRange(0, window.innerWidth, -BOUNDS, BOUNDS)(x)
  const POS_Y = mapRange(0, window.innerHeight, -BOUNDS, BOUNDS)(y)
  document.body.style.setProperty('--coefficient-x', POS_X)
  document.body.style.setProperty('--coefficient-y', POS_Y)
}

document.addEventListener('pointermove', update)

The magic happens in CSS-land. This is one of the major benefits of using custom properties this way. JavaScript is telling CSS what’s happening with the interaction. But, it doesn’t care what CSS does with it. That’s a rad decoupling. I use this JavaScript snippet in so many of my demos for this very reason. We can create different experiences simply by updating the CSS.

How do we do that? Use calc() and custom properties that are scoped directly to the .scene element. Consider these updated styles for .scene:

.scene {
  --rotation-y: 75deg;
  --rotation-x: -14deg;
  transform: translate3d(0, 0, 100vmin)
    rotateX(-16deg)
    rotateY(28deg)
    rotateX(calc(var(--coefficient-y, 0) * var(--rotation-x, 0deg)))
    rotateY(calc(var(--coefficient-x, 0) * var(--rotation-y, 0deg)));
}

The makes the scene rotate on the X and Y-axes based on the user’s pointer movement. But we can adjust this behavior by tweaking the values for --rotation-x and --rotation-y.

Each cuboid will move its own way. They are able to move on either the X, Y, or Z-axis. But, we only need to define one transform. Then we can use scoped custom properties to do the rest.

.logo__square {
  transform: translate3d(
    calc(min(0, var(--coefficient-x, 0) * var(--offset-x, 0)) * 1%),
    calc((var(--coefficient-y) * var(--offset-y, 0)) * 1%),
    calc((var(--coefficient-x) * var(--offset-z, 0)) * 1vmin)
  );
}
.logo__square--one {
  --offset-x: 50;
  --offset-y: 10;
  --offset-z: -2;
}
.logo__square--two {
  --offset-x: -35;
  --offset-y: -20;
  --offset-z: 4;
}
.logo__square--three {
  --offset-x: 25;
  --offset-y: 30;
  --offset-z: -6;
}

That will give you something like this:

And we can tweak these to our heart’s content until we get something we’re happy with!

Adding an intro animation to the mix

OK, I fibbed a bit and have one final (I promise!) way we can enhance our work. What if we had some sort of intro animation? How about a wave or something that washes across and reveals the logo?

We could do this with the pseudo-elements of the body element:

:root {
  --hue: 215;
  --initial-delay: 1;
  --wave-speed: 2;
}

body:after,
body:before {
  content: '';
  position: absolute;
  height: 100vh;
  width: 100vw;
  background: hsl(var(--hue), 100%, calc(var(--lightness, 50) * 1%));
  transform: translate(100%, 0);
  animation-name: wave;
  animation-duration: calc(var(--wave-speed) * 1s);
  animation-delay: calc(var(--initial-delay) * 1s);
  animation-timing-function: ease-in;
}
body:before {
  --lightness: 85;
  animation-timing-function: ease-out;
}
@keyframes wave {
  from {
    transform: translate(-100%, 0);
  }
}

Now, the idea is that the DigitalOcean logo is hidden until the wave washes over the top of it. For this effect, we’re going to animate our 3D elements from an opacity of 0. And we’re going to animate all the sides to our 3D elements from a brightness of 1 to reveal the logo. Because the wave color matches that of the logo, we won’t see it fade in. Also, using animation-fill-mode: both means that our elements will extend the styling of our keyframes in both directions.

This requires some form of animation timeline. And this is where custom properties come into play. We can use the duration of our animations to calculate the delays of others. We looked at this in my “How to Make a Pure CSS 3D Package Toggle” and “Animated Matryoshka Dolls in CSS” articles.

:root {
  --hue: 215;
  --initial-delay: 1;
  --wave-speed: 2;
  --fade-speed: 0.5;
  --filter-speed: 1;
}

.cylinder__segment,
.cuboid__side,
.logo__cap {
  animation-name: fade-in, filter-in;
  animation-duration: calc(var(--fade-speed) * 1s),
    calc(var(--filter-speed) * 1s);
  animation-delay: calc((var(--initial-delay) + var(--wave-speed)) * 0.75s),
    calc((var(--initial-delay) + var(--wave-speed)) * 1.15s);
  animation-fill-mode: both;
}

@keyframes filter-in {
  from {
    filter: brightness(1);
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

How do we get the timing right? A little tinkering and making use of the “Animations Inspector” in Chrome’s DevTool goes a long ways. Try adjusting the timings in this demo:

You may find that the fade timing is unnecessary if you want the logo to be there once the wave has passed. In that case, try setting the fade to 0. And in particular, experiment with the filter and fade coefficients. They relate to the 0.75s and 1.15s from the code above. It’s worth adjusting things and having a play in Chrome’s Animation Inspector to see how things time in.

That’s it!

Putting it all together, we have this neat intro for our 3D DigitalOcean logo!

And, of course, this only one approach to create the DigitalOcean logo in 3D with CSS. If you see other possibilities or perhaps something that can be optimized further, drop a link to your demo in the comments!

Congratulations, again, to the CSS-Tricks team and DigitalOcean for their new partnership. I’m excited to see where things go with the acquisition. One thing is for sure: CSS-Tricks will continue to inspire and produce fantastic content for the community. 😎


Creating the DigitalOcean Logo in 3D With CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-the-digitalocean-logo-in-3d-with-css/feed/ 1 365447
How to Make a Pure CSS 3D Package Toggle https://css-tricks.com/how-to-make-a-pure-css-3d-package-toggle/ https://css-tricks.com/how-to-make-a-pure-css-3d-package-toggle/#respond Wed, 12 Jan 2022 15:29:38 +0000 https://css-tricks.com/?p=360168 You know how you can get cardboard boxes that come totally flat? You fold ‘em up and tape ‘em to make them into a useful box. Then when it’s time to recycle them, you cut them back apart to flatten …


How to Make a Pure CSS 3D Package Toggle originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
You know how you can get cardboard boxes that come totally flat? You fold ‘em up and tape ‘em to make them into a useful box. Then when it’s time to recycle them, you cut them back apart to flatten them. Recently, someone reached out to me about essentially this concept as a 3D animation and I thought it would make an interesting tutorial to do it entirely in CSS, so here we are!

How might that animation look? How could we create that packing timeline? Could the sizing be flexible? Let’s make a pure CSS package toggle.

Here’s what we’re working towards. Tap to pack and unpack the cardboard box.

Where to start?

Where do you even start with something like this? It’s best to plan ahead. We know we’re going to have a template for our package. And that will need folding up in three dimensions. If working with 3D in CSS is new to you, I recommend this article to get you started.

If you’re familiar with 3D CSS, it might be tempting to construct a cuboid and go from there. But, that’s going to pose some problems. We need to consider how a package goes from 2D to 3D.

Let’s start by creating a template. We need to plan ahead with our markup and think about how we want our packing animation to work. Let’s start with some HTML.

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Mixins are a good idea

There’s quite a bit happening there. It’s a lot of divs. I often like to use Pug for generating markup so I can split things up into reusable blocks. For example, every side will have two flaps. We can create a Pug mixin for the sides and use attributes to apply a modifier class name to make all that markup a lot easier to write.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")

We’re using two mixins. One creates the flaps for each side of the box. The other creates the sides of the box. Notice in the side mixin we are making use of block. That is where children of mixin usage get rendered which is particularly useful, as we need to nest some of the sides to make our lives easier later.

Our generated markup:

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Nesting the sides

Nesting the sides makes it easier to fold up our package. Much like each side has two flaps. The children of a side can inherit the sides’ transform and then apply their own. If we started with a cuboid, it would be hard to leverage this.

Screenshot showing HTML markup on the left and a rendering the unfolded cardboard box on the right. The markup shows how one of the box’s sides is a parent container that sets the broad side of the box and contains children for the corresponding top and bottom flaps. Orange arrows connect each element to the visual rendering to outline which parts of the box correspond in HTML correspond to the visual rendering.

Check out this demo that flips between nested and non-nested elements to see the difference in action.

Each box has a transform-origin set to the bottom right corner with 100% 100%. Checking the “Transform” toggle rotates each box 90deg. But, see how the behavior of that transform changes if we nest the elements.

We’re flipping between the two versions of markup but not changing anything else.

Nested:

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>

Not nested:

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>

Transforming all the things

After applying some styles to our HTML, we have our package template.

The styles specify the different colors and position the sides to the package. Each side gets a position that’s relative to the “main” side. (You’ll see why all that nesting is useful in a moment.)

There are some things to be aware of. Much like working with cuboids, we are using --height, --width, and --depth variables for sizing. This will make it easier to change our package sizing down the line.

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}

Why define sizing like this? We are using a unit-less default sizing of 20, an idea I picked up from Lea Verou’s 2016 CSS ConfAsia talk (starting at 52:44). Using custom properties as “data” instead of “values,” we are free to do what we want with them using calc(). Additionally, JavaScript doesn’t have to care about value units and we can change to pixels, a percentage, etc., without having to make changes elsewhere. You could refactor this into a coefficient in the --root, but it could also quickly become overkill.

The flaps for each side also need a size ever so smaller than the sides they are a part of. This is so we can see a slight gap as we would in real life. Also, the flaps on two sides need to sit a little lower. This is so that when we fold them up, we don’t get z-index fighting between them.

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}

We’re also starting to consider the transform-origin for the individual pieces. A top flap will rotate from its bottom edge and a bottom flap will rotate from its top edge.

We can use a pseudo-element for the tab on that right side. We are using clip-path to get that desired shape.

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}

Let’s start working with our template on a 3D plane. We can start by rotating the .scene on the X and Y axis.

.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}

Folding up

We’re ready to start folding up our template! Our template will fold up based on a custom property, --packaged. If the value is 1, then we can fold up the template. For example, let’s fold some of the sides and the pseudo-element tab.

.package__side--tabbed,
.package__side--tabbed:after {
  transform: rotateY(calc(var(--packaged, 0) * -90deg)); 
}
.package__side--extra {
  transform: rotateY(calc(var(--packaged, 0) * 90deg));
}

Or, we could write a rule for all sides that aren’t the “main” one.

.package__side:not(.package__side--main),
.package__side:not(.package__side--main):after {
  transform: rotateY(calc((var(--packaged, 0) * var(--rotation, 90)) * 1deg));
}
.package__side--tabbed { --rotation: -90; }

And that would cover all the sides.

Remember when I said the nested sides allow us to inherit a parent’s transform? If we update our demo so we can change the value of --packaged, we can see how the value affects the transforms. Try sliding the --packaged value between 1 and 0 and you’ll see exactly what I mean.

Now that we have a way to toggle the folding state of our template, we can start working on some motion. Our previous demo flips between the two states. We can make use of transition for that. The quickest way? Add a transition to the transform of every child in the .scene.

.scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s);
}

Multi-step transitions!

But we don’t fold the template all up in one go — in real life, there’s a sequence to it where we’d fold up one side and its flap first then move on to the next, and so on. Scoped custom properties are perfect for this.

.scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step, 1) * var(--delay, 0.2)) * 1s);
}

Here we are saying that, for each transition, use a transition-delay of --step multiplied by --delay. The --delay value won’t change but each element can define which “step” it is in the sequence. And then we can be explicit about the order in which things happen.

.package__side--extra {
  --step: 1;
}
.package__side--tabbed {
  --step: 2;
}
.package__side--flipped,
.package__side--tabbed::after {
  --step: 3;
}

Consider the following demo for a better idea of how this works. Change the slider values to update the order in which things happen. Can you change which car wins?

That same technique is key for what we are going to for. We could even introduce an --initial-delay that adds a slight pause to everything for even more realism.

.race__light--animated,
.race__light--animated:after,
.car {
  animation-delay: calc((var(--step, 0) * var(--delay-step, 0)) * 1s);
}

If we look back at our package, we can take this further and apply a “step” to all the elements that are going to transform. It’s quite verbose but it does the job. Alternatively, you could inline these values in the markup.

.package__side--extra > .package__flap--bottom {
  --step: 4;
}
.package__side--tabbed > .package__flap--bottom {
  --step: 5;
}
.package__side--main > .package__flap--bottom {
  --step: 6;
}
.package__side--flipped > .package__flap--bottom {
  --step: 7;
}
.package__side--extra > .package__flap--top {
  --step: 8;
}
.package__side--tabbed > .package__flap--top {
  --step: 9;
}
.package__side--main > .package__flap--top {
  --step: 10;
}
.package__side--flipped > .package__flap--top {
  --step: 11;
}

But, it doesn’t feel very realistic.

Maybe we oughta flip the box, too

If I were folding up the box in real life, I’d likely flip the box up before folding in the top flaps. So how might we do that? Well, those with an eager eye might have noticed the .package__wrapper element. We are going to use this to slide the package. Then we’re going to rotate the package on the x-axis. This will create the impression of flipping the package onto its side.

.package {
  transform-origin: 50% 100%;
  transform: rotateX(calc(var(--packaged, 0) * -90deg));
}
.package__wrapper {
  transform: translate(0, calc(var(--packaged, 0) * -100%));
}

Adjusting the --step declarations accordingly gives us something like this.

Unfolding the box

If you flip between the folded and not folded states, you’ll notice that the unfold doesn’t look right. The unfolding sequence should be the exact reverse of the folding sequence. We could flip the --step based on --packaged and the number of steps. Our latest step is 15. We can update our transition to this:

.scene *,
.scene *:after {
  --no-of-steps: 15;
  --step-delay: calc(var(--step, 1) - ((1 - var(--packaged, 0)) * (var(--step) - ((var(--no-of-steps) + 1) - var(--step)))));
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step-delay) * var(--delay, 0.2)) * 1s);
}

That is quite the mouthful of calc to reverse the transition-delay. But, it works! We must remind ourselves to keep that --no-of-steps value up to date though!

We do have another option. As we continue down the “pure CSS” route, we will eventually make use of the checkbox hack to toggling between the folding states. We could have two sets of defined “steps” where one set is active when our checkbox gets checked. It’s certainly a more verbose solution. But, it does give us more finite control.

/* Folding */
:checked ~ .scene .package__side--extra {
  --step: 1;
}
/* Unfolding */
.package__side--extra {
  --step: 15;
}

Sizing and centering

Before we ditch the use of [dat.gui](https://github.com/dataarts/dat.gui) in our demo, let’s have a play with the size of our package. We want to check that our package remains centered while folding and flipping. In this demo, the package has a larger --height and the .scene has a dashed border.

We may as well tweak our transform to better center the package while we’re at it:

/* Considers package height by translating on z-axis */
.scene {
  transform: rotateX(calc(var(--rotate-x, -24) * 1deg)) rotateY(calc(var(--rotate-y, -32) * 1deg)) rotateX(90deg) translate3d(0, 0, calc(var(--height, 20) * -0.5vmin));
}
/* Considers package depth by sliding the depth before flipping */
.package__wrapper {
  transform: translate(0, calc((var(--packaged, 0) * var(--depth, 20)) * -1vmin));
}

This gives us reliable centering in the scene. It all comes down to preference though!

Adding in the checkbox hack

Now let’s get dat.gui out of the way and make this “pure” CSS. For this, we need to introduce a bunch of controls in the HTML. We are going to use a checkbox for folding and unfolding our package. Then we’re going to use a radio button to pick a package size.

<input id="package" type="checkbox"/>

<input id="one" type="radio" name="size"/>
<label class="size-label one" for="one">S</label>

<input id="two" type="radio" name="size" checked="checked"/>
<label class="size-label two" for="two">M</label>

<input id="three" type="radio" name="size"/>
<label class="size-label three" for="three">L</label>

<input id="four" type="radio" name="size"/>
<label class="size-label four" for="four">XL</label>

<label class="close" for="package">Close Package</label>
<label class="open" for="package">Open Package</label>

In the final demo, we will hide the inputs and make use of the label elements. For now, though, let’s leave them all visible. The trick is to use the sibling combinator (~) when certain controls get :checked. We can then set custom property values on the .scene.

#package:checked ~ .scene {
  --packaged: 1;
}
#one:checked ~ .scene {
  --height: 10;
  --width: 20;
  --depth: 20;
}
#two:checked ~ .scene {
  --height: 20;
  --width: 20;
  --depth: 20;
}
#three:checked ~ .scene {
  --height: 20;
  --width: 30;
  --depth: 20;
}
#four:checked ~ .scene {
  --height: 30;
  --width: 20;
  --depth: 30;
}

And here is the demo with that working!

Final polish

Now we’re in a place to make things look “pretty” and add some extra touches. Let’s start by hiding all the inputs.

input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

We can style the sizing options as rounded buttons:

.size-label {
  position: fixed;
  top: var(--top);
  right: 1rem;
  z-index: 3;
  font-family: sans-serif;
  font-weight: bold;
  color: #262626;
  height: 44px;
  width: 44px;
  display: grid;
  place-items: center;
  background: #fcfcfc;
  border-radius: 50%;
  cursor: pointer;
  border: 4px solid #8bb1b1;
  transform: translate(0, calc(var(--y, 0) * 1%)) scale(var(--scale, 1));
  transition: transform 0.1s;
}
.size-label:hover {
  --y: -5;
}
.size-label:active {
  --y: 2;
  --scale: 0.9;
}

We want to be able to tap anywhere to toggle between folding and unfolding our package. So our .open and .close labels will take up the full screen. Wondering why we have two labels? It’s a little trick. If we use a transition-delay and scale up the appropriate label, we can hide both labels while the package transitions. This is how we combat spam tapping (even though it won’t stop a user hitting the space bar on a keyboard).

.close,
.open {
  position: fixed;
  height: 100vh;
  width: 100vw;
  z-index: 2;
  transform: scale(var(--scale, 1)) translate3d(0, 0, 50vmin);
  transition: transform 0s var(--reveal-delay, calc(((var(--no-of-steps, 15) + 1) * var(--delay, 0.2)) * 1s));
}

#package:checked ~ .close,
.open {
  --scale: 0;
  --reveal-delay: 0s;
}
#package:checked ~ .open {
  --scale: 1;
  --reveal-delay: calc(((var(--no-of-steps, 15) + 1) * var(--delay, 0.2)) * 1s);
}

Check out this demo to see where we’ve added background-color to both .open and .close. Neither label is visible during the transition.

We’ve got complete functionality! But, our package is a little underwhelming at the moment. Let’s add extra details to make things more “box”-like with things like parcel tape and packing labels.

Little details like this are only limited by our imagination! We can use our --packaged custom property to affect anything. For example, the .package__tape is transitioning the scaleY transform:

.package__tape {
  transform: translate3d(-50%, var(--offset-y), -2px) scaleX(var(--packaged, 0));
}

The thing to remember is that whenever we add a new feature that affects the sequence, we need to update our steps. Not only the --step values, but also the --no-of-steps value.

That’s it!

That’s how you make a pure CSS 3D package toggle. Are you going to drop this into your website? Unlikely! But, it’s fun to see how you might achieve these things with CSS. Custom properties are so powerful.

Why not get super festive and give the gift of CSS!

Stay Awesome! ʕ •ᴥ•ʔ


How to Make a Pure CSS 3D Package Toggle originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-to-make-a-pure-css-3d-package-toggle/feed/ 0 https://www.youtube.com/embed/kZOJCVvyF-4 Lea Verou: CSS Variables: var(--subtitle) - CSSConf.Asia 2016 nonadult 360168
CSS in 3D: Learning to Think in Cubes Instead of Boxes https://css-tricks.com/css-in-3d-learning-to-think-in-cubes-instead-of-boxes/ https://css-tricks.com/css-in-3d-learning-to-think-in-cubes-instead-of-boxes/#comments Fri, 23 Oct 2020 14:19:30 +0000 https://css-tricks.com/?p=323488 My path to learning CSS was a little unorthodox. I didn’t start as a front-end developer. I was a Java developer. In fact, my earliest recollections of CSS were picking colors for things in Visual Studio.

It wasn’t until later …


CSS in 3D: Learning to Think in Cubes Instead of Boxes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
My path to learning CSS was a little unorthodox. I didn’t start as a front-end developer. I was a Java developer. In fact, my earliest recollections of CSS were picking colors for things in Visual Studio.

It wasn’t until later that I got to tackle and find my love for the front end. And exploring CSS came later. When it did, it was around the time CSS3 was taking off. 3D and animation were the cool kids on the block. They almost shaped my learning of CSS. They drew me in and shaped (pun intended) my understanding of CSS more than other things, like layout, color, etc.

What I’m getting at is I’ve been doing the whole 3D CSS thing a minute. And as with anything you spend a lot of time with, you end up refining your process over the years as you hone that skill. This article is a look at how I’m currently approaching 3D CSS and goes over some tips and tricks that might help you!

https://codepen.io/jh3y/pen/mLaXRe

Everything’s a cuboid

For most things, we can use a cuboid. We can create more complex shapes, for sure but they usually take a little more consideration. Curves are particularly hard and there are some tricks for handling them (but more on that later).

We aren’t going to walk through how to make a cuboid in CSS. We can reference Ana Tudor’s post for that, or check out this screencast of me making one:

At its core, we use one element to wrap our cuboid and then transform six elements within. Each element acts as a side to our cuboid. It’s important that we apply transform-style: preserve-3d. And it’s not a bad idea to apply it everywhere. It’s likely we’ll deal with nested cuboids when things get more complex. Trying to debug a missing transform-style while hopping between browsers can be painful.

* { transform-style: preserve-3d; }

For your 3D creations that are more than a few faces, try and imagine the whole scene built from cuboids. For a real example, consider this demo of a 3D book. It’s four cuboids. One for each cover, one for the spine, and one for the pages. The use of background-image does the rest for us.

Setting a scene

We’re going to use cuboids like LEGO pieces. But, we can make our lives a little easier by setting a scene and creating a plane. That plane is where our creation will sit and makes it easier for us to rotate and move the whole creation.

For me, when I create a scene, I like to rotate it on the X and Y axis first. Then I lay it flat with rotateX(90deg). That way, when I want to add a new cuboid to the scene, I add it inside the plane element. Another thing I will do here is to 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);
}

Start with a boilerplate

Creating cuboids of various sizes and across a plane makes for a lot of repetition for each creation. For this reason, I use Pug to create my cuboids via a mixin. If you’re not familiar with Pug, I wrote a 5-minute intro.

A typical scene looks 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')

As for the CSS. My cuboid class is currently looking like this:

.cuboid {
  // Defaults
  --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%;
}

Which, by default, gives me something like this:

Powered by CSS variables

You may have noticed a fair few CSS variables (aka custom properties) in there. This is a big time-saver. I’m powering my cuboids with CSS variables.

  • --width: The width of a cuboid on the plane
  • --height: The height of a cuboid on the plane
  • --depth: The depth of a cuboid on the plane
  • --x: The X position on the plane
  • --y: The Y position on the plane

I use vmin mostly as my sizing unit to keep everything responsive. If I’m creating something to scale, I might create a responsive unit. We mentioned this technique in a previous article. Again, I lay the plane down flat. Now I can refer to my cuboids as having height, width, and depth. This demo shows how we can move a cuboid around the plane changing its dimensions.

Debugging with dat.GUI

You might have noticed that little panel in the top right for some of the demos we’ve covered. That’s dat.GUI. It’s a lightweight controller library for JavaScript that super useful for debugging 3D CSS. With not much code, we can set up a panel that allows us to change CSS variables at runtime. One thing I like to do is use the panel to rotate the plane on the X and Y-axis. That way, it’s possible to see how things are lining up or work on a part that you 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 at this point. Use x||y on the plane
// Or, use 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()

If you watch the timelapse video in this tweet. You’ll notice that I rotate the plane a lot as I build up the scene.

https://twitter.com/jh3yy/status/1312126353177673732?s=20

That dat.GUI code is a little repetitive. We can create functions that will take a configuration and generate the controller. It takes a little tinkering to cater to your needs. I started playing with dynamically generated controllers in this demo.

Centering

You may have noticed that by default each cuboid is half under and half above the plane. That’s intentional. It’s also something I only recently started to do. Why? Because we want to use the containing element of our cuboids as the center of the cuboid. This makes animation easier. Especially, if we’re considering rotating around the Z-axis. I found this out when creating “CSS is Cake”. After making the cake, I then decided I wanted each slice to be interactive. I then had to go back and change my implementation to fix the rotation center of the flipping slice.

https://codepen.io/jh3y/pen/KKVGoGJ

Here I’ve broken that demo down to show the centers and how having an offset center would affect the demo.

Positioning

If we are working with a scene that’s more complex, we may split it up into different sections. This is where the concept of sub-planes comes in handy. Consider this demo where I’ve recreated my personal workspace.

https://twitter.com/jh3yy/status/1310658720746045440?s=20

There’s quite a bit going on here and it’s hard to keep track of all the cuboids. For that, we can introduce sub-planes. Let’s break down that demo. The chair has its own sub-plane. This makes it easier to move it around the scene and rotate it — among other things — without affecting anything else. In fact, we can even spin the top without moving the feet!

Aesthetics

Once we’ve got a structure, it’s time to work on the aesthetics. This all depends on what you’re making. But you can get some quick wins from using certain techniques. I tend to start by making things “ugly” then go back and make CSS variables for all the colors and apply them. Three shades for a certain thing allows us to differentiate the sides of a cuboid visually. Consider this toaster example. Three shades cover the sides of the toaster:

https://codepen.io/jh3y/pen/KKVjLrx

Our Pug mixin from earlier allows us to define class names for a cuboid. Applying color to a side usually looks something like this:

/* The front face uses a linear-gradient to apply the 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);
}

It’s a little tricky to include extra elements with our Pug mixin. But let’s not forget, every side to our cuboid offers two pseudo-elements. We can use these for various details. For example, the toaster slot and the slot for the handle on the side are pseudo-elements.

Another trick is to use background-image for adding details. For example, consider the 3D workspace. We can use background layers to create shading. We can use actual images to create textured surfaces. The flooring and the rug are a repeating background-image. In fact, using a pseudo-element for textures is great because then we can transform them if needed, like rotating a tiled image. I’ve also found that I get flickering in some cases working directly with a cuboid side.

https://codepen.io/jh3y/pen/XWdQBRx

One issue with using an image for texture is how we create different shades. We need shades to differentiate the different sides. That’s where the filter property can help. Applying a brightness``() filter to the different sides of a cuboid can lighten or darken them. Consider this CSS flipping table. All the surfaces are using a texture image. But to differentiate the sides, brightness filters are applied.

https://codepen.io/jh3y/pen/xJXvjP

Smoke and mirrors perspective

How about shapes — or features we want to create that seem impossible — using a finite set of elements? Sometimes we can trick the eye with a little smoke and mirrors. We can provide a “faux” like sense of 3D. The Zdog library does this well and is a good example of this.

Consider this bundle of balloons. The strings holding them use the correct perspective and each has its own rotation, tilt, etc. But the balloons themselves are flat. If we rotate the plane, the balloons maintain the counter plane rotation. And this gives that “faux” 3D impression. Try out the demo and switch off the countering.

https://codepen.io/jh3y/pen/NWNVgJw

Sometimes it takes a little out-of-the-box thinking. I had a house plant suggested to me as I built the 3D workspace. I have a few in the room. My initial thought was, “No, I can make a square pot, and how would I make all the leaves?” Well actually, we can use some eye tricks on this one too. Grab a stock image of some leaves or a plant. Remove the background with a tool like remove.bg. Then position many images in the same spot but rotate them each a certain amount. Now, when they’re rotated, we get the impression of a 3D plant.

Tackling awkward shapes

Awkward shapes are tough to cover in a generic way. Every creation has its own hurdles. But, there is a couple of examples that could help give you ideas for tackling things. I recently read an article about the UX of LEGO interface panels. In fact, approaching 3D CSS work like it’s a LEGO set isn’t a bad idea. But the LEGO interface panel is a shape we could make with CSS (minus the studs — I only recently learned this is what they are called). It’s a cuboid to start with. Then we can clip the top face, make the end face transparent, and rotate a pseudo-element to join it up. We can use the pseudo-element for adding the details with some background layers. Try turning the wireframe on and off in the demo below. If we want the exact heights and angles for the faces, we can use some math to workout the hypoteneuse etc.

Another awkward thing to cover is curves. Spherical shapes are not in the CSS wheelhouse. We have various options at this point. One option is to embrace that fact and create polygons with a finite number of sides. Another is to create rounded shapes and use the rotation method we mentioned with the plant. Each of these options could work. But again, it’s on a use case basis. Each has pros and cons. With the polygon, we surrender the curves or use so many elements that we get an almost curve. The latter could result in performance issues. With the perspective trick, we may also end up with performance issues depending. We also surrender being able to style the “sides” of the shape as there aren’t any.

Z fighting

Last, but not least, it’s worth mentioning “Z-fighting.” This is where certain elements on a plane may overlap or cause an undesirable flicker. It’s hard to give good examples of this. There’s not a generic solution for it. It’s something to tackle on a case-by-case basis. The main strategy is to order things in the DOM as appropriate. But sometimes that’s not the only issue.

Being accurate can sometimes cause issues. Let’s refer to the 3D workspace again. Consider the canvas on the wall. The shadow is a pseudo-element. If we place the canvas exactly against the wall, we are going to hit issues. If we do that, the shadow and the wall are going to fight for the front position. To combat this, we can translate things by a slight amount. That will solve the issue and declare what should sit in front.

Try resizing this demo with the “Canvas offset” on and off. Notice how the shadow flickers when there is no offset? That’s because the shadow and the wall are fighting for view. The offset sets the --x to a fraction of 1vmin that we’ve named --cm. That’s a responsive unit being used for that creation.

That’s “it”!

Take your CSS to another dimension. Use some of my tips, create your own, share them, and share your 3D creations! Yes, making 3D things in CSS can be tough and is definitely a process that we can refine as we go along. Different approaches work for different people and patience is a required ingredient. I’m interested to see where you take your approach!

The most important thing? Have fun with it!


CSS in 3D: Learning to Think in Cubes Instead of Boxes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-in-3d-learning-to-think-in-cubes-instead-of-boxes/feed/ 4 323488
ztext.js https://css-tricks.com/ztext-js/ https://css-tricks.com/ztext-js/#comments Tue, 22 Sep 2020 14:56:46 +0000 https://css-tricks.com/?p=321292 Super cool project from Bennett Feely! It makes any web type into 3D lettering with a mouseover effect that moves the 3D objects in space. It’s reminiscent of Zdog, but for type. It works its magic by stacking a …


ztext.js originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Super cool project from Bennett Feely! It makes any web type into 3D lettering with a mouseover effect that moves the 3D objects in space. It’s reminiscent of Zdog, but for type. It works its magic by stacking a bunch of copies of the glyphs on top of each other that are offset by some translateZ, then using some perspective and rotateX/rotateY on a parent element to do the interactive stuff.

The effect is extremely fun. I can’t believe Fisher-Price hasn’t already implemented it site-wide.

Accessibility-wise, I have some questions. Even in the <h1> on the website, it turns into eight <h1> elements, which I can’t image is super great for screen readers, not to mention the slew of <span> elements inside. I’d think you could mitigate some of the problem with a single parent wrapper using an aria-label, yes?

Copy and paste also has weird results. If I copy, like, straight across a word, I tend to get just what I want. But if I copy from before the word to after it, I’ll get all the extra copies, which I definitely do not want. Maybe that’s fixable with some user-select: none; dancing.

It’s not just type, either! Bennet’s example on an <img> is neat in how it makes like printing a photo on (real world) canvas and stretching it around the frame so that even the edges have color.

My favorite is how it looks on <svg> though. So cool.

To Shared LinkPermalink on CSS-Tricks


ztext.js originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/ztext-js/feed/ 2 321292
The Mad Magazine Fold-In Effect in CSS https://css-tricks.com/the-mad-magazine-fold-in-effect-in-css/ https://css-tricks.com/the-mad-magazine-fold-in-effect-in-css/#respond Thu, 25 Jun 2020 20:13:44 +0000 https://css-tricks.com/?p=314419 This was always my favorite thing in Mad magazine. One page (the inside of the back cover, I think) was covered in a zany illustration. You folded that page in thirds, covering up the middle-third of that image, and a …


The Mad Magazine Fold-In Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This was always my favorite thing in Mad magazine. One page (the inside of the back cover, I think) was covered in a zany illustration. You folded that page in thirds, covering up the middle-third of that image, and a new image would form because the illustration was designed to perfectly line up with those folds. The new image (and text!) was part of the joke.

Every one was a clever trick, so of course, I’m delighted to see that trick make it’s way to CSS, courtesy of Thomas Park.

I’m pretty surprised Thomas was able to do it with a single state (:hover / :active) . I would have bet a nickel that it would have needed @keyframes to adjust the 3D transforms into different positions during the animation, but it looks like multiple transitions happening (both parent and child) handle that.


If you’re in the mood for other cool CSS paper effects…

Here’s a new one from Lynn Fisher:

A classic from Mandy Michael:

And more folding from Mattia Astorino:

To Shared LinkPermalink on CSS-Tricks


The Mad Magazine Fold-In Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/the-mad-magazine-fold-in-effect-in-css/feed/ 0 314419
Zdog https://css-tricks.com/zdog/ https://css-tricks.com/zdog/#comments Wed, 24 Jul 2019 18:04:45 +0000 http://css-tricks.com/?p=292135 David DeSandro has loads of super cool JavaScript libraries he’s created over the years. His latest is Zdog, a “round, flat, designer-friendly pseudo-3D engine for canvas & SVG.” It’s only been about a month since he dropped it (although, …


Zdog originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
David DeSandro has loads of super cool JavaScript libraries he’s created over the years. His latest is Zdog, a “round, flat, designer-friendly pseudo-3D engine for canvas & SVG.” It’s only been about a month since he dropped it (although, like any good library, it’s been simmering) and it has been pretty incredible to watch it capture the imagination of loads of designers and developers.

I’m shouting out Zdog here because I think it’s worth y’all checking out. There is something about the API and how easy it becomes to draw in a 3D space that is clicking with folks. It might just click with you! Having a little moment like that is what web dev is all about ;).

See the Pen
Zdog Spaceship
by Ryan Mulligan (@hexagoncircle)
on CodePen.

The Official Site

(And GitHub repo.)

See the Pen
Ztocat
by Jessica Paoli (@skullface)
on CodePen.

CodePen Collections

Here is a collection from Dave that gathers community-built examples:

And here’s is a Collection from Dave himself of demos he created while building the library.

See the Pen
Zdog and Goo
by Chris Gannon (@chrisgannon)
on CodePen.

There is a lot of cool stuff all around the web with Zdog, like this idea of plotting data with it from Zach:

Plus a font!!

See the Pen
Zfont Advanced Demo
by James Daniel (@rakujira)
on CodePen.

CodePen Topic

We created a Topic on CodePen to help explore the library and find examples as well.

We also covered it over on the CodePen Blog when the library dropped because it was so dang exciting.

(By the way, if you have a good idea for a “Topic” on CodePen, let me know! The main thing we need is a good strong Collection of “starter” Pens to help people understand concepts and use the library, plus a Collection of strong, complete examples. I have some cool swag and stuff I could send your way if you’re into the idea of helping.)


Zdog originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/zdog/feed/ 2 292135
Creating Photorealistic 3D Graphics on the Web https://css-tricks.com/creating-photorealistic-3d-graphics-web/ https://css-tricks.com/creating-photorealistic-3d-graphics-web/#comments Fri, 04 Aug 2017 13:40:42 +0000 http://css-tricks.com/?p=257036 Before becoming a web developer, I worked in the visual effects industry, creating award-winning, high-end 3D effects for movies and TV Shows such as Tron, The Thing, Resident Evil, and Vikings. To be able to create these effects, we …


Creating Photorealistic 3D Graphics on the Web originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Before becoming a web developer, I worked in the visual effects industry, creating award-winning, high-end 3D effects for movies and TV Shows such as Tron, The Thing, Resident Evil, and Vikings. To be able to create these effects, we would need to use highly sophisticated animation software such as Maya, 3Ds Max or Houdini and do long hours of offline rendering on Render Farms that consisted of hundreds of machines. It’s because I worked with these tools for so long that I am now amazed by the state of the current web technology. We can now create and display high-quality 3D content right inside the web browser, in real time, using WebGL and Three.js.

Here is an example of a project that is built using these technologies. You can find more projects that use three.js on their website.

Some projects using three.js

As the examples on the three.js website demonstrate, 3D visualizations have a vast potential in the domains of e-commerce, retail, entertainment, and advertisement.

WebGL is a low-level JavaScript API that enables creation and display of 3D content inside the browser using the GPU. Unfortunately, since WebGL is a low-level API, it can be a bit hard and tedious to use. You need to write hundreds of lines of code to perform even the simplest tasks. Three.js, on the other hand, is an open source JavaScript library that abstracts away the complexity of WebGL and allows you to create real-time 3D content in a much easier manner.

In this tutorial, I will be introducing the basics of the three.js library. It makes sense to start with a simple example to communicate the fundamentals better when introducing a new programming library but I would like to take this a step further. I will also aim to build a scene that is aesthetically pleasant and even photorealistic to a degree.

We will just start out with a simple plane and sphere but in the end it will end up looking like this:

See the Pen learning-threejs-final by Engin Arslan (@enginarslan) on CodePen.

Photorealism is the pinnacle of computer graphics but achieving is not necessarily a factor of the processing power at your disposal but a smart deployment of techniques from your toolbox. Here are a few techniques that you will be learning about in this tutorial that will help your scenes achieve photo-realism.

  • Color, Bump and Roughness Maps.
  • Physically Based Materials.
  • Lighting with Shadows.
Photorealistic 3D portrait by Ian Spriggs

The basic 3D principles and techniques that you will learn here are relevant in any other 3D content creation environment whether it is Blender, Unity, Maya or 3Ds Max.

This is going to be a long tutorial. If you are more of a video person or would like to learn more about the capabilities of three.js you should check out my video training on the subject from Lynda.com.

Requirements

When using three.js, if you are working locally, it helps to serve the HTML file through a local server to be able to load in scene assets such as external 3D geometry, images, etc. If you are looking for a server that is easy to setup, you can use Python to spin up a simple HTTP Server. Python is pre-installed on many operating systems.

You don’t have to worry about setting a local dev server to follow this tutorial though. You will instead rely on data URL’s to load in assets like images to remove the overhead of setting up a server. Using this method you will be able to easily execute your three.js scene in online code editors such as CodePen.

This tutorial assumes a prior, basic to intermediate, knowledge of JavaScript and some understanding of front-end web development. If you are not comfortable with JavaScript but want to get started with it in an easy manner you might want to check out the course/book “Coding for Visual Learners: Learning JavaScript with p5.js”. (Disclaimer: I am the author)

Let’s get started with building 3D graphics on the Web!

Getting Started

I have already prepared a Pen that you can use to follow this tutorial with.

The HTML code that you will be using is going to be super simple. It just needs to have a div element to host the canvas that is going the display the 3D graphics. It also loads up the three.js library (release 86) from a CDN.

<div id="webgl"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/86/three.min.js"></script> 

Codepen hides some of the HTML structure that is currently present for your convenience. If you were building this scene on some other online editors or on your local your HTML would need to have something like this code below where main.js would be the file that would hold the JavaScript code.

<!DOCTYPE html>
<html>
<head>
    <title>Three.js</title>
    <style type="text/css">
        html, body {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <div id="webgl"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/86/three.min.js"></script>
    <script src="./main.js"></script>
</body>
</html>

Notice the simple CSS declaration inside the HTML. This is what you would have in the CSS tab of Codepen:

html, body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

This is to ensure that you don’t have any margin and padding values that might be applied by your browser and you don’t get a scrollbar so that you can have the graphics fill the entire screen. This is all we need to get started with building 3D Graphics.

Part 1 – Three.js Scene Basics

When working with three.js and with 3D in general, there are a couple of required objects you need to have. These objects are scene, camera and the renderer.

First, you should create a scene. You can think of a scene object as a container for every other 3D object that you are going to work with. It represents the 3D world that you will be building. You can create the scene object by doing this:

 var scene = new THREE.Scene();

Another thing that you need to have when working with 3D is the camera. Think of camera like the eyes that you will be viewing this 3D world through. When working with a 2D visualization, the concept of a camera usually doesn’t exist. What you see is what you get. But in 3D, you need a camera to define your point of view as there are many positions and angles that you could be looking at a scene from. A camera doesn’t only define a position but also other information like the field of view or the aspect ratio.

var camera = new THREE.PerspectiveCamera(
    45, // field of view
    window.innerWidth / window.innerHeight, // aspect ratio
    1, // near clipping plane (beyond which nothing is visible)
    1000 // far clipping plane (beyond which nothing is visible)
);

The camera captures the scene for display purposes but for us to actually see anything, the 3D data needs to be converted into a 2D image. This process is called rendering and you need a renderer to render the scene in three.js. You can initialize a renderer like this:

var renderer = new THREE.WebGLRenderer();

And then set the size of the renderer. This will dictate the size of the output image. You will make it cover the window size.

renderer.setSize(window.innerWidth, window.innerHeight);

To be able to display the results of the render you need to append the domElement property of the renderer to your HTML content. You will use the empty div element that you created that has the id webgl for this purpose.

document.getElementById('webgl').appendChild(renderer.domElement);

And having done all this you can call the render method on the renderer by providing the scene and the camera as the arguments.

renderer.render(
    scene,
    camera
);

To have things a bit tidier, put everything inside a function called init and execute that function.

init();

And now you would see nothing… but a black screen. Don’t worry, this is normal. The scene is working but since you didn’t include any objects inside the scene, what you are looking at is basically empty space. Next, you will be populating this scene with 3D objects.

See the Pen learning-threejs-01 by Engin Arslan (@enginarslan) on CodePen.

Adding Objects to the Scene

Geometric objects in three.js are made up of two parts. A geometry that defines the shape of the object and a material that defines the surface quality, the appearance, of the object. The combination of these two things makes up a mesh in three.js which forms the 3D object.

Three.js allows you to create some simple shapes like a cube or a sphere in an easy manner. You can create a simple sphere by providing the radius value.

var geo = new THREE.SphereGeometry(1);

There are various kinds of materials that you could use on geometries. Materials determine how an object reacts to the scene lighting. We can use a material to make an object reflective, rough, transparent, etc.. The default material that three.js objects are created with is the MeshBasicMaterial. MeshBasicMaterial is not affected by the scene lighting at all. This means that your geometry is going to be visible even when there is no lighting in the scene. You can pass an object with a color property and a hex value to the MeshBasicMaterial to be able to set the desired color for the object. You will use this material for now but later update it to have your objects be affected by the scene lighting. You don’t have any lighting in the scene for now so MeshBasicMaterial should be a good enough choice.

var material = new THREE.MeshBasicMaterial({
        color: 0x00ff00
});

You can combine the geometry and material to create a mesh which is going to form the 3D object.

var mesh = new THREE.Mesh(geometry, material);

Create a function to encapsulate this code that creates a sphere. You won’t be creating more than one sphere in this tutorial but it is still good to keep things neat and tidy.

function getSphere(radius) {
    var geometry = new THREE.SphereGeometry(radius);
    var material = new THREE.MeshBasicMaterial({
        color: 0x00ff00
    });
    var sphere = new THREE.Mesh(geometry, material);
    return sphere;
}

var sphere = getSphere(1);

Then you need to add this newly created object to the scene for it to be visible.

scene.add(sphere);

Let’s check out the scene again. You will still see a black screen.

See the Pen learning-threejs-02 by Engin Arslan (@enginarslan) on CodePen.

The reason why you don’t see anything right now is that whenever you add an object to the scene in three.js, the object gets placed at the center of the scene, at the coordinates of 0, 0, 0 for x, y and z. This simply means that you currently have the camera and the sphere at the same position. You should change the position of either one of them to be able to start seeing things.

3D coordinates

Let’s move the camera 20 units on the z axis. This is achieved by setting the position.z property on the camera. 3D objects have position, rotation and scale properties that would allow you to transform them into the 3D space.

camera.position.z = 20;

You could move the camera on other axises as well.

camera.position.x = 0;
camera.position.y = 5;
camera.position.z = 20;

The camera is positioned higher now but the sphere is not at the center of the frame anymore. You need to point the camera to it. To be able to do so, you can call a method on the camera called lookAt. The lookAt method on the camera determines which point the camera is looking at. The points in the 3D space are represented by Vectors. So you can pass a new Vector3 object to this lookAt method to be able to have the camera look at the 0, 0, 0 coordinates.

camera.lookAt(new THREE.Vector3(0, 0, 0));

The sphere object doesn’t look too smooth right now. The reason for that is the SphereGeometry function actually accepts two additional parameters, the width and height segments, that affects the resolution of the surface. Higher these values, smoother the curved surfaces will appear. I will set this value to be 24 for width and height segments.

var geo = new THREE.SphereGeometry(radius, 24, 24);

See the Pen learning-threejs-03 by Engin Arslan (@enginarslan) on CodePen.

Now you will create a simple plane geometry for the sphere to be sitting on. PlaneGeometry function requires a width and height parameter. In 3D, 2D objects don’t have both of their sides rendering by default so you need to pass a side property to the material to have both sides of the plane geometry to render.

function getPlane(w, h) {
  var geo = new THREE.PlaneGeometry(w, h);
  var material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    side: THREE.DoubleSide,
  });
  var mesh = new THREE.Mesh(geo, material);

  return mesh;
}

You can now add this plane object to the scene as well. You will notice that the initial rotation of the plane geometry is parallel to the y-axis but you will likely need it to be horizontal for it to act as a ground plane. There is one important thing you should keep in mind regarding the rotations in three.js though. They use radians as a unit, not degrees. A rotation of 90 degrees in radians is equivalent to Math.PI/2.

var plane = getPlane(50, 50);
scene.add(plane);
plane.rotation.x = Math.PI/2;

When you created the sphere object, it got positioned using its center point. If you would like to move it above the ground plane then you can just increase its position.y value by the current radius amount. But that wouldn’t be a programmatic way of doing things. If you would like the sphere to stay on the plane whatever its radius value is, you should make use of the radius value for the positioning.

sphere.position.y = sphere.geometry.parameters.radius;

See the Pen learning-threejs-04 by Engin Arslan (@enginarslan) on CodePen.

Animations

You are almost done with the first part of this tutorial. But before we wrap it up, I want to illustrate how to do animations in three.js. Animations in three.js make use of the requestAnimationFrame method on the window object which repeatedly executes a given function. It is somewhat like a setInterval function but optimized for the browser drawing performance.

Create an update function and pass the renderer, scene, and camera in there to execute the render method of the renderer inside this function. You will also declare a requestAnimationFrame function inside there and call this update function recursively from a callback function that is passed to the requestAnimationFrame function. It is better to illustrate this in code than to write about it.

function update(renderer, scene, camera) {
  renderer.render(scene, camera);

  requestAnimationFrame(function() {
    update(renderer, scene, camera);
  });
}

Everything might look same to you at this point but the core difference is that the requestAnimationFrame function is making the scene render around 60 frames per second with a recursive call to the update function. Which means that if you are to execute a statement inside the update function, that statement would get executed at around 60 times per second. Let’s add a scaling animation to the sphere object. To be able to select the sphere object from inside the update function you could pass it as an argument but we will use a different technique. First, set a name attribute on the sphere object and give it a name of your liking.

sphere.name = 'sphere';

Inside the update function, you could find this object using its name by using the getObjectByName method on its parent object, the scene.

var sphere = scene.getObjectByName('sphere');
sphere.scale.x += 0.01;
sphere.scale.z += 0.01;

With this code, the sphere is now scaling on its x and z axises. Our intention is not to create a scaling sphere though. We are setting up the update function so that you can leverage for different animations later on. Now that you have seen how it works you can remove this scaling animation.

See the Pen learning-threejs-05 by Engin Arslan (@enginarslan) on CodePen.

Part 2 – Adding Realism to the Scene

Currently, we are using MeshBasicMaterial which displays the given color even when there is no lighting in the scene which results in a very flat look. Real-world materials don’t work this way though. The visibility of the surface in the real world depends on how much light is reflecting back from the surface back to our eyes. Three.js comes with a couple of different materials that provide a better approximation of how real-world surfaces behave and one of them is the MeshStandardMaterial. MeshStandardMaterial is a physically based rendering material that can help you achieve photorealistic results. This is the kind of material that modern game engines like Unreal or Unity use and is an industry standard in gaming and visual effects.

Let’s start using the MeshStandardMaterial on our objects and change the color of the materials to white.

var material = new THREE.MeshStandardMaterial({
  color: 0xffffff,
});

You will once again get a black render at this point. That is normal. For objects to be visible we need to have lights in the scene. This wasn’t a requirement with MeshBasicMaterial as it is a simple material that displays the given color at all conditions but other materials require an interaction with light to be visible. Let’s create a SpotLight creating function. You will be creating two spotlights using this function.

function getSpotLight(color, intensity) {
  var light = new THREE.SpotLight(color, intensity);

  return light;
}

var spotLight_01 = getSpotlight(0xffffff, 1);
scene.add(spotLight_01);

You might start seeing something at this point. Position the light and the camera a bit differently for a better framing and shading. Also create a secondary light as well.

var spotLight_02 = getSpotlight(0xffffff, 1);
scene.add(spotLight_02);

camera.position.x = 0;
camera.position.y = 6;
camera.position.z = 6;

spotLight_01.position.x = 6;
spotLight_01.position.y = 8;
spotLight_01.position.z = -20;

spotLight_02.position.x = -12;
spotLight_02.position.y = 6;
spotLight_02.position.z = -10;

Having done this you have two light sources in the scene, illuminating the sphere from two different positions. The lighting is helping a bit in understanding the dimensionality of the scene, but things are still looking extremely fake at this point because the lighting is missing a critical component: the shadows!

Rendering a shadow in Three.js is unfortunately not too straightforward. This is because shadows are computationally expensive and we need to activate shadow rendering on multiple places. First, you need to tell the renderer to start rendering shadows:

var renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;

Then you need to tell the light to cast shadows. Do that in the getSpotLight function.

light.castShadow = true;

You should also tell the objects to cast and/or receive shadows. In this case, you will make the sphere cast shadows and the plane to receive shadows.

mesh.castShadow = true;
mesh.receiveShadow = true;

After all these settings we should start seeing shadows in the scene. Initially, they might be a bit lower quality. You can increase the resolution of the shadows by setting the light shadow map size.

light.shadow.mapSize.x = 4096;
light.shadow.mapSize.y = 4096;

MeshStandardMaterial has a couple of properties such as the roughness and metalness that controls the interaction of the surface with the light. The properties take values in between 0 and 1 and they control the corresponding behavior of the surface. Increase the roughness value on the plane material to 1 to see the surface look more like a rubber as the reflections get blurrier.

// material adjustments
var planeMaterial = plane.material;
planeMaterial.roughness = 1;

We won’t be using 1 as a value in this tutorial though. Feel free to experiment with values but set it back to 0.65 for roughness and 0.75 for metalness.

planeMaterial.roughness = 0.65;
planeMaterial.metalness = 0.75;

Even though the scene should be looking much more promising right now it is still hard to call it realistic. The truth is, it is very hard to establish photorealism in 3D without using texture maps.

See the Pen learning-threejs-06 by Engin Arslan (@enginarslan) on CodePen.

Texture Maps

Texture maps are 2D images that can be mapped on a material for the purpose of providing surface detail. So far you were only getting solid colors on the surfaces but using a texture map you can map any image you would like on a surface. Texture maps are not only used to manipulate the color information of surfaces but they can also be used to manipulate other qualities of the surface like reflectiveness, shininess, roughness, etc.

Textures can be derived from photographic sources or can also be painted from scratch as well. For a texture to be useful in a 3D context it should be captured in a certain manner. Images that have reflections or shadows in them, or images where the perspective is too distorted wouldn’t make great texture maps. There are several dedicated websites for finding textures online. One of them is textures.com which has a pretty good archive. They have some free download options but requires you to register to be able to do so. Another website for 3D textures is Megascans which does high resolution, high-quality environment scans that are of high-end production quality.

I have used a website called mb3d.co.uk for this example. This site provides seamless, free to use textures. A seamless texture implies a texture that can be repeated on the surface many times without having any discontinuations where the edges meet. This is the link to the texture file that I have used. I have decreased the size to 512px for width and height and converted the image file to data URI using an online service called ezgif to be able to include it as part of the JavaScript code as opposed to loading it in as a separate asset. (hint: don’t include tags as you are outputting the data if you are to use this service)

Create a function that returns the data URI we have generated so that we don’t have to put that huge string in the middle of our code.

function getTexture() {
    var data = 'data:image/jpeg;base64,/...'; // paste your data URI inside the quotation marks.
    return data
}

Next, you need to load in the texture and apply it on the plane surface. You will be using the three.js TextureLoader for this purpose. After loading in the texture you will load in the texture to the map property of the desired material to have it as a color map on the surface.

var textureLoader = new THREE.TextureLoader();
var texture = textureLoader.load(getTexture());
planeMaterial.map = texture;

Things would be looking rather ugly right now as the texture on the surface is pixelated. The image is stretching too much to cover the entire surface. What you can do is to make the image repeat itself instead of scaling so that it doesn’t get as pixelated. To do so, you need to set the wrapS and wrapT properties on the desired map to THREE.RepeatWrapping and specify a repetition value. Since you will be doing this for other kinds of maps as well (like bump or roughness map) it is better to create a loop for this:

var repetition = 6;
var textures = ['map']// we will add 'bumpMap' and 'roughnessMap'
textures.forEach((mapName) => {
  planeMaterial[mapName].wrapS = THREE.RepeatWrapping;
  planeMaterial[mapName].wrapT = THREE.RepeatWrapping;
  planeMaterial[mapName].repeat.set(repetition, repetition);
});

This should look much better. Since the texture you are using is seamless you wouldn’t notice any disconnections around the edges where the repetition happens.

Loading of a texture is actually an asynchronous operation. This means that your 3D scene is generated before the image file is loaded in. But since you are continuously rendering the scene using requestAnimationFrame this doesn’t cause any issues in this example. If you weren’t doing this, you would need to use callbacks or other async methods to manage the loading order.

See the Pen learning-threejs-07 by Engin Arslan (@enginarslan) on CodePen.

Other Texture Maps

As mentioned in the previous chapter, textures are not only used to define the color of the surfaces but to define other qualities of it as well. One other way that textures can be used as are bump maps. When used as a bump map, the brightness values of the texture simulates a height effect.

planeMaterial.bumpMap = texture;

Bump map should also be using the same repetition configuration as the color map so include it in the textures array.

var textures = ['map', 'bumpMap'];

With a bump map, brighter the value of a pixel, higher the corresponding surface would look. But a bump map doesn’t actually change the surface, it just manipulates how the light interacts with the surface to create an illusion of uneven topology. The bump amount looks a bit too much right now. Bump maps work best when they are used in subtle amounts. So let’s change the bumpScale parameter to something lower for a more subtle effect.

planeMaterial.bumpScale = 0.01;

Notice how this texture made a huge difference in appearance. The reflections are not perfect anymore but nicely broken up as they would be in real life. Another kind of map slot that is available to the StandardMaterial is the roughness map. A texture map used as a roughness map allows you to control the sharpness of the reflections using the brightness values of a given image.

planeMaterial.roughnessMap = texture;
var textures = ['map', 'bumpMap', 'roughnessMap'];

According to the three.js documentation, the StandardMaterial works best when used in conjunction with an environment map. An environment map simulates a distant environment reflecting off of the reflective surfaces in the scene. It really helps when you are trying to simulate reflectivity on objects. Environment maps in three.js are in the form of cube maps. A Cube map is a panoramic view of a scene that is mapped inside a cube. A cube map is made up of 6 separate images that correspond to each face of a cube. Since loading 6 mode images inside an online editor is going to be a bit too much work you won’t actually be using an environment map in this example. But to be able to make this sphere object a bit more interesting, add a roughness map to it as well. You will be using this texture but 320x320px in size and as a data URI.

Create a new function called getMetalTexture

function getMetalTexture() {
    var data = 'data:image/jpeg;base64,/...'; // paste your data URI inside the quotation marks.
    return data
}

And apply it on the sphere material as bumpMap and roughnessMap:

var sphereMaterial = sphere.material;
var metalTexture = textureLoader.load(getMetalTexture());

sphereMaterial.bumpMap = metalTexture;
sphereMaterial.roughnessMap = metalTexture;
sphereMaterial.bumpScale = 0.01;
sphereMaterial.roughness = 0.75;
sphereMaterial.metalness = 0.25;

See the Pen learning-threejs-08 by Engin Arslan (@enginarslan) on CodePen.

Wrapping it up!

You are almost done! Here you will do just a couple of small tweaks. You can see the final version of this scene file in this Pen.

Provide a non-white color to the lights. Notice how you can actually use CSS color values as strings to specify color:

var spotLight_01 = getSpotlight('rgb(145, 200, 255)', 1);
var spotLight_02 = getSpotlight('rgb(255, 220, 180)', 1);

And add some subtle random flickering animation to the lights to add some life to the scene. First, assign a name property to the lights so you can locate them inside the update function using the getObjectByName method.

spotLight_01.name = 'spotLight_01';
spotLight_02.name = 'spotLight_02';

And then create the animation inside the update function using the Math.random() function.

var spotLight_01 = scene.getObjectByName('spotLight_01');
spotLight_01.intensity += (Math.random() - 0.5) * 0.15;
spotLight_01.intensity = Math.abs(spotLight_01.intensity);

var spotLight_02 = scene.getObjectByName('spotLight_02');
spotLight_02.intensity += (Math.random() - 0.5) * 0.05;
spotLight_02.intensity = Math.abs(spotLight_02.intensity);

And as a bonus, inside the scene file, I have included the OrbitControls script for the three.js camera which means that you can actually drag your mouse on the scene to interact with the camera! I have also made it so that the scene resizes with the changing window size. I have achieved this using an external script for convenience.

See the Pen learning-threejs-final by Engin Arslan (@enginarslan) on CodePen.

Now, this scene is somewhat close to becoming photorealistic. There are still many missing pieces though. The sphere ball is too dark due to lack of reflections and ambient lighting. The ground plane is looking too flat in the glancing angles. The profile of the sphere is too perfect – it is CG (Computer Graphics) perfect. The lighting is not actually as realistic as it could be; It doesn’t decay (lose intensity) with the distance from the source. You should also probably add particle effects, camera animation, and post-processing filters if you want to go all the way with this. But this still should be a good enough example to illustrate the power of three.js and the quality of graphics that you can create inside the browser. For more information on what you could achieve using this amazing library, you should definitely check out my new course on Lynda.com about the subject!

Thanks for making it this far! Hope you enjoyed this write-up and feel free to reach to me @inspiratory on Twitter or on my website with any questions you might have!


Creating Photorealistic 3D Graphics on the Web originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-photorealistic-3d-graphics-web/feed/ 2 257036
Solving the Last Item Problem for a Circular Distribution with Partially Overlapping Items https://css-tricks.com/solving-last-item-problem-circular-distribution-partially-overlapping-items/ https://css-tricks.com/solving-last-item-problem-circular-distribution-partially-overlapping-items/#comments Mon, 22 May 2017 09:20:53 +0000 http://css-tricks.com/?p=254774 Let’s say we wanted to have something like this:

Clockwise circular (cyclic) distribution with partially overlapping items.

At first, this doesn’t seem too complicated. We start with 12 numbered items:

- 12.times do |i|
  .item #{i}

We give these items …


Solving the Last Item Problem for a Circular Distribution with Partially Overlapping Items originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s say we wanted to have something like this:

Clockwise circular (cyclic) distribution with twelve partially overlapping square items. Every item's top left corner is underneath the previous item's bottom left corner
Clockwise circular (cyclic) distribution with partially overlapping items.

At first, this doesn’t seem too complicated. We start with 12 numbered items:

- 12.times do |i|
  .item #{i}

We give these items dimensions, position them absolutely in the middle of their container, give them a background, a box-shadow (or a border) and tweak the text-related properties a bit so that everything looks nice.

$d: 2em;

.item {
  position: absolute;
  margin: calc(50vh - #{.5*$d}) 0 0 calc(50vw - #{.5*$d});
  width: $d; height: $d;
  box-shadow: inset 0 0 0 4px;
  background: gainsboro;
  font: 900 2em/ #{$d} trebuchet ms, tahoma, verdana, sans-serif;
  text-align: center;
}

So far, so good:

See the Pen by thebabydino (@thebabydino) on CodePen.

Now all that’s left is to distribute them on a circle, right? We get a base angle $ba for our distribution, we rotate each item by its index times this $ba angle and then translate it along its x axis:

$n: 12;
$ba: 360deg/$n;

.item {
  transform: rotate(var(--a, 0deg)) translate(1.5*$d);
	
  @for $i from 1 to $n { &:nth-child(#{$i + 1}) { --a: $i*$ba } }
}

The result seems fine at first:

See the Pen by thebabydino (@thebabydino) on CodePen.

However, on closer inspection, we notice that we have a problem: item 11 is above both item 0 and item 10, while item 0 is below both item 1 and 11:

Highlighting the issue we encounter with our circular distribution using the above code. The last item (11), ends up both over one before it (10) and over the one after (0), while the first item (0) is both under the one before it (11) and under the one after it (1).
Highlighting the issue we encounter with our circular distribution.

There are a number of ways to get around this, but they feel kind of hacky and tedious because they involve either duplicating elements, cutting corners with clip-path, adding pseudo-elements to cover the corners or cut them out via overflow. Some of these are particularly inefficient if we also need to animate the position of the items or if we want the items to be semi transparent.

So, what’s the best solution then?

3D to the rescue! A really neat thing we can do in this case is to rotate these items in 3D such that their top part goes towards the back (behind the plane of the screen) and their bottom part comes forward (in front of the plane of the screen). We do this by chaining a third transform function – a rotateX():

transform: rotate(var(--a, 0deg)) translate(1.5*$d) rotateX(40deg)

At this point, nothing seems to have changed for the better – we still have the same problem as before and, in addition to that, our items appear to have shrunk along their y axes, which isn’t something we wanted.

See the Pen by thebabydino (@thebabydino) on CodePen.

Let’s tackle these issues one by one. First off, we need to make all our items belong to the same 3D rendering context and we do this by setting transform-style: preserve-3d on their parent (which in this case happens to be the body element).

The result after ensuring all our items are within the same 3D rendering context: they are all in the correct order, with every item's top left corner underneath the bottom left corner of the previous item.
The result after ensuring all our items are within the same 3D rendering context (live demo).

Those on current Firefox may have noticed we have a different kind of issue now. Item 8 appears both above the previous one (7) and above the next one (9), while item 7 appears both below the previous one (6) and below the next one (8).

Screenshot illustrating the Firefox issue described above.
Screenshot illustrating the Firefox issue.

This doesn’t happen in Chrome or in Edge and it’s due to a known Firefox bug where 3D transformed elements are not always rendered in the correct 3D order. Fortunately, this is now fixed in Nightly (55).

Now let’s move on to the issue of the shrinking height. If we look at the first item from the side after the last rotation, this is what we see:

Geometric illustration. First item and its projection onto the plane of the screen, side view from the + of the x axis. From this point, we see these as two lines, AB and CD, which intersect in the middle, this intersection being the point O. The angle between them is the angle of rotation of each item around its own x axis, 40° in this case.
First item and its projection onto the plane of the screen, side view.

The AB line, rotated at 40° away from the vertical is the actual height of our item (h). The CD line is the projection of this AB line onto the plane of the screen. This is the size we perceive our item’s height to be after the rotation. We want this to be equal to d, which is also equal to the other dimension of our item (its width).

We draw a rectangle whose left edge is this projection (CD) and whose top right corner is the A point. Since the opposing edges in a rectangle are equal, the right edge AF of this rectangle equals the projection d. Since the opposing edges of a rectangle are also parallel, we also get that the ∠OAF (or ∠BAF, same thing) angle equals the ∠AOC angle (they’re alternate angles).

Geometric illustration. We draw a rectangle whose left edge is the CD projection and whose top right corner is the A point.
Creating the CDFA rectangle.

Now let’s remove everything but the right triangle AFB. In this triangle, the AB hypotenuse has a length of h, the ∠BAF angle is a 40° one and the AF cathetus is d.

Geometric illustration focused on the right triangle AFB
The right triangle AFB

From here, we have that the cosine of the ∠BAF angle is d/h:

cos(40°) = d/h → h = d/cos(40°)

So the first thing that comes to mind is that, if we want the projection of our items to look as tall as it is wide, we need to give it a height of $d/cos(40deg). However, this doesn’t fix the squished text or any squished backgrounds, so it’s a better idea to leave it with its initial height: $d and to chain another transform – a scaleY() using a factor of 1/cos(40deg). Even better, we can store the rotation angle into a variable $ax and then we have:

$d: 2em;
$ax: 40deg;

.item {
  transform: rotate(var(--a, 0deg)) translate(1.5*$d) rotateX($ax) scaleY(1/cos($ax));
}

The above changes give us the desired result (well, in browsers that support CSS variables and don’t have 3D order issues):

The final result after fixing the height issue: all items are square again and they are all in the correct order, with every item's top left corner underneath the bottom left corner of the previous item.
The final result after fixing the height issue (live demo).

This method is really convenient because it doesn’t require us to do anything different for any one item in particular and it works nicely, without any other extra tweaks needed, in the case of semitransparent items. However, the above demo isn’t too exciting, so let’s take a look at a few slightly more interesting use cases.

Note that the following demos only work in WebKit browsers, but this is not something related to the method presented in the article, it’s just a result of the currently poor support of calc() for anything other than length values.

The first is a tic toc loader, which is a pure CSS recreation of a gif from the Geometric Animations tumblr. The animation is pretty fast in this case, so it may be a bit hard hard to notice the effect here. It only works in WebKit browsers as Firefox and Edge don’t support calc() as an animation-delay value and Firefox doesn’t support calc() in rgb() either.

Animated gif showing a tic toc loader. Eighteen bars are distributed on a circle, all pointing towards the origin. Every two opposing bars animate at the same time, rotating by half a turn around their own central points. Once they are done, the next pair of opposing bars starts animating.
Tic toc loader (see the live demo, WebKit only)

The second is a sea shell loader, also a pure CSS recreation of a gif from the same Tumblr and also WebKit only for the same reasons as the previous one.

Animated gif showing a sea shell loader. There are two layers, with eighteen bars distributed on identical circles on each layer. All the bars rotate around their own central points at the same time, with those on the layer behind being 90 degrees away from those on the layer in front at all times.
Sea shell loader (see the live demo, WebKit only)

The third demo is a diagram. It only works in WebKit browsers because Firefox and Edge don’t support calc() values inside rotate() functions and Firefox doesn’t support calc() inside hsl() either:

Diagram showing five discs distributed clockwise on a circle, partly overlapping, with each of the discs partly underneath the disc following it.
Diagram (see the live demo, WebKit only)

The fourth is a circular image gallery, WebKit only for the same reason as the diagram above.

Circular image gallery. Image thumbnails are distributed in a similar fashion to the discs in the previous demo, on a circle around the current image. Clicking on a thumbnail selects that image and moves it in the middle where it grows to its natural size, while the previously selected image shrinks and moves back in place on the circle. All images show pictures of Amur leopards.
Circular image gallery (see the live demo, WebKit only)

The fifth and last is another loading animation, this time inspired by the Disc Buddies .gif by Dave Whyte.

Animated gif. 12 discs are distributed on a circle in a similar fashion to the previous demos. The ones on odd positions shift out on another outer layer. The two layers rotate in opposite directions, then the items on the outer layer shift back on the inner layer and then the animation repeats itself.
Disc Buddies loading animation (see the live demo, WebKit only)

Solving the Last Item Problem for a Circular Distribution with Partially Overlapping Items originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/solving-last-item-problem-circular-distribution-partially-overlapping-items/feed/ 13 254774
Simplifying CSS Cubes with Custom Properties https://css-tricks.com/simplifying-css-cubes-custom-properties/ https://css-tricks.com/simplifying-css-cubes-custom-properties/#comments Tue, 16 May 2017 14:39:03 +0000 http://css-tricks.com/?p=254602 I know there are a ton of pure CSS cube tutorials out there. I’ve done a few myself. But for mid-2017, when CSS Custom Properties are supported in all major desktop browsers, they all feel… outdated and very WET. …


Simplifying CSS Cubes with Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I know there are a ton of pure CSS cube tutorials out there. I’ve done a few myself. But for mid-2017, when CSS Custom Properties are supported in all major desktop browsers, they all feel… outdated and very WET. I thought I should do something to fix this problem, so this article was born. It’s going to show you the most efficient path towards building a CSS cube that’s possible today, while also explaining what common, but less than ideal cube coding patterns you should steer clear of. So let’s get started!

HTML structure

The HTML structure is the following: a .cube element with .cube__face children (6 of them). We’re using Haml so that we write the least amount of code possible:

.cube
  - 6.times do
    .cube__face

We’re not using .front, .back and classes like that. They’re not useful because they bloat the code and make it less logical. Instead, we’ll use :nth-child() to target the faces. We don’t need to worry about browser support for that, since we’re building something with 3D transforms here, which assumes much newer browser support!

Basic styles

All these elements are absolutely positioned:

[class*='cube'] { position: absolute }

The .cube is the child of a scene element which is the body in our case because we want to keep things as simple as possible. If we had multiple 3D shapes within the scene and we wanted them to interact in a 3D manner, then our cube would have been a child of that assembly and the assembly would have been a child of the scene.

We make the body cover the entire viewport and set a perspective on it so that whatever is closer looks bigger and whatever is further away looks smaller.

body {
  height: 100vh;
  perspective: 25em
}

Something else that I often like to do when the full-height body is the scene is to set the font-size on the .cube such that it depends on the minimum viewport dimension. This makes our whole cube scale nicely with the viewport if I then set the cube dimensions in em units.

.cube { font-size: 8vmin }

The reason why I’m not setting the cube dimensions directly in vmin units is an Edge bug.

We then give the .cube element a transform-style of preserve-3d so that its cube children don’t get flattened into its plane in case we decide to animate it and we put it in the middle of the scene using top and left offsets. This is the initial positioning of the cube and it’s best to use offsets, not a translate() transform for this. I’ve seen that sometimes people get confused about this because they’ve heard that, for performance reasons, it’s better to use transforms, not offsets… that’s true, but it applies for animating the position, not for the initial positioning. The very simple rule here is: use offsets or margins, whichever is more convenient at that point for initial positioning, use transforms from animating the position starting from that initial position.

.cube {
  top: 50%; left: 50%;
  transform-style: preserve-3d;
}

We then pick a cube edge length and set it as the width and height of the cube faces. We also give the faces a negative margin of minus half the cube edge so that they’re dead in the middle. Again, this is related to the initial positioning the cube faces. We also give them a box-shadow just so that we can see them.

$cube-edge: 8em;

.cube__face {
  margin: -.5*$cube-edge;
  width: $cube-edge; height: $cube-edge;
  box-shadow: 0 0 0 2px;
}

I often see code where transform-style: preserve-3d has been set on everything. That’s unnecessary and a misunderstanding of how preserve-3d works. It’s only necessary to set it on something that’s going to have a 3D transform applied (right away, following user interaction, via an auto-running animation… doesn’t matter how) and has 3D transformed children. In our particular case, that’s just the .cube element. The scene doesn’t get transformed in 3D and the .cube__face elements don’t have children.

Another unnecessary thing I see is setting explicit dimensions on the .cube element. This element isn’t visible. We don’t have any text directly in it, we’re not setting and backgrounds, borders or shadows on it. Its only purpose here is to serve as a container whose position we can animate in order to easily move all its face children at once, in the same way. Not setting any dimensions on this absolutely positioned .cube element means that its dimensions are computed to 0x0, so it’s also pointless to set any %-value offsets on its face children. top: 0 is the exact same thing as top: 50% or as any other percent value for an element whose parent has 0x0 dimensions. The same is valid for all the other offsets (right, bottom, left).

I’ve been asked why not set top and left for the .cube to calc(50% - #{.5*$cube-edge}) and remove the margin from the .cube__face altogether if I care about compacting code so much. Well, that’s because the two don’t really produce the same result, even though the .cube__face elements do end up in the middle of the screen in both cases. To illustrate this, let’s give our .cube element a red box-shadow just so that we can see it and check out the two cases side by side:

See the Pen by thebabydino (@thebabydino) on CodePen.

In the above demo, our .cube element is positioned differently in the two cases. When using the calc() value for its offsets and skipping the margin on its children, its position doesn’t coincide with the middle of the scene anymore, but with the top left corner of its face children. So what? It’s not going to be visible in our actual demo anyway…

While that’s true, a different position also means a different transform-origin. And that changes things if we decide to rotate or scale our .cube (and that’s something we decided we’d do). So consider the following keyframe animation for our cube:

@keyframes rot { to { transform: rotateY(1turn) } }

This is a rotation around the cube’s y axis. The result is not the same for the two cases:

See the Pen by thebabydino (@thebabydino) on CodePen.

In both cases, the faces rotate around the y axis of their parent cube, but the position of this y axis relative to the faces is different. It coincides with the faces’ y axes in the initial case, and with the faces’ left edges in the second case. This is the reason why I’m not bringing the negative margin of the cube faces into the offsets of the parent cube: it would impact animating the cube in 3D.

Building the cube with transforms

What we have in the demos above isn’t a cube yet. In order to do that, we need to position the faces in 3D. There are multiple transform combinations that achieve the same effect, but the most efficient and logical one is to start by rotating the first four faces in increments of 90° around one of the axes in their plane (x or y) and the remaining two faces by ±90° around the other axis in the same plane. Then we chain a translation of half the cube edge length along the axis that’s perpendicular onto their plane (their z) axis.

A very detailed explanation of how translations and rotations work as well as how we get the transform chains for creating a cuboid can be found in this older article. The case of a cube is a simplified version where all dimensions along the three axes are equal.

Considering we choose to rotate the first four faces around their y axes, our transform chains look as follows:

.cube__face:nth-child(1) {
  transform: rotateY(  0deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(2) {
  transform: rotateY( 90deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(3) {
  transform: rotateY(180deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(4) {
  transform: rotateY(270deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(5) {
  transform: rotateX( 90deg) translateZ(.5*$cube-edge)
}
.cube__face:nth-child(6) {
  transform: rotateX(-90deg) translateZ(.5*$cube-edge)
}

Now we replace the rotateY(ay) and rotateX(ax) components with their rotate3d(i, j, k, a) equivalents. The i, j and k in the rotate3d() function are the components of the unit vector of the rotation axis along the x, y and z axes of coordinates, while a is the rotation angle around that rotation axis.

Since the rotation axis in the case of a rotateY() is the y axis, the components of the unit vector along the other two axes (i along the x axis and k along the z axis) are 0, while the component along the y axis (j) is 1. Also, a is ay in this case.

Similarly, in the case of a rotateX(), we have that i is 1, j and k are 0 and a is ax. So our equivalent chains using rotate3d would be:

.cube__face:nth-child(1) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */,   0deg /*  0*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(2) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */,  90deg /*  1*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(3) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 180deg /*  2*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(4) {
  transform: rotate3d(0 /* i */, 1 /* j */, 0 /* k */, 270deg /*  3*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(5) {
  transform: rotate3d(1 /* i */, 0 /* j */, 0 /* k */,  90deg /*  1*90° */) 
    translateZ(.5*$cube-edge)
}
.cube__face:nth-child(6) {
  transform: rotate3d(1 /* i */, 0 /* j */, 0 /* k */, -90deg /* -1*90° */) 
    translateZ(.5*$cube-edge)
}

We notice a few things in the code above. First of all, the k component is always 0. Then, the i component is 0 for the first four faces and 1 for the remaining two, while the j component is 1 for the first four faces and 0 for the last two. Finally, the angle value can always be written as a multiplier times 90°.

This means we can introduce CSS variables into our code so we don’t have to repeat those transform functions:

.cube__face {
  transform: rotate3d(var(--i), var(--j), 0, calc(var(--m)*90deg)) 
    translateZ(.5*$cube-edge);
	
  &:nth-child(1) { --i: 0; --j: 1; --m:  0; }
  &:nth-child(2) { --i: 0; --j: 1; --m:  1; }
  &:nth-child(3) { --i: 0; --j: 1; --m:  2; }
  &:nth-child(4) { --i: 0; --j: 1; --m:  3; }
  &:nth-child(5) { --i: 1; --j: 0; --m:  1; }
  &:nth-child(6) { --i: 1; --j: 0; --m: -1; }
}

Since both --i and --j each keep the same value for the first four faces and get a different one only for the last two, we can set their defaults to be 0 and 1 respectively and then switch them to 1 and 0 respectively for faces 5 and 6. These two faces can be selected by :nth-child(n + 5). Also, we can set the default for --m to be 0 and thus completely eliminate the need for the :nth-child(1) rule.

.cube__face {
  transform: rotate3d(var(--i, 0), var(--j, 1), 0, calc(var(--m, 0)*90deg)) 
    translateZ(.5*$cube-edge);
	
  &:nth-child(n + 5) { --i: 1; --j: 0 }

  &:nth-child(2 /* 2 = 1 + 1 */) { --m:  1 }
  &:nth-child(3 /* 3 = 2 + 1 */) { --m:  2 }
  &:nth-child(4 /* 4 = 3 + 1 */) { --m:  3 }
  &:nth-child(5 /* 5 = 4 + 1 */) { --m:  1 /*  1 = pow(-1, 4) */ }
  &:nth-child(6 /* 6 = 5 + 1 */) { --m: -1 /* -1 = pow(-1, 5) */ }
}

Pushing things a bit further, we notice that, whether it’s 1 or 0, --j can be replaced with calc(1 - var(--i)) and that --m is either the face index for the first four faces or -1 raised to the face index for the last two faces. This allows us to eliminate the --j variable and set the multiplier --m within a loop:

.cube__face {
  --i: 0;
  transform: rotate3d(var(--i), calc(1 - var(--i)), 0, calc(var(--m, 0)*90deg)) 
    translateZ(.5*$cube-edge);
  
  &:nth-child(n + 5) { --i: 1 }
  
  @for $f from 1 to 6 {
    &:nth-child(#{$f + 1}) { --m: if($f < 4, $f, pow(-1, $f)) }
  }
}

The result can be seen below:

Black cube wireframe.
The static cube (live demo).

The biggest difference here is when it comes to the compiled code. With this CSS variables method we only write the transform functions once:

.cube__face {
  --i: 0;
  transform: rotate3d(var(--i), calc(1 - var(--i)), 0, calc(var(--m, 0)*90deg)) 
    translateZ(4em);
}

.cube__face:nth-child(n + 5) { --i: 1 }

.cube__face:nth-child(2) { --m: 1 }
.cube__face:nth-child(3) { --m: 2 }
.cube__face:nth-child(4) { --m: 3 }
.cube__face:nth-child(5) { --m: 1 }
.cube__face:nth-child(6) { --m: -1 }

Without CSS variables, the best we could have done still involved repeating the transform functions for each and every face:

.cube__face:nth-child(1) {
  transform: rotateY(0deg) translateZ(4em)
}
.cube__face:nth-child(2) {
  transform: rotateY(90deg) translateZ(4em)
}
.cube__face:nth-child(3) {
  transform: rotateY(180deg) translateZ(4em)
}
.cube__face:nth-child(4) {
  transform: rotateY(270deg) translateZ(4em)
}
.cube__face:nth-child(5) {
  transform: rotateX(90deg) translateZ(4em)
}
.cube__face:nth-child(6) {
  transform: rotateX(-90deg) translateZ(4em)
}

Animating the cube

We can add a keyframe animation to our .cube element:

.cube { animation: ani 2s ease-in-out infinite }

@keyframes ani {
  50% { transform: rotateY(90deg) rotateX(90deg) scale3d(.5, .5, .5) }
  100% { transform: rotateY(180deg) rotateX(180deg) }
}

The result can be seen below:

Animated gif. Black cube wireframe, scaling down and then back up as it rotates around its vertical axis.
The animated cube (live demo).

Current support status and cross-browser version

Those of you not using a WebKit browser may have noticed that the above demos don’t work. This is because, currently, Firefox and Edge don’t support using calc() values in place of much else other than length values. This includes the unitless and angle values within rotate3d(). A way to make things cross-browser would be not to replace --j with the calc(1 - var(--i)) equivalent and use an angle --a custom property instead of the calc(var(--m)*90deg):

.cube__face {
  transform: rotate3d(var(--i, 0), var(--j, 1), 0, var(--a)) 
    translateZ(.5*$cube-edge);
  
  &:nth-child(n + 5) { --i: 1; --j: 0 }
  
  @for $f from 1 to 6 {
    &:nth-child(#{$f + 1}) { --a: if($f < 4, $f, pow(-1, $f))*90deg }
  }
}

This does mean we now have a bit of redundancy, but it’s not that bad and our result is now cross-browser.

Adding text and backgrounds

Next, we can add text to the cube faces. Either the same for all of them:

.cube
  - 6.times do
    .cube__face Boo!

… or a different one for each (we’re switching to Pug here because it allows us to write a bit less code than Haml would in this case):

- var txt = ['ginger', 'anise', 'nutmeg', 'cinnamon', 'vanilla', 'cloves'];
- var n = txt.length;

.cube
  while n--
    .cube__face #{txt[n]}

In this case, we also set text-align: center, the line-height to $cube-edge and tweak the $cube-edge and the font-size values for the best text fit:

$cube-edge: 5em;

.cube {
 font: 8vmin/ #{$cube-edge} cookie, cursive;
 text-align: center;
}

We get the following result:

Black cube wireframe rotated in 3D with text on every one of the cube faces.
The cube with text (live demo, animated).

We could also give our faces some pastel gradient backgrounds:

$pastels: (#feffaa, #b2ff90) (#fbc2eb, #a6c1ee) (#84fab0, #8fd3f4) (#a1c4fd, #c2e9fb) 
  (#f6d365, #fda085) (#ffecd2, #fcb69f);

.cube__face {
  background: linear-gradient(var(--ga), var(--gs));
  
  @for $f from 0 to 6 {
    &:nth-child(#{$i + 1}) {
      --ga: random(360)*1deg; /* gradient angle */
      --gs: nth($pastels, $f + 1); /* gradient stops */
    }
  }
}

The above gives us a nice pastel cube:

Cube rotated in 3D with a different pastel gradient background for each of its faces.
The pastel cube (live demo, animated).

A use case

I’ve used this method of creating cuboids in a demo inspired by an animation loop by Dave Whyte.

Animated gif. Cuboidal bricks are falling one by one to form the uppermost circular ring on top of a structure
Build the factories (live demo, WebKit only)

Rotating the cube on drag

After this, there’s one more itch to scratch: what about not having the cube auto-animated using CSS keyframes, but instead rotated on drag? Let’s see how we can do that!

We start by selecting our .cube element and we establish what happens during the stages of the drag. On mousedown/ touchstart, we lock everything into place for the cube rotation. This means setting a drag flag to true and reading the coordinates of the point where this happens, which are also the coordinates where the first movement detected by mousemove/ touchmove is going to start. On mousemove/ touchmove, if the drag flag is true, we rotate our cube. On mouseup/ touchend and again, only if the drag flag is true, we perform a release-like action: we set the drag flag to false again and we clear the initial coordinates.

const _C = document.querySelector('.cube');

let drag = false, x0 = null, y0 = null;

/* helper function to handle both mouse and touch */
function getE(ev) { return ev.touches ? ev.touches[0] : ev };

function lock(ev) {
  let e = getE(ev);

  drag = true;
  x0 = e.clientX;
  y0 = e.clientY;
};

function rotate(ev) {
  if(drag) { /* rotation happens here */ }
};

function release(ev) {
  if(drag) {
    drag = false;
    x0 = y0 = null;
  }
};

addEventListener('mousedown', lock, false);
addEventListener('touchstart', lock, false);

addEventListener('mousemove', rotate, false);
addEventListener('touchmove', rotate, false);

addEventListener('mouseup', release, false);
addEventListener('touchend', release, false);

Now all that’s left to do is fill up the contents of the rotate() function!

For every little movement caught by the mousemove/ touchmove listeners, we have a start point and an end point. The coordinates of the end point (x,y) are those we read via clientX and clientY every time the mousemove/ touchmove fires. The coordinates of the start point (x0,y0) are either the same as those of the end point of the previous little movement or, if there was no previous movement, those of the point where mousedown/ touchstart fired. This means that, after doing everything else we need to do within the rotate() function, we set x0 to x and y0 to y:

function rotate(ev) {
  if(drag) {
    let e = getE(ev), 
        x = e.clientX, y = e.clientY;
    
    /* rotation code here */
    	
    x0 = x;
    y0 = y;
  }
};

Next, we compute the coordinate differences between the end point and the start point of the current little movement along the two axes (dx and dy), as well as diagonally (d). If d is 0, then we haven’t really moved (and maybe nothing should fire, but just in case), so we just exit the function without doing anything else, not even setting x0 and y0 to x and y respectively – they’re the same in this case anyway.

function rotate(ev) {
  if(drag) {
    let e = getE(ev), 
        x = e.clientX, y = e.clientY, 
        dx = x - x0, dy = y - y0, 
        d = Math.hypot(dx, dy);
		
    if(d) {
      /* actual rotation happens here */
      
      x0 = x;
      y0 = y;
    }
  }
};

The way we handle rotation on drag starting from the previous state which may be transformed in some way is the following: we chain a rotate3d() corresponding to the current little movement to the computed transform value of our cube at the start of the current little movement. That is, unless the computed transform value is none, in which case we chain it to nothing. We could write this whole transform chain into a stylesheet or as an inline style or… we could again use CSS variables!

In the CSS, we set the transform property of the .cube element to a rotate3d(var(--i), var(--j), 0, var(--a)) chained to a previous value of the transform chain var(--p). In order to simplify things, we keep the component of the unit vector of the axis of rotation along the z axis fixed to 0.

.cube {
  transform: rotate3d(var(--i), var(--j), 0, var(--a)) var(--p);
}

Because we’ve done the above and CSS variables are inherited, we now need to explicitly set --i and --j for the .cube__face elements to 0 and 1 respectively. Otherwise, the values inherited from the .cube element get applied, not the defaults specified within var().

.cube__face {
  --i: 0; --j: 1;
  transform: rotate3d(var(--i), var(--j), 0, var(--a)) 
    translateZ(.5*$cube-edge);
}

Going back to the JavaScript, we read the computed transform value and set it to the --p variable. The angle of rotation depends on the distance d between the start and end points of our current little movement and a constant A. We limit this result to two decimals. For a direction of motion towards the top, in the negative direction of the y axis, we rotate the cube clockwise around the x axis. This means we take the --i component to be -dy. For a direction of motion towards the right, in the positive direction of the x axis, we rotate the cube clockwise around the y axis, which means we take the --j component to be dx.

const A = .2;

function rotate(ev) {
  if(drag) {
    let e = getE(ev), 
        x = e.clientX, y = e.clientY, 
        dx = x - x0, dy = y - y0, 
        d = Math.hypot(dx, dy);
		
    if(d) {
      _C.style.setProperty('--p', getComputedStyle(_C).transform.replace('none', ''));
      _C.style.setProperty('--a', `${+(A*d).toFixed(2)}deg`);
      _C.style.setProperty('--i', +(-dy).toFixed(2));
      _C.style.setProperty('--j', +(dx).toFixed(2));
      
      x0 = x;
      y0 = y;
    }
  }
};

Finally, we can set some arbitrary defaults for these custom properties such that the initial position of our cube makes it look a bit more 3D than viewing it right from the front would.

.cube {
  transform: rotate3d(var(--i, -7), var(--j, 8), 0, var(--a, 47deg)) 
    var(--p, unquote(' '));
}

The unquote(' ') value is due to using Sass. While an empty space is a perfectly valid value for a CSS custom property in plain CSS, Sass throws an error when seeing stuff like var(--p, ), so we need to introduce that “no value” default using unquote().

The result of all the above is a cube we can drag using both mouse and touch:

See the Pen by thebabydino (@thebabydino) on CodePen.


Simplifying CSS Cubes with Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/simplifying-css-cubes-custom-properties/feed/ 8 254602
Star Wars Crawl Text https://css-tricks.com/snippets/css/star-wars-crawl-text/ https://css-tricks.com/snippets/css/star-wars-crawl-text/#comments Tue, 31 Jan 2017 13:04:12 +0000 http://css-tricks.com/?page_id=250613 The opening to Star Wars is iconic. The effect of text scrolling both up and away from the screen was both a crazy cool special effect for a movie back in 1977 and a cool typographical style that was brand …


Star Wars Crawl Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The opening to Star Wars is iconic. The effect of text scrolling both up and away from the screen was both a crazy cool special effect for a movie back in 1977 and a cool typographical style that was brand new at the time.

We can achieve a similar effect with HTML and CSS! This post is more about how to get that sliding text effect rather than trying to re-create the full Star Wars opening sequence or matching the exact styles used in the movie, so let’s get to a place where this is the final result:

See the Pen Star Wars Intro by Geoff Graham (@geoffgraham) on CodePen.

The Basic HTML

First, let’s set up out HTML for the content:

<section class="star-wars">

  <div class="crawl">
    
    <div class="title">
      <p>Episode IV</p>
      <h1>A New Hope</h1>
    </div>
    
    <p>It is a period of civil war. Rebel spaceships, striking from a hidden base, have won their first victory against the evil Galactic Empire.</p>     
    <p>During the battle, Rebel spies managed to steal secret plans to the Empire’s ultimate weapon, the DEATH STAR, an armored space station with enough power to destroy an entire planet.</p>
    <p>Pursued by the Empire’s sinister agents, Princess Leia races home aboard her starship, custodian of the stolen plans that can save her people and restore freedom to the galaxy…</p>

  </div>

</section>

This gives us all the pieces we need:

  • A container called star-wars that we will use to position the content. This is also necessary because we will be using the perspective CSS property, where having a parent element is a helpful way to add depth or skew a child element’s transform property.
  • A container called crawl that will hold the actual text and be the the element that we apply the CSS animation to.
  • The content!

You may have noticed that the movie title is wrapped in an extra <div> container called title. This is not necessary, but could provide you with additional styling options should you need them.

The Basic CSS

The trick is to imagine a three-dimensional space where the text crawls vertical up the Y-axis and out along the Z-axis. This gives the impression the text is both slide up the screen and away from the viewer at the same time.

The X, Y and Z-axis of a three-dimensional plane

First, let’s set up the document <body> so that the screen is not scrollable. We want the text to come up from the bottom of the screen without the viewer being able to scroll and see the text before it enters. We can use overflow: hidden to do that:

body {
  /* Force the body to fill the entire screen */
  width: 100%;
  height: 100%;
  /* Hide elements that flow outside the viewable space */
  overflow: hidden;
  /* Black background for the screen */
  background: #000;
}

Now we can move on to styling our star-wars container, which is the parent element for our demo:

.star-wars {
  /* Flexbox to center the entire element on the screen */
  display: flex;
  justify-content: center;
  /* This is a magic number based on the context in which this snippet is used and effects the perspective */
  height: 800px;
  /* This sets allows us to transform the text on a 3D plane, and is somewhat a magic number */
  perspective: 400px;
  /* The rest is totally up to personal styling preferences */
  color: #feda4a;
  font-family: 'Pathway Gothic One', sans-serif;
  font-size: 500%;
  font-weight: 600;
  letter-spacing: 6px;
  line-height: 150%;
  text-align: justify;
}

Next up, we can apply styles to the crawl element. Again, this element is important because it contains the properties that will transform the text and be animated.

.crawl {
  /* Position the element so we can adjust the top property in the animation */
  position: relative;
  /* Making sure the text is fully off the screen at the start and end of the animation */
  top: -100px;
  /* Defines the skew origin at the very center when we apply transforms on the animation */
  transform-origin: 50% 100%;
}

So far, we have a nice looking bunch of text, but it’s neither skewed nor animated. Let’s make that happen.

Animation!

This is what you really care about, right? First, we’re going to define the @keyframes for the animation. The animation is doing a little more than animating for us, because we’re going to be adding our transform properties here, particularly for the movement along the Z-axis. We’ll start the animation at 0% where the text is closest to the viewer and is located below the screen, out of view, then end the animation at 100% where it is far away from the viewer and flowing up and over the top of the screen.

/* We're calling this animation "crawl" */
@keyframes crawl {
  0% {
    /* The element starts below the screen */
    top: 0;
    /* Rotate the text 20 degrees but keep it close to the viewer */
    transform: rotateX(20deg) translateZ(0);
  }
  100% { 
    /* This is a magic number, but using a big one to make sure the text is fully off the screen at the end */
    top: -6000px;
    /* Slightly increasing the rotation at the end and moving the text far away from the viewer */
    transform: rotateX(25deg) translateZ(-2500px);
  }
}

Now, let’s apply that animation on the .crawl element:

.crawl {
  /* Position the element so we can adjust the top property in the animation */
  position: relative;
  /* Defines the skew origin at the very center when we apply transforms on the animation */
  transform-origin: 50% 100%;
  /* Adds the crawl animation, which plays for one minute */
  animation: crawl 60s linear;
}

Fun Times With Fine-Tuning

You can have a little more fun with things once the main effect is in place. For example, we can add a little fade at the top of the screen to accentuate the effect of the text crawling off into the distance:

.fade {
  position: relative;
  width: 100%;
  min-height: 60vh;
  top: -25px;
  background-image: linear-gradient(0deg, transparent, black 75%);
  z-index: 1;
}

Add that element to the top of the HTML and text will flow behind a gradient that goes from transparent to the same background as the <body>:

<div class="fade"></div>

The Full Example

Here is the full code from this post pulled together.

<div class="fade"></div>

<section class="star-wars">

  <div class="crawl">

    <div class="title">
      <p>Episode IV</p>
      <h1>A New Hope</h1>
    </div>
    
    <p>It is a period of civil war. Rebel spaceships, striking from a hidden base, have won their first victory against the evil Galactic Empire.</p>      
    <p>During the battle, Rebel spies managed to steal secret plans to the Empire’s ultimate weapon, the DEATH STAR, an armored space station with enough power to destroy an entire planet.</p>
    <p>Pursued by the Empire’s sinister agents, Princess Leia races home aboard her starship, custodian of the stolen plans that can save her people and restore freedom to the galaxy…</p>

  </div>

</section>
body {
  width: 100%;
  height: 100%;
  background: #000;
  overflow: hidden;
}

.fade {
  position: relative;
  width: 100%;
  min-height: 60vh;
  top: -25px;
  background-image: linear-gradient(0deg, transparent, black 75%);
  z-index: 1;
}

.star-wars {
  display: flex;
  justify-content: center;
  position: relative;
  height: 800px;
  color: #feda4a;
  font-family: 'Pathway Gothic One', sans-serif;
  font-size: 500%;
  font-weight: 600;
  letter-spacing: 6px;
  line-height: 150%;
  perspective: 400px;
  text-align: justify;
}

.crawl {
  position: relative;
  top: 9999px;
  transform-origin: 50% 100%;
  animation: crawl 60s linear;
}

.crawl > .title {
  font-size: 90%;
  text-align: center;
}

.crawl > .title h1 {
  margin: 0 0 100px;
  text-transform: uppercase;
}

@keyframes crawl {
  0% {
    top: 0;
    transform: rotateX(20deg)  translateZ(0);
  }
  100% { 
    top: -6000px;
    transform: rotateX(25deg) translateZ(-2500px);
  }
}

Other Examples

Some other folks have made more faithful renditions of the Star Wars opening using other techniques than the ones covered here in this post.

Tim Pietrusky has a beautifully orchestrated version using top for the movement and opacity to create the fading effect:

See the Pen Star Wars opening crawl from 1977 by Tim Pietrusky (@TimPietrusky) on CodePen.

Yukulélé uses margin to move the the along the screen:

See the Pen Pure CSS Star Wars opening crawl by Yukulélé (@yukulele) on CodePen.

Karottes uses transform much like this post, but relies more on TranslateY to move the text along the Y-axis.

See the Pen Star Wars Crawl by Karottes (@Karottes) on CodePen.


Star Wars Crawl Text originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/snippets/css/star-wars-crawl-text/feed/ 4 250613
Reactive Audio WebVR https://css-tricks.com/reactive-audio-webvr/ https://css-tricks.com/reactive-audio-webvr/#comments Tue, 27 Dec 2016 13:44:04 +0000 http://css-tricks.com/?p=248841 Virtual reality has become a thing again! All of the usual suspects are involved: HTC, Microsoft, Samsung, and Facebook, among others, are all peddling their respective devices. These predictable players shouldn’t be having all the fun, though!

You make websites. …


Reactive Audio WebVR originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Virtual reality has become a thing again! All of the usual suspects are involved: HTC, Microsoft, Samsung, and Facebook, among others, are all peddling their respective devices. These predictable players shouldn’t be having all the fun, though!

You make websites. You know a bit of Javascript. You have a mobile device. You can have a slice of this virtual pie too! WebVR is here, and it’s not that difficult to learn. If you already know the basics of three.js, you might be surprised at how simple it is to get it going. If you haven’t ever used three.js, this will be a fun way to learn it.

I’ve been making websites for quite a while, but only in the last couple of years have I explored the use of front-end technologies for more than just websites. Having spent some time using tools such as canvas and three.js, my mind has been opened to the wonderful potential this side of the web can offer us as developers (and artists!).

Polyop – Ceremony. Music video created with three.js and WebVR controls

I’ve taken the path of making trippy visuals with Javascript and am now one-third of audio-visual techno act, Polyop, because of it. As part of a vinyl release, we’ve created a 360 degree music video built with three.js and webVR controls. I’d thought I’d share with you the basic concepts I picked up while developing it.

But I don’t have those fancy goggles

There’s no denying that not having the kit seems like a barrier to entry. However, you don’t need any sort of extra hardware for most of this tutorial so you can still have fun moving your phone around exploring the 3D world you’ll create.

To play with the VR portion of this tutorial, you’ll want some sort of VR Viewer. The cheapest way to do this is to buy a headset that turns your mobile phone into a VR headset, you simply slot it your phone in and away you go. These headsets range from a £3 to £50 so have a look around to see what best suits you and your budget. “Google Cardboard” is the term you’ll hear about these types of devices.

What we’ll be making

Here’s a demo. All the source code for the steps we’ll be taking is available on GitHub too.

If you’re viewing on a mobile or tablet, you can look around by moving the device. If you’re on a laptop, you have to click and drag. If you have a VR Viewer for your phone, there’s an option to go into actual VR mode by clicking on the “start VR” button.

We’ll tackle it in three parts:

  1. Make the three.js scene (+ demo)
  2. Add in VR Controls (device motion) (+ demo)
  3. Apply the VR Effect (stereoscopic picture) (+ demo)

Making the scene

Those who have some experience with three.js may want to skip this part and head straight for the VR stuff.

Three.js has become the web dev’s favorite library for creating 3D scenes. Don’t let that extra dimension scare you; it’s not so difficult to get going! Before we even think about VR, we’re going to make a simple 3D world that has a bunch of cubes, slowly spinning.

If you’re new to three.js I recommend taking a look at the “creating a scene” tutorial included in the documentation. It goes into a little more detail than I will, and you’ll have a spinning cube up and running in no time. Otherwise feel free to jump straight in here, we’ll still be going quite slow.

Setup

Firstly we need to set up a document with the three.js library included. You can install with Bower, npm, or keep it simple and get the file from a CDN.

Please note that the three.js API changes from time to time. This tutorial has been created with r82 and while it’s always good to use the newest version of any library, for our purposes it may make sense to use the same version used in the examples.

<!DOCTYPE html>
<html lang="en">

  <head>
    <title>WebVR Tutorial</title>
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no">
    <style>
      body {
        margin: 0;
      }
    </style>
  </head>

  <body>
    <script src="lib/three.js"></script>
    <script>
      // All scripts will go here
    </script>
  </body>

</html>

Now we need to set up the scene, the camera, and the renderer. The scene acts as a container for all objects to go inside. The camera is one of those objects and gives us a point of view from inside the scene. The renderer takes the view from the camera and paints it onto a canvas element.

// Create the scene and camera
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 10000 );

// Create the renderer
var renderer = new THREE.WebGLRenderer();

// Set the size of the renderer to take up the entire window
renderer.setSize( window.innerWidth, window.innerHeight );

// Append the renderer canvas element to the body
document.body.appendChild( renderer.domElement );

We’ll also need to tell the renderer to render the scene:

// Render the scene
renderer.render( scene, camera );

For now on, you should make sure this rendering happens last in your code. Later we’ll be firing it every frame inside of an animate() function.

At this point, your scene should be rendering with a canvas element on the page, but all you’ll see is black.

Let’s add a cube to the scene

It comprises of a geometry and a material, held together in a mesh:

// Create cube
var material = new THREE.MeshNormalMaterial();
var geometry = new THREE.BoxGeometry( 50, 50, 50 );
var mesh = new THREE.Mesh( geometry, material );

// Add cube to scene
scene.add(mesh);

Now you should see a cube being rendered, yay!

Let’s make lots of cubes by wrapping the code in a for loop:

var cubes = [];

for (var i = 0; i < 100; i++) {

  var material = new THREE.MeshNormalMaterial();
  var geometry = new THREE.BoxGeometry( 50, 50, 50 );
  var mesh = new THREE.Mesh( geometry, material );

  // Give each cube a random position
  mesh.position.x = (Math.random() * 1000) - 500;
  mesh.position.y = (Math.random() * 1000) - 500;
  mesh.position.z = (Math.random() * 1000) - 500;

  scene.add(mesh);
  
  // Store each mesh in array
  cubes.push(mesh);

 }

You’ll notice that I’ve also given each cube a random position by changing their position property. X,Y and Z refers to their positions along each axis. Our camera is at position (0,0,0), in the center of the scene. By giving each cube a random position along each axis (between -500 and 500), the cubes will be surrounding the camera in all directions.

I’ve also stored each cube’s mesh in an array, which will allow us to animate them. We need to create an animate() function that will fire every frame:

function animate() {

  requestAnimationFrame( animate );

  // Every frame, rotate the cubes a little bit
  for (var i = 0; i < cubes.length; i++) {
    cubes[i].rotation.x += 0.01;
    cubes[i].rotation.y += 0.02;
  }

  // Render the scene
  renderer.render( scene, camera );

}

The animate() function iterates through the cubes array and updates the rotation property of each mesh. It will constantly loop every frame because we’re calling it recursively using requestAnimationFrame. You’ll also notice I’ve moved renderer.render() inside this function, so that the scene is being rendered every frame too.

Make sure you call animate() somewhere in the script to start the animation loop.

That’s our scene done! If you’re struggling, have a look at the source code for this step, I’ve tried my best to include descriptive comments. You’ll notice I’ve rearranged the code slightly from the snippets in this article, along with a better use of variable names.

Time to get virtual

Before we get started, it’s good to know what we’re actually playing with! The WebVR website sums it up very well:

WebVR is an experimental JavaScript API that provides access to Virtual Reality devices, such as the Oculus Rift, HTC Vive, Samsung Gear VR, or Google Cardboard, in your browser.

At the moment the API only works in special browser builds, which may be fun to play with, but are lacking an audience. Luckily for us, however, the WebVR Polyfill swoops in to save the day. It makes your VR creations available on mobile devices via Google Cardboard (or similar viewers), while also allowing users to view the same content without a VR viewer. You should know that the polyfill doesn’t support any other VR devices, such as the Oculus Rift or HTC Vive.

To use the polyfill, include the script in your page, before all other scripts. The next two parts to this tutorial won’t work if you don’t have it included.

Controls

A critical component to any virtual reality experience is capturing the motion of the user and using that information to update the orientation of the camera in the virtual scene. We can achieve this in three.js with the VRControls constructor. VRControls doesn’t come built with the library but as an extra you can find in the repository. You should include it in a separate script tag after the three.js library.

You’ll be surprised at how simple it is to implement. Firstly, create the controls, passing in the camera:

var controls = new THREE.VRControls( camera );

This now means that the controls will be affecting the camera, which is essentially just an object in the scene like any other mesh. You could use these controls to rotate a cube rather than the camera if you wanted to.

In your animate() function you’ll also need to tell the controls to update every frame:

controls.update();

And that’s it! If you look at what you’ve made using a mobile device, you should be able to “look around” the scene by moving the device. On a laptop without these capabilities, you’ll have to click and drag with the mouse, this click and drag fallback is an extra bonus we get with the WebVR polyfill.

Take a look at the source code for this step if you’re stuck.

VR Effect

At this point you may already be satisfied with what you’ve created. Looking around using the motion of your device is super fun and opens up all sorts of possibilities for making something cool. When making the interactive video for Polyop, I felt this behavior was immersive enough and chose not to introduce the stereoscopic feature.

However I promised actual VR and so that’s what you’re hear for! The final piece of the puzzle is to get three.js to render two separate images, one for each eye. We’ll do this using the VREffect constructor. Just like you did with VRControls, include the script and away we go. First we need to define the effect:

effect = new THREE.VREffect(renderer);
effect.setSize(window.innerWidth, window.innerHeight);

We define a new VREffect, passing in the renderer. From now on we don’t need to deal with the renderer, it will be dealt with by VREffect. That’s why we’re now setting the size of the effect instead of the renderer. Importantly, we need to swap out the way we render in the animate function:

effect.render( scene, camera );

We’re now telling the effect to render, not the renderer. At the moment nothing will have changed. The VREffect simply takes in the renderer you give it and renders as normal when you tell it to. To get the stereoscopic effect we’re looking for; we need to do a little more.

Firstly, we need to search for any connected VR devices. Because we’re using the WebVR Polyfill, all we get is one “device” connected, which will be Google Cardboard. Here’s how we get it:

var vrDisplay;
navigator.getVRDisplays().then(function(displays) {
    if (displays.length > 0) {
     vrDisplay = displays[0];
   }  
});

navigator.getVRDisplays returns a promise function which will be invoked once it has finished looking for devices. In this instance, we take the first and only item in the displays array and define it globally as vrDisplay so we can use it elsewhere. If we weren’t using the polyfill, there might be more than one device in the array, and you’d probably want to add in some user functionality to choose between them. Luckily today we don’t have to accommodate for little Johnny and his fifty different VR devices.

Now we have our single device defined as vrDisplay, we need to fire it up! The method to do this is requestPresent, and we’ll give it the canvas element we’re rendering to.

document.querySelector('#startVR').addEventListener('click', function() {
  vrDisplay.requestPresent([{source: renderer.domElement}]);
});

To avoid abuse of the webVR API, it is required that you wrap any calls of requestPresent in an event listener. This one fires on the click of a button element with an ID of “startVR”.

The last thing we need to do is make sure everything renders properly after a resize of the renderer. This happens not just when the screen size changed but when we switch in and out of VR mode.

// Resize the renderer canvas
function onResize() {
effect.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
}
      
// Resize the renderer canvas when going in or out of VR mode
window.addEventListener('vrdisplaypresentchange', onResize);

// Resize the renderer canvas if the browser window size changes
window.addEventListener('resize', onResize);

The onResize() function resets the size of the effect (and therefore the renderer) while also updating some properties of the camera.

Once again, if you’re feeling a bit muddled, take a look at the source code of this final step.

Summing up

Congratulations! You’ve officially entered cyberspace. What to do with your new powers?

Why not build on the work we’ve already done today? Perhaps try and transform the scene into something a little more aesthetically pleasing by using lighting and different geometries/materials? Maybe you could even try making the objects bounce to music using the Audio API? To give you an idea, here’s one I made earlier.


Reactive Audio WebVR originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/reactive-audio-webvr/feed/ 2 248841