Finally: Sprite animations implemented via CSS3 Animations
I’m lazy, show me the end result first!
CSS Animations are great – they are a lot less pain for the normal web developer, are rendered more smoothly as the browser can control the framerate, and even become hardware accelerated. You can animate almost everything with it and be happy. Unfortunately, 90% of animations a classical 2D game engine requires cannot be implemented via CSS Animations: yes, I’m talking about the good old sprite animations.
Sprite animations require a big spritesheet that includes all frames (I use big png’s), and for the render, only the size of a frame is shown, while the animation is done by shifting the masked image. In web development, the easiest way to implement sprite animations is by utilizing a <div> with the spritesheet set as background-image, and then shifting its background-position via JavaScript. So since background-position really is just a CSS property, CSS Animations shouldn’t be an issue, no? You bet.
CSS animations are implemented around the concept of keyframing. You define a key rule, name it, and then define keyframe rules in percentages. Within these rules, almost every CSS property is allowed (no gradients, unfortunately!). As second step, the named keyframe animation is used like a variable and placed upon another css rule, using the -webkit-animation syntax. It is important what happens now: if you create two keyframe rules at 0% and 100%, changing the background-position-x from 0px to -100px, it will create frames inbetween for a smooth transition. This is usually very nice, but it destroys the idea of sprite animating: sprite frames need to be switched from one to another instantly, not scrolled!
Frustrated as I was, I gave up and let it go for now, implementing it in JavaScript instead. Today though, I revisited CSS Animations with new knowledge and new motivation, and yes, finally found out how to do it. Let me show you how.
The markup
This is the easiest part. Just a div with a class.
<div class='animated-sprite'></div>
The basic CSS rule
We now continue with the CSS, starting to style it as if we would animate it normally via JavaScript.
div.animated-sprite {
width: 86px;
height: 90px;
background-image: url(animated-sprite.png);
}
Where do the values come from? A single frame has the size 86×90, so we resize the div to exactly the size of one frame. This should therefore display the first frame of the spritesheet that we set as background.
The magic sauce
This still looks to simple, right? Well, lets spice it up! We now change our markup to the following:
div.animated-sprite {
width: 1px;
height: 1px;
background-image: url(animated-sprite.png);
background-size: 7px 6px;
-webkit-transform: scaleX(86) scaleY(90);
-webkit-transform-origin: top left;
}
For those of you who end up thinking “Holy crap, what is he thinking?!”, let me detail what this is doing:
- scale the element down to 1px by 1px
- scale down the background in proportion using CSS3′s background-size
- scale it up again using 2D CSS transforms
- set the transform origin to the top left corner so the scaling doesn’t move the element
If you closely followed the instructions, you will notice that the element looks exactly the same than before, with the much simpler rule above. Usually, I was expecting to see a blurred pixel – after all, you are scaling up a one pixel element! Luckily, 2D CSS transforms are smart enough to remap the actual image resource after the scale, realizing that the original image is of a higher resolution, and then displaying it with the highest resolution possible. Now what is this good for, you might ask? Read on.
Tricking out the easing
The whole purpose of resizing the element to the absolute minimum the browser can handle, is that it’s also the absolute minimum CSS transforms’ easing can handle. This means if you animate something via keyframes by a single pixel, it cannot tween inbetween, meaning it cannot do a subpixel transition. This is good, as we just fooled the CSS transform engine: We can now animate the background position by single pixels, which the result ending up shifting the background spritesheet by a whole frame, without any transition inbetween. Keyframes become frames.
Creating the final keyframe animation
Now all that’s left for us is the actual CSS animation that we still need to create. This is painful by hand, so I created a little helper function that generates them on the fly. Enjoy:
function generateKeyframeAnimation(animationName, frameWidth, frameHeight, spriteWidth, spriteHeight, frames) {
var css = '@-webkit-keyframes ''+animationName+'' {', step = 100 / (frames-1), horizontalFrames = spriteWidth/frameWidth;
for (var i=0; i < frames; i++) {
css += 'n'+((i) * step)+'% { background-position: '+(-i)+'px '+(-Math.floor(i / horizontalFrames))+'px; }';
};
return css+'n}';
}
But in the appropriate values and as animationName any name the animation should receive, and it will return something like this:
@-webkit-keyframes 'animated-sprite' {
0% { background-position: 0px 0px; }
14.285714285714286% { background-position: -1px 0px; }
28.571428571428573% { background-position: -2px 0px; }
42.85714285714286% { background-position: -3px 0px; }
57.142857142857146% { background-position: -4px 0px; }
71.42857142857143% { background-position: -5px 0px; }
85.71428571428572% { background-position: -6px 0px; }
100% { background-position: -7px 0px; }
}
Take this and put it in your CSS, then add a couple animation rules to the actual animated-sprite rule, so the end result will look like this:
div.animated-sprite {
width: 1px;
height: 1px;
background-image: url(animated-sprite.png);
background-size: 7px 6px;
-webkit-transform: scaleX(86) scaleY(90);
-webkit-transform-origin: top left;
-webkit-animation-name: 'animated-sprite';
-webkit-animation-duration: 1s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
}
This will tell CSS to map the animation we just created to this element, have it executed in one second (I’m doing 8 fps here), and playback should be linear and looping.
The final result, with some caveats
That’s it, really! We tricked out CSS transforms, had keyframes become frames, and have awesome sprite animations running with nothing but pure CSS. Neat, huh?
However, there are a couple of caveats, which is why this is very experimental, and not meant for any production use:
- This will not be faster than the JavaScript animation method. I was thinking to speed it up through hardware accelerated 3D transforms, but scale3D unfortunately ignores the image resource, and actually produces a big blurred pixel when scaling the 1px image up again.
- There’s a little bump at the end/start of every loop. I don’t know why, but I bet it’s related to how CSS transforms jump from 100% to 0%. Might be fixable.
- This requires tons of generated CSS for larger animations, not really practical. A JavaScript engine can compute steps on the fly.
- This is only a temporary workaround, until all browser implement the ‘step’ easing property. It will automatically disable the easing, with no crazy workarounds required.
Enjoy, until next time!
12 Comments
Shwartz on December 7th, 2010
I am looking on all of this -web-kit-animation, itiration, duration rules and thinking – this is more for the Javascript tasks not CSS? Or this is not anymore CSS but actually kind a API to browsers special features?
Overall very nice step by step tutorial. Thank you for sharing!
louisremi on December 7th, 2010
“Luckily, 2D CSS transforms are smart enough to remap the actual image resource after the scale, realizing that the original image is of a higher resolution, and then displaying it with the highest resolution possible”
Nice workaround, but I doubt this is in the spec. and if it’s not, I bet you won’t have as much luck with other browsers.
Paul on December 7th, 2010
@Dirk cool demo! Your idea sounds interesting as well :)
@Shwartz: This is really CSS, a new CSS3 feature that is in draft state but already implemented in WebKit based browsers.
@louisremi: Yes, this is why it’s marked experimental :)
mors on December 7th, 2010
You’re not using css3 animations because such thing does not exist.
You’re using proprietary webkit css extensions.Could you fix that please instead of using misleading buzzwords ?
Doug Neiner on December 7th, 2010
Hi Paul!
So first, really cool work around, but I played with another method I wondered if you had tried yet. Basically, you have an extra keyframe 0.01% before the change that shows the previous value. I played with a few different values, and it seems to slide instead of step occasionally if the timing gets off from the change, but this doesn’t use any hacks and should be hardware accelerated. Might give you some stuff to play with.
http://pixelgraphics.us/share_d2tx54/test.html
Also, the timing jump happens because of how you are splitting up the values not because of a bug I don’t think. Scott González and I played with it a bit, and it seems your final step should happen before 100%.
Paul on December 7th, 2010
@ Doug: way to go, awesome! This seems so simple after thinking about it, haha :)
Now the way to make it hardware accelerated is to add an extra node and then use translate3d instead of background position to move the hole thing. I will create another test and see how it performs..
Thanks!
Bram.us » Sprite animations implemented via CSS3 Animations on December 8th, 2010
[...] + '//connect.facebook.net/en_US/all.js'; document.getElementById('fb-root').appendChild(e); }()); Great stuff by Paul Bakaus. Notice the very — very! — use of the CSS background-size and CSS transforms!About this [...]
bam on December 8th, 2010
So.. what’s her name?
handloomweaver on December 13th, 2010
Paul
Any update to this with translate 3d & the extra node?
Philippines Virtual Assistant on December 15th, 2010
Nice animations … Is there a good tool you can recommend that’s WYSIWYG type of thing for CSS3 … great job… thanks — Kathleen :)
jcDesigns on December 16th, 2010
@Philippines Virtual Assistant
I haven’t tried it, but I read about and looked at it. I think this is what you are looking for:
CSS
HTML5
Performance
Dirk on December 7th, 2010
Was scratching my head lately on exactly the same prob, to make eyes blink. http://www.eleqtriq.com/2010/11/showcase-a-pop-up-book-in-html-and-css/
Left it with a shorter-than-duration-of-one-frame-transition then, but your idea is brilliant