You know, Porky Pig coming out of those red rings announcing the end of a Looney Tunes cartoon. We’ll get there, but first we need to cover some CSS concepts.
Everything in CSS is a box, or rectangle. Rectangles stack, and can be displayed on top of, or below, other rectangles. Rectangles can contain other rectangles and you can style them such that the inner rectangle is visible outside the outer rectangle (so they overflow) or that they’re clipped by the outer rectangle (using overflow: hidden
). So far, so good.
What if you want a rectangle to be visible outside its surrounding rectangle, but only on one side. That’s not possible, right?
Perhaps, when you look at the image above, the wheels start turning: What if I copy the inner rectangle and clip half of it and then position it exactly?. But when it comes down to it, you can’t choose to have an element overflow at the top but clip at the bottom.
Or can you?
3D transforms
Using 3D transforms you can rotate, transform, and translate elements in 3D space. Here’s a group of practical examples I gathered showcasing some possibilities.
For 3D transforms to do their thing, you need two CSS properties:
perspective
, using a value in pixels, to determine how pronounced the 3D effect istransform-style: preserve-3d
, to tell the browser to keep elements positioned in 3D space.
Even with the good support that 3D transforms have, you don’t see 3D transforms ‘in the wild’ all that much, sadly. Websites are still a “2D” thing, a flat page that scrolls. But as I started playing around with 3D transforms and scouting examples, I found one that was by far the most interesting as far as 3D transforms go:
The image clearly shows three planes but this effect is achieved using a single <div>
. The two other planes are the ::before
and ::after
pseudo-elements that are moved up and down respectively, using translate()
, to stack on top of each other in 3D space. What is noticeable here is how the ::after
element, that normally would be positioned on top of an element, is behind that element. The creator was able to achieve this by adding transform: translateZ(-1px);
.
Even though this was one of many 3D transforms I had seen at this point, it was the first one that made me realize that I was actually positioning elements in 3D space. And if I can do that, I can also make elements intersect:
I couldn’t think of how this sort of thing would be useful, but then I saw the Porky Pig cartoon animation. He emerges from behind the bottom frame, but his face overlaps and stacks on top of the top edge of the same frame — the exact same sort of clipping situation we saw earlier. That’s when my wheels started turning. Could I replicate that effect using just CSS? And for extra credit, could I replicate it using a single <div>
?
I started playing around and relatively quickly had this to show for it:
Here we have a single <div>
with its ::before
and an ::after
pseudo-elements. The div itself is transparent, the ::before
has a blue border and the ::after
has been rotated along the x-axis. Because the div has perspective
, everything is positioned in 3D and, because of that, the ::after
pseudo-element is above the border at the top edge of the frame and behind the border at the bottom edge of the frame.
Here’s that in code:
div { transform: perspective(3000px); transform-style: preserve-3d; position: relative; width: 200px; height: 200px;
} div::before { content: ""; width: 100%; height: 100%; border:10px solid darkblue;
} div::after { content: ""; position: absolute; background: orangered; width: 80%; height: 150%; display: block; left: 10%; bottom: -25%; transform: rotateX(-10deg);
}
With perspective
, we can determine how far a viewer is from “z=0” which we can consider to be the “horizon” of our CSS 3D space. The larger the perspective, the less pronounced the 3D effect, and vice versa. For most 3D scenes, a perspective
value between 500 and 1,000 pixels works best, though you can play around with it to get the exact effect you want. You can compare this with perspective drawing: If you draw two horizon points close together, you get a very strong perspective; but if they’re far apart, then things appear flatter.
From rectangles to cartoons
Rectangles are fun, but what I really wanted to build was something like this:
I couldn‘t find or create a nicely cut-out version of Porky Pig from that image, but the Wikipedia page contains a nice alternative, so we’ll use that.
First, we need to split the image up into three parts:
<div>
: the blue background behind Porky::after
: all the red circles that form a sort of tunnel::before
: Porky Pig himself in all his glory, set as a background image
We’ll start with the <div>
. That will be the background as well as the base for the rest of the elements. It’ll also contain the perspective
and transform-style
properties I called out earlier, along with some sizes and the background color:
div { transform: perspective(3000px); transform-style:preserve-3d; position: relative; width: 200px; height: 200px; background: #4992AD;
}
Alright, next up, we‘ll move to the red circles. The element itself has to be transparent because that’s the opening where Porky emerges. So how shall we go about it? We can use a border just like the example earlier in this article, but we only have one border and that can have a solid color. We need a bunch of circles that can accept gradients. We can use box-shadow
instead, chaining multiple shadows in the property values. This gets us all of the circles we need, and by using a blur radius value of 0
with a large spread radius, we can create the appearance of multiple “borders.”
box-shadow: <x-offset> <y-offset> <blur-radius> <spread-radius> <color>;
We‘ll use a border-radius
that‘s as large as the <div>
itself, making the ::before
a circle. Then we’ll add the shadows. When we add a few red circles with a large spread and add blurry white, we get an effect that looks very similar to the Porky’s tunnel.
box-shadow: 0 0 20px 0px #fff, 0 0 0 30px #CF331F, 0 0 20px 30px #fff, 0 0 0 60px #CF331F, 0 0 20px 60px #fff, 0 0 0 90px #CF331F, 0 0 20px 90px #fff, 0 0 0 120px #CF331F, 0 0 20px 120px #fff, 0 0 0 150px #CF331F;
Here, we’re adding five circles, where each is 30px
wide. Each circle has a solid red background. And, by using white shadows with a blur radius of 20px
on top of that, we create the gradient effect.
With the background and the circles sorted, we’re now going to add Porky. Let’s start with adding him at the spot we want him to end up, for now above the circles.
div::before { position: absolute; content: ""; width: 80%; height: 150%; display: block; left: 10%; bottom: -12%; background: url("Porky_Pig.svg") no-repeat center/contain;
}
You might have noticed that slash in “center/contain
” for the background
. That’s the syntax to set both the position (center
) and size (contain
) in the background
shorthand CSS property. The slash syntax is also used in the font
shorthand CSS property where it’s used to set the font-size
and line-height
like so: <font-size>/<line-height>
.
The slash syntax will be used more in future versions of CSS. For example, the updated rgb()
and hsl()
color syntax can take a slash followed by a number to indicate the opacity, like so: rgb(0 0 0 / 0.5)
. That way, there’s not need to switch between rgb()
and rgba()
. This already works in all browsers, except Internet Explorer 11.
Both the size and positioning here is a little arbitrary, so play around with that as you see fit. We’re a lot closer to what we want, but now need to get it so the bottom portion of Porky is behind the red circles and his top half remains visible.
The trick
We need to transpose both the circles as well as Porky in 3D space. If we want to rotate Porky, there are a few requirements we need to meet:
- He should not clip through the background.
- We should not rotate him so far that the image distorts.
- His lower body should be below the red circles and his upper body should be above them.
To make sure Porky doesn‘t clip through the background, we first move the circles in the Z direction to make them appear closer to the viewer. Because preserve-3d
is applied it means they also zoom in a bit, but if we only move them a smidge, the zoom effect isn’t noticeable and we end up with enough space between the background and the circles:
transform: translateZ(20px);
Now Porky. We’re going to rotate him around the X-axis, causing his upper body to move closer to us, and the lower part to move away. We can do this with:
transform: rotateX(-10deg);
This looks pretty bad at first. Porky is partially hidden behind the blue background, and he’s also clipping through the circles in a weird way.
We can solve this by moving Porky “closer” to us (like we did with the circles) using translateZ()
, but a better solution is to change the position of our rotation point. Right now it happens from the center of the image, causing the lower half of the image to rotate away from us.
If we move the starting point of the rotation toward the bottom of the image, or even a little bit below that, then the entirety of the image rotates toward us. And because we already moved the circles closer to us, everything ends up looking as it should:
transform: rotateX(-10deg);
transform-origin: center 120%;
To get an idea of how everything works in 3D, click “show debug” in the following Pen:
Animation
If we keep things as they are — a static image — then we wouldn’t have needed to go through all this trouble. But when we animate things, we can reveal the layering and enhance the effect.
Here‘s the animation I’m going for: Porky starts out small at the bottom behind the circles, then zooms in, emerging from the blue background over the red circles. He stays there for a bit, then moves back out again.
We’ll use transform
for the animation to get the best performance. And because we’re doing that, we need to make sure we keep the rotateX
in there as well.
@keyframes zoom { 0% { transform: rotateX(-10deg) scale(0.66); } 40% { transform: rotateX(-10deg) scale(1); } 60% { transform: rotateX(-10deg) scale(1); } 100% { transform: rotateX(-10deg) scale(0.66); }
}
Soon, we’ll be able to directly set different transforms, as browsers have started implementing them as individual CSS properties. That means that repeating that rotateX(-10deg)
will eventually be unnecessary; but for now, we have a little bit of duplication.
We zoom in and out using the scale()
function and, because we’ve already set a transform-origin
, scaling happens from the center-bottom of the image, which is precisely the effect we want! We’re animating the scale up to 60% of Porky’s actual size, we have the little break at the largest point, where he fully pops out of the circle frame.
The animation goes on the ::before
pseudo-element. To make the animation look a little more natural, we’re using an ease-in-out
timing function, which slows down the animation at the start and end.
div::before { animation-name: zoom; animation-duration: 4s; animation-iteration-count: infinite; animation-fill-mode:forwards; animation-timing-function: ease-in-out;
}
What about reduced motion?
Glad you asked! For people who are sensitive to animations and prefer reduced or no motion, we can reach for the prefers-reduced-motion
media query. Instead of removing the full animation, we’ll target those who prefer reduced motion and use a more subtle fade effect rather than the full-blown animation.
@media (prefers-reduced-motion: reduce) { @keyframes zoom { 0% { opacity:0; } 100% { opacity: 1; } } div::before { animation-iteration-count: 1; }
}
By overwriting the @keyframes
inside a media query, the browser will automatically pick it up. This way, we still accentuate the effect of Porky emerging from the circles. And by setting animation-iteration-count
to 1
, we still let people see the effect, but then stop to prevent continued motion.
Finishing touches
Two more things we can do to make this a bit more fun:
- We can create more depth in the image by adding a shadow behind Porky that grows as he emerges and appears to zoom in closer to the view.
- We can turn Porky as he moves, to embellish the pop-out effect even further.
That second part we can implement using rotateZ()
in the same animation. Easy breezy.
But the first part requires an additional trick. Because we use an image for Porky, we can’t use box-shadow
because that creates a shadow around the box of the ::before
pseudo-element instead of around the shape of Porky Pig.
That’s where filter: drop-shadow()
comes to the rescue. It looks at the opaque parts of the element and adds a shadow to that instead of around the box.
@keyframes zoom { 0% { transform: rotateX(-10deg) scale(0.66); filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0)); } 40% { transform: rotateZ(-10deg) rotateX(-10deg) scale(1); filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5)); } 60% { transform: rotateZ(-10deg) rotateX(-10deg) scale(1); filter: drop-shadow(-10px 10px 10px rgba(0,0,0,0.5)); } 100% { transform: rotateX(-10deg) scale(0.66); filter: drop-shadow(-5px 5px 5px rgba(0,0,0,0)); }
}
And that‘s how I re-created the Looney Tunes animation of Porky Pig. All I can say now is, “That’s all Folks!”
The post Re-Creating the Porky Pig Animation from Looney Tunes in CSS appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.