Update: A way more awesome update with more techniques: Click me!
Warning: This is hackier and more dirty than anything I have published in recent years, and only works in WebKit. Use with extreme caution.

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:

  1. scale the element down to 1px by 1px
  2. scale down the background in proportion using CSS3’s background-size
  3. scale it up again using 2D CSS transforms
  4. 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?

Final running demo

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!