A motion blur

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 JavaScript.

CSS animation, Web Animations API or canvas?

In this tutorial, you're going to create an animation on the HTML5 canvas. But 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.

In the past, people used Flash to make animations for the web. Wondering what happened to it? 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

At the end of the previous tutorial, the rectangle already moved across the screen. Now it's time to take a closer look at how that works, and how far you can take it. Let's start with the most basic version, so you can see what each part does. Here's a simple draw() function:

function draw() {
    context.fillStyle = '#ff8080';
    context.fillRect(rectX, rectY, 150, 100);
}

As you can see, the draw operation fillRect() now uses variables as its arguments. If you change the values of rectX and rectY, you should get a moving rectangle.

Let's mix this up with a basic 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 basic game loop without clear

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 the 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 draw each drawing on top of the previous one, 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. It starts at the upper-left corner at 0,0 and covers canvas.width by canvas.height.

With this new clear method in place, your game loop looks like this:

A basic game loop with update, clear and draw

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 smoothly on just 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 delay 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 your objects at 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 effect 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 on modern hardware, it will run super fast and reach a very high frame rate. Every motion is now performed at lightning speed. When you press an arrow key to move, you'll end up at the edge 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 time. See the following code:

let oldTimeStamp = 0;
let movingSpeed = 120;

function gameLoop(timeStamp) {
    // Calculate the number of seconds passed since the last frame
    let secondsPassed = (timeStamp - oldTimeStamp) / 1000;
    oldTimeStamp = timeStamp;

    // Pass the time to the update
    update(secondsPassed);
    draw();

    window.requestAnimationFrame(gameLoop);
}

function update(secondsPassed) {
    // Move 120 pixels per second, no matter the frame rate
    rectX += movingSpeed * secondsPassed;
    rectY += movingSpeed * secondsPassed;
}

At the start of the gameLoop(), the number of seconds that have passed 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 60 fps, that's roughly 0.0167 seconds per frame. This means that when you want to move an object at 120 pixels per second, you have to multiply 120 by the number of seconds that have passed since the last frame. A game running at 60 fps will move the object 2 pixels per frame. That's what's happening in the update() function.

When the frame rate goes up or down, the distance per frame changes with it. 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 time between two frames gets so long that it breaks your game. Imagine very slow hardware. To keep up with the time that has passed, your objects have to take a huge step every frame. Steps that big make your game logic unstable: an object could jump right through a wall, for example.

You can easily replicate this behavior by running your game in a browser tab and switching to another tab. The browser automatically pauses requestAnimationFrame() when a tab goes into the background, so your game loop stops. The moment you switch back to your first tab, the loop starts again and the time between the last two frames is huge.

To fix this, you'll need to limit the time factor to a maximum for each frame. With the following code in your game loop, your game never moves ahead more than 0.1 second in a single frame. For a game that normally runs at 60 fps, that's still the movement of 6 frames in one step, but no more than that. 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);

Want your game to calculate every single step, even after a long pause? Then use a fixed timestep with an accumulator, as explained in the game loop tutorial.

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 adding easing 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 just 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 Classic tab 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 tool for easing functions shows all 31 of them in action, from a soft sine to a bouncing ball.

Easing functions

For JavaScript and CSS

Find the right easing for your animation or game. Use the tool to see it in action and make your project 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. Next, you'll use easing for more than just the position.

Animate size, rotation, opacity and color

So far, you've only animated the position. But the easing works for anything you can put in a number. Size, rotation, opacity, even color. Use lerp() in combination with an easing function again:

function update(secondsPassed) {
    timePassed += secondsPassed;
    let t = Math.min(timePassed / duration, 1);
    let eased = easeOutCubic(t);
    let colorEased = easeInOutCubic(t);

    size = lerp(0, 80, eased);        // Grow
    angle = lerp(0, Math.PI, eased);  // Half a turn
    opacity = lerp(0, 1, eased);      // Fade in

    // From red (255, 128, 128) to blue (0, 153, 176)
    red = Math.round(lerp(255, 0, colorEased));
    green = Math.round(lerp(128, 153, colorEased));
    blue = Math.round(lerp(128, 176, colorEased));
}

Drawing it takes a few new steps:

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);

    context.save();
    context.globalAlpha = opacity;

    // Move to the center of the square and turn around that point
    context.translate(375, 200);
    context.rotate(angle);

    // Draw around (0, 0), so the square grows from its center
    context.fillStyle = 'rgb(' + red + ', ' + green + ', ' + blue + ')';
    context.fillRect(-size / 2, -size / 2, size, size);
    context.restore();
}
  • Size - Because the square is drawn around (0, 0), it grows from its center instead of from its top-left corner.
  • Rotation - translate() moves the origin of the canvas to the center of the square, and rotate() turns it around that point. The tutorial about images and sprites explains rotating in more detail.
  • Opacity - globalAlpha makes everything you draw see-through, from 0 (invisible) to 1 (solid).
  • Color - A color is three numbers: red, green and blue. Lerp each of them and the color changes smoothly. This one uses easeInOutCubic(), so the color doesn't rush to the end.

