Spicy Yoghurt | Last updated: 11 October 2026 | HTML5 game tutorial
Create a smooth canvas animation
Create a smooth canvas animation with JavaScript. Learn how to clear the canvas, add motion trails and easing, and keep it sharp on every screen. By the end of this tutorial, you can create a basic animation with the use JavaScript.
CSS animation, Web Animations API or canvas?
There's more than one way to make things move on a web page. Which one you pick depends on what you want to animate. Here's a quick overview:
- CSS animations and transitions - The easiest way to animate HTML elements, like a button that grows when you hover over it or a menu that slides in. You describe the start and the end in a CSS animation, and the browser does the rest.
- Web Animations API - The same kind of animations as CSS, but started from JavaScript with element.animate(). The Web Animations API lets you pause, reverse or speed up an animation while it runs.
- Canvas - You draw every frame yourself, with JavaScript. It's more work, but you can draw anything you like and hundreds of moving objects are no problem. That's why games use it.
So, for a website with a few moving elements, CSS is usually the way to go. For a game, a particle effect or anything else with lots of moving objects, you'll want the canvas. That's what this tutorial is about.
Wondering what happened to Flash, the way people used to make web animations? You can read all about it in the introduction to HTML5 game development. Now, let's continue and learn how to create your first canvas animation.
Draw objects dynamically on the canvas
In the previous tutorial you've learned how to draw repeatedly on the canvas. But the used draw() function is very static. It only draws one rectangle at the same position, over and over again. Let's make things a bit more interesting. Change the old draw() function to this new one:
function draw() {
context.fillStyle = '#ff8080';
context.fillRect(rectX, rectY, 150, 100);
}As you can see, the draw operation fillRect() is now using variable arguments as input. If you change the values of rectX and rectY, you should get a moving rectangle.
Let's mix this up with the game loop. With the following code you can update the position of the rectangle:
let rectX = 0;
let rectY = 0;
function gameLoop(timeStamp) {
// Update game objects in the loop
update();
draw();
window.requestAnimationFrame(gameLoop);
}
function update() {
rectX += 1;
rectY += 1;
}When to update the position of your game objects?
A simple update() function slightly changes the coordinates of the rectangle. Every frame the update() function is called in the gameLoop() and this will make the rectangle move. It is important to perform the update() before the draw(), so that you'll always draw the latest state of your game to the canvas.
Remember this when you start to add other tasks to your game loop. Always update the state of your game objects first and, as a final action, draw everything to the screen. For now, your game loop looks like this:
A blur of repeated graphics
When executing the code, you should expect to see the rectangle move in a slanted line, from the upper left corner to the lower right. Here is what you get:
What is happening here? It looks like an endless repetition of rectangles, creating a big blur. The rectangle is drawn on the canvas, but the previous rectangle is never removed. Why is that?
How does the canvas work?
Well, the canvas acts as a drawing board. You can draw on it, change position of the rectangle, and draw it again. Do this quickly enough and you will get the perception of motion.
But when you forget to clear the canvas, you will draw each drawing on top of the previous. Creating a blur of drawings, like in the example above.
You can make use of this effect to create some interesting graphics, but that's not what you need right now. You want to animate the rectangle and not create a blur.
Clear the canvas before drawing
To solve this problem, you'll have to clear the canvas before every new drawing operation. So, every time you draw, you start with a clean slate. This will prevent the blur effect.
Add clearRect() to the draw() function to always clear the canvas before drawing:
function draw() {
// Clear the entire canvas
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#ff8080';
context.fillRect(rectX, rectY, 150, 100);
}The clearRect() function clears a part of the canvas. In this case, it is set to clear an area covering the entire canvas. Starting from the upper left corner at 0,0 , continuing for canvas.width and canvas.height.
With this new clear method in place, your game loop looks like this:
When you run the new code, the rectangle is animated correctly. See the example below:
Effects of dynamic frame rate on the animation
The next few sections are a recap of the JavaScript game loop tutorial. If you already know how to move objects relative to time, scroll down to the next section.
Right now, the animation is really simple and runs smooth on about every device. But what if the animation was a lot more complex, like when running a game with many different animated objects?
Your computer or mobile device might not be able to keep up and delays some frames. Later, when there is less stress on the system, it might speed up again. The motion of your animation will be held back or speed up too and you don't want that to happen. You want to move you objects with a constant speed, no matter the frame rate, so your animation will always look the same on any kind of hardware or device.
When running older games, you can see this kind of effects very clearly. These games do not compensate for different frame rates and were designed for old hardware with a slow clock speed. When you run a game like that with modern hardware, it will run super-fast and reach a very high frame rate. Every motion is now performed with lightning speed. When you press an arrow key to move, you'll end up at the end of the screen in a flash.
The movement speed of objects doesn't seem to take the frame rate into account. But how can you fix this for your own animation or game?
Handle dynamic frame rates
To compensate for the effects of a dynamic frame rate, you'll want to include time as a factor in your animation. This way, it's no longer the frame rate (and hardware) that decides the speed of your game, but it's time. See the following code:
let secondsPassed = 0;
let oldTimeStamp = 0;
let movingSpeed = 50;
function gameLoop(timeStamp) {
// Calculate how much time has passed
secondsPassed = (timeStamp - oldTimeStamp) / 1000;
oldTimeStamp = timeStamp;
// Pass the time to the update
update(secondsPassed);
draw();
window.requestAnimationFrame(gameLoop);
}
function update(secondsPassed) {
// Use time to calculate new position
rectX += (movingSpeed * secondsPassed);
rectY += (movingSpeed * secondsPassed);
}At the start of the gameLoop() the number of passed seconds gets calculated. This value is passed to the update() function. There it is used to calculate the new position of the rectangle. Using time as a factor.
Move relative to time
Let's explain this some more. When your game runs at 60fps, that's roughly 0.0167 seconds per frame. This means that when you want to move an object with 50 pixels per second, you have to multiply 50 by the number of seconds that have passed since the last frame. A game running at 60 fps will move the object 0.835 pixels per frame. That's what's happening in the update() function.
When the frame rate increases or decreases, the movement speed will too. No matter how much time has passed, your objects will always move at the desired speed. This makes the animation more suitable for different kinds of hardware, with different frame rates. Of course, a lower frame rate will make the animation look choppy, but the displacement of the objects stays the same.
Limit the time skip
In some special cases, the movement correction to make objects always move at the same speed becomes so large, it will eventually break your game. Imagine hardware that is infinitely slow, your movement speed will be huge to make up for the gap in time between your previous and current frame. Your objects would have to move with such a big step each frame that game logic becomes unstable.
You can easily replicate this behaviour by running your game in a browser tab and switching to another random tab. The moment you switch back to your first tab, the time between the last two frames is huge.
To fix this, you'll need to limit the time factor to a maximum amount for each frame. By adding the next code to you game loop, your game will never move ahead more than 0.1 second in (game) time. For a game that would normally run at 60fps, this would still mean you've squeezed 6 frames into 1. You can play with this number to make it fit your game.
// Move forward in time with a maximum amount
secondsPassed = Math.min(secondsPassed, 0.1);Turn the blur into motion trails
Remember the blur of rectangles at the start of this tutorial? It was a mistake back then, but you can also use it on purpose. A ball that leaves a trail looks fast, a comet needs a tail, and a mouse cursor with a trail is just fun to play with.
The trick is to not clear the canvas completely. Instead, you paint over it with a see-through color. The old drawings don't disappear at once, but fade out a little more every frame:
function draw() {
// Paint over the canvas with a see-through white
context.fillStyle = 'rgba(255, 255, 255, 0.2)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#ff8080';
context.fillRect(rectX, rectY, 150, 100);
}The last number in rgba() is the alpha: how see-through the color is. With 0.2, every frame keeps 80% of the old picture. After 10 frames there's only about 11% left of a drawing, so at 60 fps the trail is roughly a sixth of a second long. A lower number gives a longer trail.
There's one catch. On a 120 Hz screen you paint over the canvas twice as often, so the trail is only half as long. You already know the fix: use time. Calculate how much to paint over from the secondsPassed of the current frame:
// Paint over 20% of the old picture for every 1/60 of a second
let fade = 1 - Math.pow(0.8, secondsPassed * 60);
context.fillStyle = 'rgba(255, 255, 255, ' + fade + ')';At 60 fps, secondsPassed * 60 is 1 and fade is 0.2, just like before. At 120 fps it's about 0.11, but twice as often, and that gives the same trail. Try it out with the buttons below:
With a long trail, you might see a faint gray shadow where the balls have been. That's a rounding problem. The colors on the canvas are stored as whole numbers from 0 to 255. When the step towards white gets smaller than half a number, it rounds to nothing, and the pixel gets stuck just below white. On a 120 Hz screen the steps are smaller, so you'll see it sooner.
You can fix this by adding that last bit of white yourself. The composite operation 'lighter' adds colors up, so every pixel gets one step closer to white:
// Push the last bit of the trail to white
context.globalCompositeOperation = 'lighter';
context.fillStyle = 'rgb(1, 1, 1)';
context.fillRect(0, 0, canvas.width, canvas.height);
context.globalCompositeOperation = 'source-over';Do this right after painting over the canvas. It only works on a solid background, so fill the canvas with white once before the animation starts. The demo above does both.
Easing the motion of an animation
Now that you can use time as a factor, you can do some fun things with your animation. Right now, the rectangle moves at the same speed all the time. Things in real life don't move like that. A car speeds up slowly and brakes before the traffic light. You can get that feeling in your animation by applying an ease to the animation.
First, turn the time into a progress from 0 to 1. 0 is the start of the animation, 1 is the end. Then use that number to calculate the position:
const duration = 1.5;
let timePassed = 0;
function update(secondsPassed) {
timePassed += secondsPassed;
// Progress from 0 (start) to 1 (end)
let t = Math.min(timePassed / duration, 1);
rectX = lerp(50, 550, easeOutCubic(t));
}
// Go from a to b, with t from 0 to 1
function lerp(a, b, t) {
return a + (b - a) * t;
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}The animation takes 1.5 seconds. Math.min() makes sure the progress stops at 1, so the rectangle stays at the end.
lerp() is short for linear interpolation. It finds the value between a and b at progress t. Halfway, at 0.5, it gives 300. You'll find this little function in about every game engine.
The easing function is where it gets interesting. It takes the progress and bends it. It still starts at 0 and ends at 1, but in between it goes faster or slower. easeOutCubic() starts fast and slows down at the end: halfway through the time, the rectangle has already covered 87.5% of the distance.
Here is the result. The numbers in the corner show the progress and what the easing function makes of it:
Ease in, ease out or both?
There are three basic kinds of easing, plus linear motion without any easing:
- Linear - The same speed from start to end. It looks a bit mechanical, but it's fine for a conveyor belt or a spinning wheel.
- Ease in - Starts slow and speeds up. Use it for something that falls or leaves the screen.
- Ease out - Starts fast and slows down. This feels natural for things that arrive or come to a stop, like a menu that slides in. When in doubt, pick this one.
- Ease in-out - Slow at the start and at the end. Good for moving from one spot on the screen to another, like a camera that pans.
Here are the cubic versions of all three:
function easeInCubic(t) {
return t * t * t;
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}Cubic means the progress is raised to the power of 3. With a power of 2 (quad) the effect is softer, with 4 (quart) or 5 (quint) it gets stronger. Switch between the easing functions in the demo below and keep an eye on the graph: it shows the progress over time.
The gray block always moves linearly, so you can compare. With ease-out the red block runs ahead of it, with ease-in it lags behind. But they always arrive at exactly the same moment.
In older code, you'll often come across easing functions with four arguments instead of one. They come from Robert Penner, who wrote the classic set of easing equations back in the days of Flash. Here's the same ease-out in that form:
// Move from 50 to 550 in 1.5 seconds
rectX = easeOutCubic(timePassed, 50, 500, 1.5);
function easeOutCubic (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}The four arguments are the time that has passed (t), the begin value (b), the change in value (c) and the duration (d). The curve is the same, these functions just do the lerp() for you. Need one with a progress from 0 to 1? Pass 0, 1 and 1: easeOutCubic(t, 0, 1, 1). And watch out, these functions don't stop at the end: when t gets bigger than d, they just keep on going.
The animation tool for easing functions has both forms. Click the tab Classic above its code to get the original functions.
The easing functions might look difficult to understand, but the good news is you don't really have to. As long as you know what goes in and what comes out, you can use them. Want to see more? The animation tool for easing functions shows more than 30 of them in action, from a soft sine to a bouncing ball.
Animation Tool
Easing functions made easy
Eased motions are a great way to spicy up your animations. Use this tool to visualise easing functions and make your animations come to life.
Use the animation tool to find the easing that fits your animation. Play around in the update() function to create new movement effects. That's all for now on easing.
Keep your canvas sharp on every screen
This section is a recap of the canvas setup tutorial. If you already know how to keep your canvas sharp with devicePixelRatio, scroll down to the next section.
Does your animation look a bit blurry on a phone, or on a laptop with a retina screen? Good chance it's because of the pixels.
A canvas has its own pixels: 750 by 400 in this tutorial. But a modern screen often has 2 or even 3 real pixels for every pixel in CSS. The browser then stretches your canvas to fill them, and stretching makes it blurry. How many real pixels there are for each CSS pixel is in window.devicePixelRatio.
The fix is to give the canvas more pixels and to scale your drawing along with it. Here's how:
const gameWidth = 750;
const gameHeight = 400;
function resizeCanvas() {
// One canvas pixel for every pixel of the screen
let ratio = window.devicePixelRatio || 1;
canvas.width = Math.round(canvas.clientWidth * ratio);
canvas.height = Math.round(canvas.clientHeight * ratio);
// Keep drawing in game coordinates (750 x 400)
context.setTransform(
canvas.width / gameWidth, 0,
0, canvas.height / gameHeight,
0, 0
);
}
window.addEventListener('resize', resizeCanvas);Call resizeCanvas() once in init(), before the game loop starts. clientWidth and clientHeight give the size of the canvas on the page, in CSS pixels. The canvas gets that many pixels times the ratio, and setTransform() scales everything you draw. Your game keeps using a playing field of 750 by 400, so you don't have to change any of your drawing code.
This only works when the size on the page comes from CSS, like the width: 100% from the canvas setup tutorial. Without it, the canvas would grow every time you resize it.
Here's the difference. The left half is drawn the old way, the right half uses the device pixel ratio. On a regular screen with a ratio of 1 both halves look the same, but on a phone the left half is clearly blurry:
Keep in mind that a canvas with three times as many pixels in both directions has nine times as many pixels to draw. If your game gets slow on phones, use Math.min(window.devicePixelRatio, 2) as the ratio.
Fix shaky edges with whole pixels
Here's another one you might notice when something moves slowly. The edges of the rectangle seem to tremble a little, and thin lines flicker between sharp and soft. What's going on?
Your objects move with time, so their position is almost never a whole number. At an x-position of 100.4, the edge of the rectangle falls in the middle of a pixel. The browser can't light up 40% of a pixel, so it mixes the colors and the edge gets soft. This is called anti-aliasing. When the object moves, the mix changes every frame, and that's the trembling you see.
You can fix it by rounding the position when you draw. Only when you draw, so your game logic keeps the exact position:
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
// Draw on whole pixels, but keep the exact position in rectX
context.fillStyle = '#ff8080';
context.fillRect(Math.round(rectX), Math.round(rectY), 150, 100);
}In the demo below both blocks move at 6 pixels per second. The top one uses the exact position, the bottom one is rounded:
Rounding has a downside: at a very low speed you can see the steps of one pixel. For big, slow shapes the exact position sometimes looks smoother, so try both. For pixel art you'll always want whole pixels. The tutorial about images and sprite animations explains how to keep pixel art crisp when you scale it.
Do you use the device pixel ratio from the previous section? Then round to real pixels instead: Math.round(rectX * ratio) / ratio.
What makes an animation stutter?
Sometimes an animation runs fine and then suddenly hitches for a moment. That's a frame that took too long, so the browser had to skip it. These are the usual suspects:
- Too much work in one frame - At 60 Hz you have about 16.7 milliseconds per frame, at 120 Hz only 8.3. When update() and draw() take longer, a frame gets skipped. The performance panel in the developer tools of your browser shows where the time goes.
- New objects in every frame - Creating new arrays and objects in the game loop leaves a lot of garbage behind. Now and then the browser cleans it up, and that can cost a frame. Create your objects once and reuse them.
- Loading in the middle of the animation - An image that's drawn for the first time sometimes needs a moment to decode. Load everything before you start, like in the images and sprites tutorial.
- Reading the page layout in the loop - Asking for sizes like clientWidth or getBoundingClientRect() every frame can make the browser recalculate the page. Read them once, and again when the window resizes.
- Moving per frame - If you move a fixed number of pixels per frame, every skipped frame is a moment where nothing moves. Move with time, as explained above.
- Saving battery - Some devices lower the frame rate to save power, like an iPhone in Low Power Mode, which can go down to 30 fps. With time-based movement your animation keeps the right speed, it just looks a bit less smooth.
Respect the reduce motion setting
Not everybody enjoys things moving on a screen. For some people, big or fast motion causes dizziness or even nausea. That's why most systems have a setting to reduce motion, and the browser passes it on as prefers-reduced-motion. You can check it in JavaScript:
const reduceMotion =
window.matchMedia('(prefers-reduced-motion: reduce)');
function update(secondsPassed) {
// Jump straight to the end when the player prefers less motion
if (reduceMotion.matches) {
rectX = 550;
return;
}
// ...
}reduceMotion.matches stays up to date, so it also works when someone changes the setting while your page is open. Right now it's off on your device.
What you do with it is up to you. Skip an animation that's only there for decoration, make it shorter, or use a fade instead of a big movement. In a game the player still has to move around, so turn off things like screen shake and flashes instead, or add an option for it to the settings menu.
Common questions about canvas animation
Should I use CSS or canvas for animations?
Use CSS to animate HTML elements, like buttons and menus. Use the canvas for games and for anything with a lot of moving objects. The overview at the top of this page explains the differences.
Why does my canvas animation leave a trail?
Because the canvas keeps everything you draw on it. Clear it at the start of every frame with clearRect(). If you do want a trail, paint over it with a see-through color instead.
Why does my canvas look blurry?
Most of the time, the canvas has fewer pixels than the screen shows. Multiply its size by window.devicePixelRatio and scale your drawing with setTransform(), as explained above.
How do I make an animation slow down at the end?
Use an ease-out function, like easeOutCubic(). Turn the time into a progress from 0 to 1, pass it through the easing function and use lerp() to calculate the position.
How do I stop or pause a canvas animation?
Keep the id that requestAnimationFrame() returns and pass it to cancelAnimationFrame(). The tutorial about the JavaScript game loop shows how to start, pause and stop the loop.
The complete code
Here's everything together. A rectangle that moves from left to right and back with ease-in-out, with a short trail, sharp on every screen and on whole pixels. It stands still when reduce motion is on. You can use it as a starting point for your own animation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Smooth Canvas Animation Tutorial | Spicy Yoghurt</title>
</head>
<body>
<canvas
id="canvas"
width="750"
height="400"
style="width: 750px; height: 400px; border: 1px solid lightgrey;"
>
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
'use strict';
const gameWidth = 750;
const gameHeight = 400;
const duration = 1.5;
const reduceMotion =
window.matchMedia('(prefers-reduced-motion: reduce)');
let canvas;
let context;
let ratio = 1;
let oldTimeStamp = 0;
let timePassed = 0;
let rectX = 50;
window.onload = init;
function init() {
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
window.requestAnimationFrame(gameLoop);
}
function resizeCanvas() {
// One canvas pixel for every pixel of the screen
ratio = window.devicePixelRatio || 1;
canvas.width = Math.round(canvas.clientWidth * ratio);
canvas.height = Math.round(canvas.clientHeight * ratio);
// Keep drawing in game coordinates (750 x 400)
context.setTransform(
canvas.width / gameWidth, 0,
0, canvas.height / gameHeight,
0, 0
);
// Start with a solid white background
context.fillStyle = 'white';
context.fillRect(0, 0, gameWidth, gameHeight);
}
function gameLoop(timeStamp) {
// Calculate how much time has passed, with a maximum
let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
oldTimeStamp = timeStamp;
secondsPassed = Math.min(secondsPassed, 0.1);
update(secondsPassed);
draw(secondsPassed);
window.requestAnimationFrame(gameLoop);
}
function update(secondsPassed) {
// Stand still when the player prefers less motion
if (reduceMotion.matches) {
return;
}
timePassed += secondsPassed;
// Progress from 0 to 2: there and back again
let t = (timePassed / duration) % 2;
if (t > 1) {
t = 2 - t;
}
rectX = lerp(50, 550, easeInOutCubic(t));
}
function draw(secondsPassed) {
// Paint over 20% of the old picture for every 1/60 second
let fade = 1 - Math.pow(0.8, secondsPassed * 60);
context.fillStyle = 'rgba(255, 255, 255, ' + fade + ')';
context.fillRect(0, 0, gameWidth, gameHeight);
// Push the last bit of the trail to white
context.globalCompositeOperation = 'lighter';
context.fillStyle = 'rgb(1, 1, 1)';
context.fillRect(0, 0, gameWidth, gameHeight);
context.globalCompositeOperation = 'source-over';
// Draw on whole pixels of the screen
let x = Math.round(rectX * ratio) / ratio;
context.fillStyle = '#ff8080';
context.fillRect(x, 150, 150, 100);
}
// Go from a to b, with t from 0 to 1
function lerp(a, b, t) {
return a + (b - a) * t;
}
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
</script>
</body>
</html>What's next?
That's all on canvas animation for now. You can move objects on the HTML5 canvas, clear it or leave a trail on purpose, and use easing functions to make the motion feel natural. You also know how to use time as a factor, and how to keep your animation sharp and smooth on every screen. If you have any questions, feel free to ask them in the comment section. You can download the example code here.
In the next step of the tutorial, you'll be learning how to create multiple objects, detect collisions and make objects interact with each other.
Comments
Thanks! We've received your comment. We check every comment by hand before it appears, so this can take a few days.
No comments yet. Be the first to leave one!