Origin
This animation was the first part of a feature called "something virtual that matters". It started mid 2012 while watching a gameplay video of Battlefield 3. I was amazed by all the work that goes into making a video game as realistic as possible. Yet, "it's just a game", as the popular wisdom goes. How can we make something virtual that is more than a game? Can we bring virtual worlds to the status of common reality and have them matter as much?
I am obsessed with these questions, and I wanted to examine what it takes to build a virtual world that matters.
This animation was the first step of an argumentation. It's an illustration of what we can currently have running on a 2D screen. The animation was supposed to go further, with the user being able to steer the plane and make it crash against the canyon, just like what we can do in video games:
But, the argumentation goes, no matter how realistic the graphics can get, it's still a partial representation made with visual effects tricks. Let's examine more closely what is happening and go from there: the plane is a 2D projection of a 3D model, the animation is a sequence of 2D images projected across time, and my physical computer is ticking billions of times a second to run all this. Should we see the plane crash into the canyon, no actual plane or pilot would be harmed in the process. We are getting some of the reality (the graphics) but not all of it. What do we need exactly?
As a result, I got sucked into a whirlwind of hundreds of more or less relevant questions and observations. For example: we won't believe a plane has actually crashed, but if we replace this animation with a chess game against the computer, we would really believe that we lost or won a game of chess, even if the game is only virtual with no physical checkerboard and pieces. Another observation: the plane might not have really crashed, but my computer running the animation has really got a little warmer. There are physical deposits of the virtual world, like power consumption and heat generation1.
I decided that the subject is either too huge, or ill-defined, or both. Typical of philosophy.
4 years and a half later, I reopened the animation, decided to clean it, stick a song on it, and call it a day. Here is how!
Graphics
There is the sky, and then there are eleven other layers, each one being a 3D element modeled and rendered in Cinema 4D with an orthographic projection.
Animation
Two technics were used to animate the scene: 1) 3D animation to get the plane roll motion, and 2) CSS animations to implement all the moving parts on the web browser.
The plane. The roll motion was made in Cinema 4D. I spent some time synchronizing the body roll with the ailerons activation. The animation was then rendered into a sequence of 216 frames, patched together vertically using SpriteGenerator4 to make a spritesheet. Click the sample below to see the full spritesheet:
Here's a video file of the looping animation:
The spritesheet is set as a background-image
of an empty div
element, and the background-position
is animated with CSS keyframes using the steps
animation-timing-function. This timing function moves the spritesheet one notch every given lapse of time, like a film projector, which achieves the appearance of motion. Here's a code sample:
#plane{ width: 230px; height: 100px; background-image: url(plane_roll_spritesheet.png); animation: roll 12s steps(215) infinite; } @keyframes roll { from {background-position: 0px -43000px;} to {background-position: 0px 0px;} }
In this example, steps
moves the spritesheet in the #plane
container 43000 pixels up, in 215 discrete steps, for a total duration of 12 seconds, and then repeats indefinitely.
The plane motion wasn't achieved with the spritesheet alone. I found that the plane needs to move about within the scene to get a more lively and believable effect. I added a zoom-in/zoom-out animation as well as a translation using transform: scale() translateX()
, and applied it to a container div
wrapping the plane, since I couldn't apply two different CSS animations to the same element.
The clouds, the canyon and the trees are animated using CSS transforms. They are positioned far outside the scene, then brought into the scene by keyframing their transform: translateX()
property. They appear from the left, move past the scene to the right, then jump back and repeat indefinitely. A slightly different starting position and animation speed for each element generates interesting overlapping patterns.
The background landscape is animated by keyframing its background-position
. Unlike the clouds, the canyon and the trees, the landscape is always visible on screen, so I couldn't use the higher performance CSS transform properties. The background image is set to repeat-x
, and it scrolls from left to right inside the landscape container indefinitely. See the code below:
#background_landscape{ position: absolute; width: 2536px; height: 46px; bottom: -1px; background-image: url(background_landscape.png); background-repeat: repeat-x; background-position: center bottom; background-size: 2536px 46px; z-index: +1; opacity: 0.9; animation: background_landscape 150s infinite linear; } @keyframes background_landscape { from {background-position: 0;} to {background-position: 2536px;} }
Notice how the second keyframe background-position: 2536px
is an exact multiple of the landscape container's width (in this case they are identical). This ensures the background image completes a full translate across the container before jumping back to zero.
Also, I found that when that container has bottom: 0
, there is an undesirable visible line along the upper border of the div
on Chrome for Android. Setting bottom
to -1px
solves the problem.
Overall, fine tunning the CSS animation values for each element was the result of a trial and error process until things looked right. The only hard rule was to respect the parallax: the apparent motion of farther elements (like the landscape) should be slower than the apparent motion of closer elements (like the trees).
Layout
I wanted to get a cinematic view of the ever running landscape: the scene should span horizontally across the screen, be vertically centered, and black bars be flancked above and below to fill the rest of the space. In addition, the animation should play well across all screen sizes.
Cinematic view. CSS Flexbox is the solution that worked best to acheive the cinematic layout. Here's the HTML and CSS setup:
<div id="container"> <header id="header"></header> <div id="scene"></div> <footer id="footer"></footer> </div>
#container{ max-width: 1920px; height: 100vh; margin: auto; display: flex; flex-flow: column; } #header{ flex: 1; align-self: center; } #scene{ flex: 3; max-height: 720px; min-height: 460px; } #footer{ flex: 1; align-self: center; }
In the code above, a container is set to span across the full height of the viewport with height: 100vh
, to display its child elements as flex elements with display: flex
, and to distribute these elements vertically with flex-flow: column
.
Each one of the 3 child elements (header, scene and footer) has a flex property. The header and the footer have a flex: 1
and the scene has a flex: 3
. This means the viewport height will be divided by 1+3+1=5, and then distributed to each element according to their flex property: 1 fraction for the header, 3 fractions for the scene, and 1 fraction for the footer. Tip: I found that the content was slightly overflowing beyond the viewport on Internet Explorer 11, which triggered scroll bars to appear. Adding align-self: center
to both the header and the footer fixes the issue.
This setup achieves the desired view and black bars layout. But additional code is required to adapt the scene to different screen sizes.
Responsive. The scene view has to adapt to a wide range of viewports height and width. Since this animation isn't a video, I can't control the aspect ratio and composition of the frame like in film. Instead, I had to allow for a range of tolerance within which the composition is acceptable.
The initial framing was set on a big desktop screen—this animation was started in 2012. But when testing on shorter and narrower mobile screens, the scene didn't adapt well: the plane was too big, it would overlap over the canyon, the trees would completely block the view, and the whole scene would look cramped.
The first fix was to assign a minimum and maximum height to the scene container. That kept the natural structure of the landscape in check: the sky is neither too high nor too low, the clouds stay on top, and the plane flies above the canyon.
The second step was to define a breakpoint that maintains the overall proportions of the scene. The breakpoint scales every single element of the scene by one half, and it kicks in below either a specific viewport width or height. Here's a code example:
@media screen and (max-width: 1024px), screen and (max-height: 920px){ #scene{ min-height: 360px; max-height: 460px; } .clouds{ animation-name: clouds_small; } } @keyframes clouds_small { from {transform: translate(-1520px) scale(0.5);} to {transform: translate(2560px) scale(0.5);} }
In this code, the breakpoint is defined at either 1024 pixels wide or 920 pixels high. Below these thresholds, the scene has specific min-height
and max-height
values, and the clouds CSS animation includes a new parameter scale(0.5)
.
I played with the CSS positioning and the keyframe values of all the elements to ensure the scene composition remains acceptable on different screen sizes. In fact, giving up static framing in favor of responsive framing generates interesting results. We can get different yet still familiar looks depending on the window size. Here's a variety of views from different screen sizes:
Notice how the upper black bar has disappeared on the last 2 views. This is actually a welcomed behavior generated for free by the combination of min-height
and CSS Flexbox: when the viewport height shrinks below a threshold, the browser maintains the scene minimum height, while collapsing the flex header which has no content. This allows the scene to display on top on short screens, which is good.
Typography. The main title is set in Bernard MT Condensed, a nice black typeface reminiscent of western movies. The font is loaded via @font-face. For the credits, I was looking for a movie poster effect. I set the text in Futura with various weights, and played around with line-height
and letter-spacing
until I got a balance that looked right. Tip: if you want to control the line width while using letter-spacing
, subtract an equal value from the margin-right
of each line, because the letter spacing is also added to the right of the last character on the line.
Sound
This is the hackiest part. It's so ugly it's beautiful.
I thought about adding music to the animation late in the process. "Volare" by Gipsy Kings was a perfect fit. But there was a problem: I couldn't just include an MP3 copy of the song because of copyright concerns. So how can I play the music?
YouTube! I could stream the music from YouTube by embedding a YouTube player. I could hide the player with CSS and programmatically play/stop the music with Javascript using the YouTube API—except it doesn't work on iOS devices: "to prevent unsolicited downloads over cellular networks at the user’s expense, embedded media cannot be played automatically in Safari on iOS—the user always initiates playback". This means that on iOS, Javascript playback is impossible until the user explicitly taps the YouTube player.
The ugly hack to circumvent this is to position the player under a custom play button and set its opacity to 0:
When you click "music", you actually click the YouTube player. Once the music starts playing, a CSS class with pointer-events:none
is applied to the player. From there on, play/pause is handled with Javascript.
That is clearly a dark UI pattern. But this animation is a modest experiment and not a production website. I thought it was acceptable to implement such a pattern for the sake of a clean appearance. Tip: on iOS, tapping the YouTube player may automatically trigger fullscreen mode. Add playsinline: '1'
to the player variables via the YouTube API to prevent that.
Tips
If you are a designer or developer, here are a list of tips you might find useful:
- Audio & video: You can play YouTube videos without automatically triggering fullscreen mode on some mobile devices. It's called inline playback. Add
playsinline: '1'
to the player variables in JavaScript. - Cinema 4D: I got some flickering issues when rendering the animation with the physical renderer and global illumination. I found some very useful tips on Creative COW Forums. To get rid of the flickering while maintaining a reasonable render time, you can set the primary GI method to QMC with a custom sample count of 24, and the secondary method to Radiosity Maps with a map density of 50%. See screenshots: General and Radiosity Maps.
- CSS Typography: if you use CSS
letter-spacing
and want to control the line width, subtract an equal value from themargin-right
of the line, because the letter spacing is also added to the right of the last character on the line. - CSS animations: CSS keyframes inside media queries don't work on Internet Explorer. Put them outside media queries.
- CSS media queries: you can assign "OR" statements for breakpoints by coma separating your media queries. For example, for a breakpoint below 1024 pixels wide or 920 pixels high, you can write
@media screen and (max-width: 1024px), screen and (max-height: 920px){}
. - CSS feature queries: you can check browser support for specific CSS property/value pairs using CSS feature queries. For example, the first paragraph letter of this page is a drop cap. But it only shows on browsers that support
initial-letter
. The CSS feature query is written as:@supports (initial-letter: 1) or (-webkit-initial-letter: 1) { p:first-of-type:first-letter{ font-weight: 300; margin-right: 0.5em; -webkit-initial-letter: 2; initial-letter: 2; } }
- HTML: If you want to display HTML code on your page, don't use "<" or ">". Instead, use "<" and ">", respectively. And if you want to display an HTML character code on your page without actually rendering it, replace the "&" that prefixes every HTML character code by its own character code "&".
Links
- There is an academic result that links logical computation with heat dissipation. It's called the Landauer's principle. It states that every irreversible logical manipulation of information goes with an increase of entropy of the system and environment doing the computation. The formula "E ≥ kT ln 2" you can see on the plane is the quantity of energy that must be emitted to the environment after such a computation. See: https://en.wikipedia.org/wiki/Landauer%27s_principle ↩
- Canyon original scene by Eric Nicolas Smit: http://www.ericsmit.com/ ↩
- Cessna 172 original free model: http://animium.com/2012/02/cessna-172-skyhawk-3d-model/ ↩
- SpriteGenerator: https://spritegenerator.codeplex.com/ ↩
- High performance CSS animations: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
- CSS Flexbox playground: https://demo.agektmr.com/flexbox/
- YouTube API reference: https://developers.google.com/youtube/iframe_api_reference