save() and restore() put the context back afterwards, so the next thing you draw isn't turned or see-through.

For something that appears, like a coin or a button, try an easing that shoots past the end and comes back. With easeOutBack(), the size goes about 10% too far and then settles. That makes the object pop:

function easeOutBack(t) {
    // How far it goes past the end, about 10%
    const s = 1.70158;
    return 1 + (s + 1) * Math.pow(t - 1, 3) + s * Math.pow(t - 1, 2);
}

size = lerp(0, 80, easeOutBack(t));  // Grow and pop

Here is the result. The size uses easeOutBack(), so the square grows a little past 80 before it settles:

Make things float, pulse and orbit with Math.sin()

Not every animation has a start and an end. A coin that floats up and down, a heart that pulses, a moon that circles a planet: they just keep on going. For that, you can use Math.sin().

Math.sin() turns a number into a smooth wave between -1 and 1. Feed it the time, and the wave repeats forever. Multiply the result by how far you want to move, and add it to the starting point:

const period = 2;  // One full wave every 2 seconds

function update(secondsPassed) {
    timePassed += secondsPassed;
    let wave = Math.sin(timePassed / period * 2 * Math.PI);

    // Float 15 pixels up and down
    coinY = 150 + wave * 15;
}

Math.sin() goes through one full wave in 2 * Math.PI. Divide the time by the period, multiply it by 2 * Math.PI, and you get one wave every 2 seconds. It's based on time, so it's the same speed on every screen.

The same wave works for other things too. Use it for the size, and a heart starts to pulse. Use Math.cos() for x and Math.sin() for y, and an object goes around in a circle:

// Pulse between 90% and 110% of the size
let scale = 1 + wave * 0.1;

// Go around in a circle with a radius of 70
let angle = timePassed / period * 2 * Math.PI;
let moonX = 375 + Math.cos(angle) * 70;
let moonY = 150 + Math.sin(angle) * 70;

Remember Pac-Man from the canvas drawing tutorial? Give his mouth a faster wave of its own, and he starts to chomp. (bite + 1) / 2 turns the wave into a number from 0 to 1, so the mouth goes from closed to open:

// One bite every 0.4 seconds
let bite = Math.sin(timePassed / 0.4 * 2 * Math.PI);

// From 0 (closed) to 0.2 * Math.PI (open, like in the drawing tutorial)
let mouth = (bite + 1) / 2 * 0.2 * Math.PI;

context.beginPath();
context.arc(pacManX, 150, 40, mouth, 2 * Math.PI - mouth);
context.lineTo(pacManX, 150);
context.fill();

Have more than one object? Give each one a head start of a fraction of a second. Then they don't move in sync, but in a wave:

for (let i = 0; i < dots.length; i++) {
    // Each dot is 0.2 seconds ahead of the previous one
    let time = timePassed + i * 0.2;
    dots[i].y = 150 + Math.sin(time / period * 2 * Math.PI) * 25;
}

Here they are together:

Make it bounce with squash and stretch

Animators have a trick that's almost a hundred years old: squash and stretch. A bouncing ball gets flatter when it hits the floor, and longer when it shoots up or falls down. In real life you'd hardly notice it, but in an animation it makes the ball feel soft and alive. It's the first of the twelve basic principles of animation from the Disney studio.

First, the bounce itself. A ball that bounces moves fast near the floor and slows down at the top. 4 * p * (1 - p) gives exactly that curve, for a progress p from 0 to 1:

const bounceTime = 1;  // One bounce per second

timePassed += secondsPassed;
// The progress of the current bounce, from 0 to 1
let p = (timePassed % bounceTime) / bounceTime;

let height = 4 * p * (1 - p);     // 0 on the floor, 1 at the top
let speed = Math.abs(1 - 2 * p);  // 1 on the floor, 0 at the top

The % starts a new bounce every second. Now for the squash and stretch. Make the ball taller and thinner when it moves fast, and wider and flatter when it touches the floor. Keep the width times the height the same, so the ball doesn't seem to grow or shrink:

// 1 on the floor, 0 a little higher
let contact = Math.max(0, 1 - height / 0.15);

// Stretch when it moves fast, squash when it hits the floor
let scaleY = lerp(1 + 0.3 * speed, 0.7, contact);
let scaleX = 1 / scaleY;

Then scale the ball from its bottom. Otherwise it squashes towards its center and floats above the floor:

context.save();
context.translate(ballX, floorY - height * 160);
context.scale(scaleX, scaleY);

// The bottom of the ball is at (0, 0)
context.beginPath();
context.arc(0, -radius, radius, 0, 2 * Math.PI);
context.fill();
context.restore();

Here is the result. The ball on the left just bounces, the one on the right squashes and stretches:

Keep it subtle. A little squash and stretch makes a ball feel bouncy, too much and it looks like jelly. You can use the same idea for a character that lands after a jump, or a button that gets pressed.

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 high-resolution 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 screen with a high pixel density, like most phones, tablets and many laptops, 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 mobile devices, use Math.min(window.devicePixelRatio, 2) as the ratio. This will limit the ratio to 2, even when a screen supports more than 2.

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. On the right, a small part of each block is shown 8 times larger, so you can see the real pixels of your screen:

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 canvas setup tutorial 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 ball moves from left to right and back with ease-in-out, changes color on the way and bounces with squash and stretch. It leaves a short trail. Above it, a square pops in with easeOutBack(), turning and fading in, and then floats up and down with Math.sin(). Everything is sharp on every screen and moves 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 floorY = 370;
        const duration = 3;
        const bounceTime = 0.75;
        const period = 2;
        const reduceMotion =
            window.matchMedia('(prefers-reduced-motion: reduce)');

        let canvas;
        let context;
        let ratio = 1;
        let oldTimeStamp = 0;
        let timePassed = 0;

        // The ball, at rest on the left
        let ballX = 100;
        let ballY = floorY;
        let ballScaleY = 1;
        let ballColor = 'rgb(255, 128, 128)';

        // The square, at its full size
        let squareY = 80;
        let squareSize = 50;
        let squareAngle = 0;
        let squareOpacity = 1;

        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;
            updateBall();
            updateSquare();
        }

        function updateBall() {
            // Progress from 0 to 2: there and back again
            let t = (timePassed / duration) % 2;
            if (t > 1) {
                t = 2 - t;
            }
            let eased = easeInOutCubic(t);
            ballX = lerp(100, 650, eased);

            // From red (255, 128, 128) to blue (0, 153, 176)
            let red = Math.round(lerp(255, 0, eased));
            let green = Math.round(lerp(128, 153, eased));
            let blue = Math.round(lerp(128, 176, eased));
            ballColor = 'rgb(' + red + ', ' + green + ', ' + blue + ')';

            // The progress of the current bounce, from 0 to 1
            let p = (timePassed % bounceTime) / bounceTime;

            // Height and speed: 0 and 1 on the floor, 1 and 0 at the top
            let height = 4 * p * (1 - p);
            let speed = Math.abs(1 - 2 * p);
            ballY = floorY - height * 160;

            // Stretch when it moves fast, squash when it hits the floor
            let contact = Math.max(0, 1 - height / 0.15);
            ballScaleY = lerp(1 + 0.3 * speed, 0.7, contact);
        }

        function updateSquare() {
            // Pop in during the first second: grow, turn and fade in
            let pop = Math.min(timePassed, 1);
            squareSize = lerp(0, 50, easeOutBack(pop));
            squareAngle = lerp(-Math.PI, 0, easeOutBack(pop));
            squareOpacity = pop;

            // Float 15 pixels up and down, one wave every 2 seconds
            let wave = Math.sin(timePassed / period * 2 * Math.PI);
            squareY = 80 + wave * 15;
        }

        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';

            // Paint the top white again, so the square leaves no trail.
            // The ball never gets this high.
            context.fillStyle = 'white';
            context.fillRect(0, 0, gameWidth, 145);

            // The floor
            context.fillStyle = '#d0d0d0';
            context.fillRect(0, floorY, gameWidth, 3);

            drawBall();
            drawSquare();
        }

        function drawBall() {
            context.save();

            // Scale from the bottom of the ball, on whole pixels
            context.translate(roundToPixel(ballX), roundToPixel(ballY));
            context.scale(1 / ballScaleY, ballScaleY);

            context.fillStyle = ballColor;
            context.beginPath();
            context.arc(0, -30, 30, 0, 2 * Math.PI);
            context.fill();
            context.restore();
        }

        function drawSquare() {
            context.save();
            context.globalAlpha = squareOpacity;

            // Turn around the center of the square, on whole pixels
            context.translate(375, roundToPixel(squareY));
            context.rotate(squareAngle);

            context.fillStyle = '#0099b0';
            context.fillRect(
                -squareSize / 2, -squareSize / 2, squareSize, squareSize
            );
            context.restore();
        }

        // Round to a whole pixel of the screen
        function roundToPixel(value) {
            return Math.round(value * ratio) / ratio;
        }

        // 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;
        }

        function easeOutBack(t) {
            // How far it goes past the end, about 10%
            const s = 1.70158;
            return 1 +
                (s + 1) * Math.pow(t - 1, 3) +
                s * Math.pow(t - 1, 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 can also make them grow, turn, fade and change color, keep them floating and pulsing with Math.sin(), and make them bounce with squash and stretch. 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

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.