Easing Animations in Canvas

Avatar of Darshan Somashekar
Darshan Somashekar on

The <canvas> element in HTML and Canvas API in JavaScript combine to form one of the main raster graphics and animation possibilities on the web. A common canvas use-case is programmatically generating images for websites, particularly games. That’s exactly what I’ve done in a website I built for playing Solitaire. The cards, including all their movement, is all done in canvas.

In this article, let’s look specifically at animation in canvas and techniques to make them look smoother. We’ll look specifically at easing transitions — like “ease-in” and “ease-out” — that do not come for free in canvas like they do in CSS.

Let’s start with a static canvas. I’ve drawn to the canvas a single playing card that I grabbed out of the DOM:

Let’s start with a basic animation: moving that playing card on the canvas. Even for fairly basic things like requires working from scratch in canvas, so we’ll have to start building out functions we can use.

First, we’ll create functions to help calculate X and Y coordinates:

function getX(params) {
  let distance = params.xTo - params.xFrom;
  let steps = params.frames;
  let progress = params.frame;
  return distance / steps * progress;
}


function getY(params) {
  let distance = params.yTo - params.yFrom;
  let steps = params.frames;
  let progress = params.frame;
  return distance / steps * progress;
}

This will help us update the position values as the image gets animated. Then we’ll keep re-rendering the canvas until the animation is complete. We do this by adding the following code to our addImage() method.

if (params.frame < params.frames) {
  params.frame = params.frame + 1;
  window.requestAnimationFrame(drawCanvas);
  window.requestAnimationFrame(addImage.bind(null, params))
}

Now we have animation! We’re steadily incrementing by 1 unit each time, which we call a linear animation.

You can see how the shape moves from point A to point B in a linear fashion, maintaining the same consistent speed between points. It’s functional, but lacks realism. The start and stop is jarring.

What we want is for the object to accelerate (ease-in) and decelerate (ease-out), so it mimics what a real-world object would do when things like friction and gravity come into play.

A JavaScript easing function

We’ll achieve this with a “cubic” ease-in and ease-out transition. We’ve modified one of the equations found in Robert Penner’s Flash easing functions, to be suitable for what we want to do here.

function getEase(currentProgress, start, distance, steps) {
  currentProgress /= steps/2;
  if (currentProgress < 1) {
    return (distance/2)*(Math.pow(currentProgress, 3)) + start;
  }
  currentProgress -= 2;
  return distance/2*(Math.pow(currentProgress, 3)+ 2) + start;
}

Inserting this into our code, which is a cubic ease, we get a much smoother result. Notice how the card speeds towards the center of the space, then slows down as it reaches the end.

Advanced easing with JavaScript

We can get a slower acceleration with either a quadratic or sinusoidal ease.

function getQuadraticEase(currentProgress, start, distance, steps) {
  currentProgress /= steps/2;
  if (currentProgress <= 1) {
    return (distance/2)*currentProgress*currentProgress + start;
  }
  currentProgress--;
  return -1*(distance/2) * (currentProgress*(currentProgress-2) - 1) + start;
}
function sineEaseInOut(currentProgress, start, distance, steps) {
  return -distance/2 * (Math.cos(Math.PI*currentProgress/steps) - 1) + start;
};

For a faster acceleration, go with a quintic or exponential ease:

function getQuinticEase(currentProgress, start, distance, steps) {
  currentProgress /= steps/2;
  if (currentProgress < 1) {
    return (distance/2)*(Math.pow(currentProgress, 5)) + start;
  }
  currentProgress -= 2;
  return distance/2*(Math.pow(currentProgress, 5) + 2) + start;
}

function expEaseInOut(currentProgress, start, distance, steps) {
  currentProgress /= steps/2;
  if (currentProgress < 1) return distance/2 * Math.pow( 2, 10 * (currentProgress - 1) ) + start;
 currentProgress--;
  return distance/2 * ( -Math.pow( 2, -10 * currentProgress) + 2 ) + start;
};

More sophisticated animations with GSAP

Rolling your own easing functions can be fun, but what if you want more power and flexibility? You could continue writing custom code, or you could consider a more powerful library. Let’s turn to the GreenSock Animation Platform (GSAP) for that.

Animation becomes a lot easier to implement with GSAP. Take this example, where the card bounces at the end. Note that the GSAP library is included in the demo.

The key function is moveCard:

function moveCard() {
  gsap.to(position, {
    duration: 2,
    ease: "bounce.out",
    x: position.xMax, 
    y: position.yMax, 
    onUpdate: function() {
      draw();
    },
    onComplete: function() {
      position.x = position.origX;
      position.y = position.origY;
    }
  });
}

The gsap.to method is where all the magic happens. During the two-second duration, the position object is updated and, with every update, onUpdate is called triggering the canvas to be redrawn.

And we’re not just talking about bounces. There are tons of different easing options to choose from.

Putting it all together

Still unsure about which animation style and method you should be using in canvas when it comes to easing? Here’s a Pen showing different easing animations, including what’s offered in GSAP.

Check out my Solitaire card game  to see a live demo of the non-GSAP animations. In this case, I’ve added animations so that the cards in the game ease-out and ease-in when they move between piles.

In addition to creating motions, easing functions can be applied to any other attribute that has a from and to state, like changes in opacity, rotations, and scaling. I hope you’ll find many ways to use easing functions to make your application or game look smoother.