Learning to animate with CSS
I’ve never animated anything before. I am not a visual artist, I can’t even design an 8-bit sprite. It’s just not in my blood. But I would like to start making some rudimentary games in the near future, so today I dipped my toes into the sprite animation pool to get a feel for the basics.
To start with, for this experiment, I used exclusively CSS and React. React wasn’t necessary, it has very little to do with the code here. I could have just used basic HTML (at least at first), but I hadn’t touched React very much lately and I wanted a light refresher for that in addition to learning these animation basics. React would come in handy later-on with this experiment though. So right off the bat, I used create-react-app to get off the ground quickly.
Since I had no idea where to start, some brief research led me to this article by Hannah Gooding which does a great job demonstrating the core concepts of sprite animation, in addition to providing some code that helped me get started.
My previous assumption regarding sprite animation was that there would be several image files for each frame and you simply rotate through each of them. Conceptually, that would work, though it could be cumbersome to load several images, even for very basic animation. Instead, you can create a single image file containing all of your frames and manipulate it in such a way that you still get the same end result as if you used multiple image files. Let’s see how with some help from a very familiar face:
Since, as mentioned, I’m no artist, I found some sprite files of Mario walking. This is a single PNG file with all 3 frames shown as you see here. I used Microsoft Paint (yes, you heard me right) to ensure that these 3 frames were evenly spaced, which is absolutely crucial. Each of these frames is 90px wide (and 105px tall, though that’s not as important for this exercise) and Mario is in the same position within each frame. The width of this image is exactly 270px (or 90px times the total number of frames). MS Paint’s rulers and gridlines were very useful to set this up (though I’m 1,000,000% sure that a more advanced art application would have made this easier).
After dropping that PNG into my React app, it was time to start coding. In React, I started by making a very simple <div> with a className of ‘mario’.
function App() {
return (
<div className="App">
<div className='mario'></div>
</div>
);
}
Then, in CSS I included the path to this image as the ‘background’. I set the width of this class to 90px so that the image is automatically cropped down to a single frame. The margins were added just to move the image out of the top-left corner of the window, though that part wasn’t necessary.
.mario {
margin-left: 50%;
margin-top: 10%;
height: 105px;
width: 90px;
background: url("/public/mario_walk_sprite.png");
}
Now we have Mario frozen in one single frame, specifically the frame on the left side of our PNG, with the other two frames cropped out. In order to animate him, we need to shift the position of the image by 90px to the left or to the right (depending on the order of the individual frames within the PNG) at regular time intervals for steady animation. In my case, I need to shift the image position to the right for the animation to play in the correct order. The code to achieve that involves 2 additions to what I already had:
@keyframes sprite {
from { background-position: 0px }
to { background-position: 270px }
}
.mario {
margin-left: 50%;
margin-top: 10%;
height: 105px;
width: 90px;
background: url("/public/mario_walk_sprite.png");
animation: sprite .5s steps(3) infinite;
}
First, we have the keyframes, which defines “sprite” and gives us the starting and finishing position of the PNG image. We will me “moving” the image 270px to the right in order to rotate through each of the animation frames. Then, within the mario class, we have the animation which takes the now-defined sprite and sets 3 pieces of information. The time comes first, which I set to half of a second, but this could be set to any interval that gives you a smooth look to the animation. Then the number of steps, which is effectively the number of frames to cycle through. Since the PNG is divided into 3 evenly-spaced frames of equal width, setting the number of steps to the number of frames will ensure that the PNG is moved the exact distance we need to center each individual frame. Lastly, it is set to run infinitely. With this, I had a perfectly good animation of Mario walking, but this isn’t quite what I was going for.
It’s one thing to animate pixel art, it’s another to allow a user to trigger when that animation begins and ends. For this, I needed to modify my PNG somewhat.
If the user is going to trigger when Mario starts moving, the user also needs to see Mario standing still. I modified the CSS code to change the name of the class I had already built and to add a new class:
@keyframes sprite {
from { background-position: 90px }
to { background-position: 360px }
}
.mario {
margin-left: 50%;
margin-top: 10%;
height: 105px;
width: 90px;
background: url("/public/mario_walk_sprite.png");
}
.mario-active {
margin-left: 50%;
margin-top: 10%;
height: 105px;
width: 90px;
background: url("/public/mario_walk_sprite.png");
animation: sprite .5s steps(3) infinite;
}
Now, the walking animation is within the class “mario-active” while Mario standing still is under the class name “mario”. The ‘mario” class has no “background-position” so it defaults to 0px, giving us the image of Mario standing still and cropping out the other images. The keyframes were also adjusted to start at 90px and end at 360px so that the animation doesn’t include the image of Mario standing still in the rotation. With that, we need a way to trigger which class to render. For that, we turn to React’s useEffect and useState:
import {useEffect, useState} from 'react';
function App() {
const [walking, setWalking] = useState(false);
useEffect(()=>{
document.addEventListener('keydown', handleDown)
}, [])
useEffect(()=>{
document.addEventListener('keyup', handleUp)
}, [])
function handleDown(e){
console.log(e.key);
if(e.key === 'ArrowRight'){
setWalking(true);
}
}
function handleUp(e){
if(e.key === 'ArrowRight'){
setWalking(false);
}
}
return (
<div className="App">
<div className={walking?'mario-active':'mario'}></div>
</div>
);
}
export default App;
We start with useState to set a boolean value for “walking” with a default of false. In the return, you can see we have a ternary that uses “walking” to set the className for the div, thereby determining whether or not we will see the image of Mario standing still or the animation of him walking.
Then we have useEffect to create event listeners to check if any keys have been pressed down. If the active key is the right arrow, “walking” will be set to true until the key is released, at which point it will become false again, effectively starting and stopping the animation and resetting Mario back to a standing position.
What’s next?
With this code in place, we can start and stop the animation, but Mario looks like he’s walking in place. The next step in this exercise in animation fundamentals is to make it look like Mario is actually moving around a game level. The simplest way that I can think of to achieve that would be to add an image of the entire level that similarly slides to the left while the same right arrow key is held down. Since we aren’t aiming to animate the background, we don’t need it to also move 90px at a time, so the level backdrop could instead be set to move just a few pixels at a time so that we end up with a smooth slide. As long as the pace of the sliding image is set to match the pace of Mario’s walk animation, it should end up looking like he is walking through the level. This wouldn’t achieve the exact same look and feel that the Nintendo classic had, since Mario could move in multiple directions without the background constantly moving with him, but it’s a good starting point as I continue to learn these basic mechanics.