Animated Skewed Panes

A silly CSS-only experiment replicating the opening scene of the film World War Z

Terry Mun
Coding & Design

--

After watching World War Z on a flight from Istanbul to Singapore — if you’d make me rate it I’ll give it a 6.5/10, but I digress—I was intrigued by the slick effect seen in the opening scene of the film:

The opening sequence of World War Z, which I wanted to replicate with CSS.

First thing first — I didn’t want to set myself unrealistic expectations, so instead of overlaying the effects over video footages, I opted to substitute it with static images instead. The effect is still rather neat, I’d say — you can check it out in its full glory, hosted on nowhere else but CodePen.

For the uninitiated, as follow is a 25-frame (incomplete) GIF of what the effect should look like. Forgive for the choppiness.

The 25-frame GIF showing how the demo should look like in a WebKit browser.

Since we will be exploiting state-of-the-art (at the time of writing) CSS properties that are probably not fully standardised and implemented in a cross-browser manner, I have to warn you that the demo will only work on WebKit-based browsers, i.e. Chrome (or its advanced sibling Canary) and Safari.

The demo takes advantage of the following features available to WebKit-based browsers:

The technique

HTML markup

The technique is rather straight forward. We first create a stage where the background image is positioned centrally in a container of known dimensions. The markup is simple:

<section class="stage">
<div class="skew-image">
<div></div><!-- #1 -->
<!-- You can add more if you want -->
</div>
</section>

By wrapping all the animated skewed panes, marked #1 (or more, if you want to have more animated panes), in a common parent .skew-image, we award ourselves with the luxury of using nth-child or nth-of-type pseudo-classes — they do not work on other selectors except for element selectors.

Styling it up

I have opted to use 100% width and 100% of the viewport height. We also position the background image centrally, size it such that it fills up the stage while preserving aspect ratio (background-size: cover) and hiding any overflows.

.stage {
background-image: url(...);
background-position: center center;
background-size: cover;
position: relative;
overflow: hidden;
width: 100%;
height: 100vh;
}

The .skew-image and its children are all positioned absolutely, and we will animate the left or right offset of the children with CSS animations to achieve the desired effect. We will also append CSS-generated content to the children, which will serve as the background of each children. The reason of doing so is to allow us to apply CSS filters, since they do not work on background images but on elements.

.skew-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.skew-image > div {
position: absolute;
top: 0;
bottom: 0;
overflow: hidden;
animation-duration: 15s; // Arbitrary timing
animation-iteration-count: infinite;
}
.skew-image > div:before {
background-image: url(...);
background-position: center center;
background-size: cover;
content: ''; // Content must be specified
display: block;
width: 100vw;
height: 100vh;
position: relative;
animation-duration: 15s; // Arbitrary timing
animation-iteration-count: infinite;
}

The CSS-generated content, :before, is set to have the same background-image as the stage. It’s width is set to 100% of the viewport width — however, should your stage not fill the width of the viewport, you will have to calculate the width of the stage and inject this value using JS, since it is impossible for the generated content to “know” the parent’s width.

Handling the animation

:before is skewed by a certain degree, say, 15º. This causes the image to be skewed. We then apply the exact inverse of the transformation, say, -15º, to the parent of the generated content. This creates the effect of the parent being skewed, but the inner image remains upright. These transformations are specified in the @keyframes property because CSS transformations are not additive on the same element (they will override each other instead).

.skew-image > div:nth-of-type(1) {
animation-name: skewShift1;
left: 0;
width: 15rem; // Arbitrary width
}
.skew-image > div:nth-of-type(1):before {
animation-name: skewBgShift1;
-webkit-filter: blur(2px);
}

Note: The CSS filter property can be customised to your own liking, with various effects other than ‘blur’ being available.

To animate the .skew-image child, we simply change the left position. In order to create the illusion that the image in the CSS-generated content remains fixed, we will offset the generated content by the same amount, but with a negative value:

@keyframes skewShift1 {
0% {
transform: translate3d(0,0,0)
skew(15deg,0);
}
50% {
transform: translate3d(40rem,0,0)
skew(15deg,0);
// Remove if fixed width is desired
width: 30rem;
}
100% {
transform: translate3d(0,0,0)
skew(15deg,0);
}
}
@keyframes skewBgShift1 {
0% {
transform: translate3d(0,0,0)
scale(1.25) // Scale is optional
skew(-15deg,0);
}
50% {
transform: translate3d(-40rem,0,0)
scale(1.25) // Scale is optional
skew(-15deg,0);
}
100% {
transform: translate3d(0,0,0)
scale(1.25) // Scale is optional
skew(-15deg,0);
}
}

Note: Remember that you might have to use vendor prefixes for the @keyframes declarations.

And… you’re done!

Here’s a functional demo on CodePen—do fiddle with it!
p/s: Rinse and repeat for additional animation panes.

Discussion

Why use CSS 3D transform?

By forcing CSS 3D transform instead of 2D, we are offloading the rendering to the GPU, thereby allowing hardware acceleration and ensuring a less sluggish performance.

Why not background-clip: text?

It is indeed possible to use the background-clip: text property, both extensively investigated by Chris and Divya, to ensure that the background clips to any text. This saves us the trouble of generating CSS content, but also removes the possibility to use CSS filters as they do not work on background images. In cases like this, you will have to generate the filtered images yourself, either manually and serving them as static resources, or through server-side script (like with GD on PHP).

Do leave a note if you see something amiss, or have suggestions on how this demo or article can be improved. Thank you!

--

--

Terry Mun
Coding & Design

Amateur photographer, enthusiastic web developer, whimsical writer, recreational cyclist, and PhD student in molecular biology. Sometimes clumsy. Aarhus, DK.