In this lesson, we cover several different ways to animate graphics drawn to Canvas.
[00:00] Animation can be defined as any visual change that occurs over time. We usually think of this as something moving, as in changing position, but it could also be something changing its size, growing and shrinking, changing its rotation, changing its color or texture or a stroke, its opacity, or even changing its overall shape as in one object morphing into another.
[00:21] The "over time" part is very important as well, as I'll now demonstrate. First I'll make a function called "Draw." This will get an x-parameter. It will clear the canvas and then draw a circle at that x with a y equal to half the canvas height.
[00:36] A naïve way to perform animation is to set up a for loop like this. The x-variable will go from 0to 600, and we'll use that to call Draw. You might expect that this will animate the circle across the canvas from the left side to the right, but when we run this, we only see the circle appear on the far right -- no animation.
[00:58] Part of the problem is that virtually no time has elapsed for this animation. There were 600 calls to the Draw function, and you can verify this if you want, but it probably took only a few milliseconds for all those calls to happen, so in a sense, the animation happened too fast for you to see.
[01:15] It's actually worse than that. The way JavaScript works is that the screen is refreshed at regular intervals. Generally without much else going on that will be around 60 times per second, but if any of the JavaScript code is running, it holds off on that screen refresh until your code is complete. Only after your code completes, the screen will refresh and then the graphics that have been drawn to the canvas will be rendered.
[01:38] Thus, if you were to draw 100 lines in a for loop, this would not result in 100 screen renders, only a single render after all that code was completed. In the example we just coded, although the Draw function was called 600 times, the circle was actually only rendered a single time, when the "animation" was finished.
[01:57] What we need to do is change the position of the circle, draw it, and then make sure that we wait until the screen renders before updating in the position again. Some languages have a Sleep function that will suspend code execution for a specified amount of time. With that, a for loop could possibly work, but JavaScript doesn't have a Sleep function, so we need something else.
[02:20] There are two solutions commonly used. The older method is to use Set Interval or Set Timeout. This allows you to specify a function in a timed interval after which the function will be called. In Set Interval, the function will be called repeatedly, while in Set Timeout, the function will be called a single time.
[02:39] Let's get rid of this for loop. I'll create a variable called x-pause, and set it to zero. Then I'll call Set Interval, passing in an anonymous function. This function will call a draw, passing x-pause and then increment x-pause. The function is the first argument to Set Interval. The second argument is the time of the interval. This should be in milliseconds.
[03:01] A trick for setting a particular frames-per-second value is to say 1,000 divided by the frames per second rate you want the interval to run at. 1,000 divided by 30 will give us a frame rate of 30 frames per second. Now you see that the circle is indeed animating across the screen.
[03:21] Now we can go in and change the interval value to 1,000 divided by 60. This will make the animation run at 60 frames per second, and then you can see that the circle is moving faster. We can set it down to 10, and you see that the circle animates very slowly.
[03:38] There are a few problems with animating with intervals. One of the main issues has to do with the screen refreshes I just talked about. Here's an actual graph of the screen refreshes happening in the top on green and a Set Interval function set to run at 60 frames per second on the bottom in blue.
[03:56] Note that it's mostly pretty close, but look here. There's a gap in the screen redraws. Maybe some garbage collection was going on or something, but notice that the Set Interval function kept running. It ran three or four times without the screen ever changing.
[04:12] Sometimes people think they can crank up the frame rate using Set Interval and get super-high frame rate animations. Here I've set the interval to run at 120 frames per second. Notice that the Interval function is indeed running a lot faster, but the frame rate doesn't improve at all. In fact, about every other call to that Interval function is wasted, because what it draws is never rendered to the screen.
[04:35] What happens when you try to go slower with the Set Interval? Here I've set it to run at 40 frames per second, and you can see that it is doing so, but it's way out of sync with the screen refreshes. Some screen refreshes happen with no updates, while others do have updates. Here I've removed the redundant frames that occur without any changes on the screen. What's left is screen refreshes where something actually did change.
[05:00] This represents the effective frame rate. You can see that you're not actually getting an even 40 frames per second, but a jittery, constantly changing frame rate. It's generally running two frames at 60 frames per second, then skipping a frame -- not so good.
[05:16] How do we sync the screen refreshes with our animation code? In comes a relatively new method called "Request Animation Frame." This is available in most modern browsers at this time. Request Animation Frame is a global function. You pass it a function, and the next time the screen is done refreshing, that function will be called.
[05:35] You can then do all your animation and drawing in that function. The next time the screen is refreshed, you'll have your new content. Request Animation Frame only happens a single time, though, so at the end of your handling function, you'll need to call it again for the next frame.
[05:50] Using this method, your code will run exactly one time for every screen refresh and will be perfectly synced. This is the ideal way to achieve the smoothest animation possible in JavaScript.
[06:02] Let's convert the program to use it. I'll remove the Set Interval code, and I'll change the Draw function to remove the parameter and just use x-pause instead. I'll increment x-pause inside there.
[06:14] Now I'll call Draw, which will execute the drawing code a single time, and then at the end of the draw function, I'll call a Request Animation Frame with Draw. This will result in the Draw function being called repeatedly in sync with the frame refresh.
[06:31] As you can see, we've achieved animation. In general, you're probably going to see frame rates of somewhere around 60 frames per second with this method. What if you really want to run at, say, 40 frames per second? There's a little trick that combines the use of Set Interval and Request Animation Frame, getting the benefits of both.
[06:49] Here I'll set up a new interval to run at 40 frames per second, but instead of calling Draw directly there, I'll call Request Animation Frame passing in Draw. This means that on the next available time the screen can be refreshed, it will, and the Draw function will be called.
[07:05] We want to remove the call to Request Animation Frame in the Draw function itself, because we only want to request it in the Set Interval handler. Now the interval will run at somewhere close to 40 frames per second and the screen refreshes will run shortly after each interval call, and also at a rate pretty close to 40. You can see the animation happening.
[07:26] Here's a graph with the Set Interval calls on the bottom and the screen refreshes on top. You can see that sometimes there's a bit of a lag but eventually it catches up and in general, runs pretty smoothly at around 40 frames per second.
[07:38] Let's end off with playing with some of the other ways you can animate an object besides just changing its position. I'll comment out the x-pause variable and create one called "angle," and initialize that to zero.
[07:51] In the Draw function, after clearing the canvas, I'll save it and translate to the center of the canvas width and height, then I'll rotate the canvas by angle, draw a rectangle there using -50, -50 as a location and 100, 100 as the size. Then I'll increment angle by a small amount. Finally, I'll restore the canvas.
[08:13] Now we have a rotating square. Let's try animating scale. I'll comment out the rotation code and create a variable called "scale." If I take the sign of that angle variable, that will result in a value that oscillates between one and minus one. If I add one to that, we'll get values ranging from zero to two.
[08:36] I'll then use that scale value in a call to context.scale. This method take two parameters -- one scaling factor for the x-axis, and one for y. We use the scale variable for both. Now the square is growing and shrinking back and forth. We can uncomment the rotation code and have both animations going on at the same time.
[09:00] There are some very simple examples, but they demonstrate the basic principles you'll use for any programmatic animation. Clear the screen, draw something, wait for the screen to render. Clear the screen again, draw something slightly different, et cetera. Once you have that sequence down, it's just a matter of what you draw and how you change it.