Animation for HTML5 canvas with Konva

I wanted to learn about animation on the canvas so I’m making a smooth spiral animation of the first 199 words or David Bowie’s ‘Space Oddity’ using the excellent Konva canvas library and its animation feature.

Konva Animations

The way to do animation with the raw canvas is via setTimeout() and requestAnimationFrame(). However, we need to bear in mind the words of the mighty MDN which says:

Probably the biggest limitation is, that once a shape gets drawn, it stays that way. If we need to move it we have to redraw it and everything that was drawn before it. It takes a lot of time to redraw complex frames and the performance depends highly on the speed of the computer it’s running on.

MDN

Which is kind-a like that part in Crocodile Dundee where, referring to the bush tucker meal he just served to his love interest, he says …’Yeah, you can eat it, but it tastes like s**t!’.

Fortunately, Konva does all this work for us under the covers, but serves it all up in the simple Animation object. The Animation constructor takes as its parameters a function and a layer. This function will be run at the point in time when the animation frame (frame from now on) fires, and it is given a number of useful inputs including the time into the animation that the frame runs, and a frame rate which is interesting if you ever need to feel your way through a performance issue.

Before we get into the spiral animation I just need to cover off a detail about how to get smooth animations. This is important because we are going to have 199 shapes on the move and we want it to appear visually smooth.

If, like me, you’re new to animations then you might make the same mistake as I did initially which is to assume that the frames execute in strict time slots, like every millisecond – they don’t.

Instead they run when the GPU has time to run them. Between frames, your device’s processor has to do a bunch of stuff – maybe it’s doing nothing, but more likely its checking for emails, doing some disk input-output, optimising the heap, mining another bitcoin, etc. That stuff takes time and it might not be able to find free time for our animation frames to run on an absolutely regular basis.

The critical principle to know is that we must not rely on any kind of counting of frames to drive the calculations of the position of the animated items. Instead we must use time. Oh – wait a minute, you said the Konva animation function gets given the time into the duration that it runs! Yes indeed, read on.

Just before we hit the code, a quick explanation of the spiral animation concept. The image below shows how the spiral works. The labels T0 to T7 show the times that the frames are fired. At each execution a blue circle is drawn just to show where the animated circle would be at that time. We have three constants – the end point at the center of the spiral where the circle must be at the end of the animation, the initial radius at T0 which is where the spiral path starts, and the full number of degrees around the point that the shape will rotate as the animation runs from start to completion – for example 2 x 360.

At each frame we will calculate the new radius as shown by the reducing radius circles, as we progress from T0 – T7. We will also calculate the angle the shape has moved in the time between the frames hence giving for example that line from T0 to T1. Then simply rinse and repeat, ensuring we jump to the final position as the duration ends.

Here is a gif of one of the paragraphs in action – if you focus on a particular word you can see the spiral path in action.

And here’s the animation function that makes it happen. The outer function is there to show the initial settings for the animation. Line 16 is the function that executes per frame. Look in the code that follows further down for the code that sets up the words etc, I left that off here for brevity.

function animate(){

  // These are the values affecting the animation
  let 
      // initial spiral outer circle radius - reduces per animation frame.
      maxRadius = 40, 
      
      // duration of animation in ms
      duration = 3000,  
      
      // angle to traverse in the duration, in radians.
      rotationDegrees = 360 * Math.PI/180; 

   layer.find('Text').opacity(0); // fade out the text
  
   let anim = new Konva.Animation(function(frame) {

     let time = frame.time;
     let t1 = performance.now()   

     // for each word...
     for (let i = 0; i < wordList.length; i++){

       let 
           // calculate the new radius
           radius = easing.EaseInCubic(frame.time, maxRadius, -maxRadius, duration), 
           // and new angle - note all the words have a different initial angle value
           angle = wordList[i].angle  + easing.EaseInCubic(frame.time, 0, rotationDegrees, duration),

           // and the final position we are destined for
           pos = wordList[i].finalPosition,

           // simple trig to calculate the x,y position of the word at this frame
           x = pos.x + (Math.sin(angle) * radius),
           y = pos.y - (Math.cos(angle) * radius);         

       // Apply the position to the word
       wordList[i].shape.position({x: x, y: y});
       
       // fade the text into view as the animation progresses
       wordList[i].shape.opacity(frame.time / duration);
       
       // If we are at the end of the animation ensure we hit the final target position
       if (time >= duration){
         wordList[i].shape.position(wordList[i].finalPosition);
       }
     }  

     // If we are at the end of the animation then stop it !
     if (time > duration){
       anim.stop()
     }
   
  }, layer);
  
  anim.start();
}

And finally here’s the full code.

A note about easings

The purpose of the animation frame function is to decide where the animating shapes should be placed on the canvas at the time the frame executes. A basic linear movement would mean that the position on the animated route would be directly proportional to the duration consumed. So, if we think about our animation as running from Time zero (T0) to time at 100% of the animation’s duration (T100%) then at T0% the shape would not have moved, at T100% the shape is at the final position of the animation, and at T50% it is precisely half way. But that’s not very slick.

Hey easing, I wanna go from X1 to X2 in 3 seconds and I’m 1.79 seconds into it – where should I be at man?

To make it slicker we can use an ‘easing’. Visually, an easing gives the animation a bounce, or elasticity, or starts slow and ends fast. In code speak, an easing is a function that we can use to calculate something other than a linear movement. As inputs we give the easing the time into the animation that the frame is running, the start and end position of the dimension being animated (for the spiral I do this twice, first for the radius and then for the angle – for a move-along-a-line animation it would be zero and the line length) , and the duration. We are effectively saying ‘Hey easing, I wanna go from X1 to X2 in 3 seconds and I’m 1.79 seconds into it – where should I be at man?‘ The easing function returns that value. There are many easing functions and we all experience them every day as we traverse the web so I won’t go on. There’s a bunch of easing functions in the code above – have fun.

Summary

We’ve had a quick look at the Konva Animation class and in particular the animation function. We’ve used the ‘time’ attribute of the frame parameter that is passed into that function to calculate where each word should be on its independent spiral path at each animation frame.

Small steps into animation, but I’m impressed again with how easy it is to use Konva from vanilla JS and achieve impressive results.

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: