Time-Based Animation in Javascript using RequestAnimationFrame
In this Post ...
I'll discuss key aspects of SVG animation using javascript. In particular, I'll be focusing on time-dependent animation with *the requestAnimationFrame
API. My purpose in doing so is to help artists, developers, AI's and anyone else who might be interested in creating with SVG.
What is RAF?
What exactly is RAF? RAF (requestAnimationFrame
) is a web API designed to enable smooth rendering closely tied to browser based rendering engines. What follows is a guide to tying into the RAF framework to create animated content using SVG.
The Animation Loop
All animation occurs within a very basic loop. This was true from the inception of animated artworks from the early days of film with hand drawn images to modern day computer-based systems. At it's core, the animation loop can be expressed as follows:
WHILE animating: UPDATE animation state MOVE objects CHANGE properties etc... RENDER the current frame (RE)DRAW objects at new positions/states DISPLAY the frame WAIT for an interval of time to elapse
In other words, all animation involves rendering a scene, waiting for an interval of time to pass, updating the scene, and rendering the updated imagery. Over and over again.
Animation on the World Wide Web
Back in the olden days, web-based animation could be implemented using just a few lines of javascript:
function animate() { while ( animating ) { update(); render(); setTimeout(animate, 50); } }
Here, update
would be a function defined to move things around and/or change object properties and render
would be a function to draw to a renderable area like a 2D canvas. The wait period was achieved using javascript's setTimeout
function.
Using this oldschool technique worked OK in the early years of WWW development. But used today it would probably result in janky animation. The reason is that setTimeout
is not synchronized with the browser engine's rendering cycle, so using it to create delays can potentially lead to erratic updates and dropped frames appearing as flicker.
RequestAnimationFrame
The good news is that a better standard for animation was introduced way back in 2015 and has since become widely supported across all browser rendering engines. That is RAF. Specifically intended to enable smooth animation, requestAnimationFrame
is optimized to operate in sync with the browser engine's rendering cycle. It is also worth noting that animation loops using RAF will pause when a user switches tabs or minimizes the animation window in order to reduce CPU utilization and battery usage. RAF is ideal for game-loops, AI simulations and any other use-cases requiring visual updates that need to be synchronized with browsers' rendering systems.
The following code shows an iteration of an animation controller I created early on to support interactive media experiences with SVG.
export const AnimationController = {
previousTimestamp : 0 ,
rafId : 0,
update : function (timestamp) {
// compute delta time in SECONDS
const deltaTime = (timestamp - this.previousTimestamp) / 1000;
this.previousTimestamp = timestamp;
GameController.updateSprites( deltaTime );
this.rafId = requestAnimationFrame( this.update.bind(this) );
},
startAnim : function () {
const startTime = performance.now() ;
this.previousTimestamp = startTime;
this.update( startTime ) ;
},
stopAnim : function () {
cancelAnimationFrame ( this.rafId ) ;
this.rafId = 0;
const stopTime = performance.now();
},
}
The AnimationController
exemplifies the application of the requestAnimationFrame
API. It shows how the animation-loop can be implemented in javascript. Notice:
-
The
update
method (a callback) receives a timestamp as input. -
update
uses the timestamp to compute the time delta between the current update and the previous frame. -
It then invokes
GameController
-- an object in the SVG Creators Collaborative ™ framework for SVG artists -- an object responsible for (among other things) updating sprites. Notice how the AC passes the time delta to the game controller. -
Finally,
update
callsrequestAnimationFrame
with a reference to itself, binding it as a method of theAnimationController
.
As defined in its API, requestAnimationFrame
returns an id for the update, which can be used elsewhere should the request need to be canceled. The one thing you might've noticed though, is that there isn't an explicit wait parameter in the loop. That's where deltaTime
comes in as I'll show in a bit.
Examples
'Time to look a couple of concrete examples. In this post I've narrowed the discussion to two kinds of time-based updates; frame-rate-independent and fixed-interval.
Frame Rate
Frame Rate refers to the frequency at which consecutive images, or frames are displayed in an animated sequence. Frame rate is measured in frames per second and taken as quotient of the number of frames divided by elapsed time:$$FPS = \frac{number\hspace{5px} of\hspace{5px} frames}{elapsed \hspace{5px} time}$$
Frame-rate-independent updates
Frame-rate-independent updates are scheduled independently of the frame rate and are used typically to animate motion and physics simulations. As an example, I've inlined an animation of a certain wicked witch flying on her broom.
I created the animation by defining an SVG sprite and tapping into the animation controller logic through the SVG Creators Collaborative ™ artists' framework. Within this framework, an SVG sprite can be defined by extending the Moveable
class I've shown in the following diagram.
Moveable
encapsulates motion in the form of position, velocity and acceleration vectors. For frame-rate independent updates move
can be called with deltaTime
to scale the motion so that it looks consistent regardless of the frame rate (which can vary considerably in web browsers) 1 .
move ( deltaTime ) {
this.vel.x += this.acceleration.x * deltaTime;
this.vel.y += this.acceleration.y * deltaTime;
this.pos.x += this.vel.x * deltaTime;
this.pos.y += this.vel.y * deltaTime;
}
With this fedorated pattern, each sprite "knows" how it's supposed to move and can take care of itself on each update (as opposed to requiring "top down control" by the controller).
Fixed Interval Updates
Sometimes you need animation updates to occur on a fixed interval, or, time-step. This is especially important if you're makeing use of key-frames. Take for example this animation of our wicked witch's cape billowing in the wind. The animation makes use of key-frames to achieve the effect.
Key frame Animation
Key frame animation is the technique of creating key frames depicting object shapes, poses, paths, etc. that occur over the course of a timeline. In the analog world animators create key-frames associated with key moments in a scene (e.g., a walk-cycle) and transition frames (referred to as "'tweens") are created to provide the illusion of motion. In the digital world key-frames are still needed to define key positions but interpolation may be achieved programmatically.
SVG artists might typically think of key-frame animation defined over SVG shapes. That's one of the beauties of SVG. It offers continuous path morphing (the capability to achieve perfectly smooth vector based transitions over time) 2 . But with RAF you can also create sprite-based animation using the age-old approach of frame swapping as we see here. When might you want to do that in SVG?
-
When the animation involves complex, organic, or unpredictable shape changes that are impossible to define with a fixed point count (e.g., liquid splashes, fire, highly detailed cloth simulations).
-
When you're going for a specific, non-interpolated "choppy" or traditional animation look for your aesthetic.
-
When you already have pre-rendered assets from another source.
-
When the performance of direct path morphing becomes an issue due to extreme path complexity.
-
When you need to incorporate raster-specific effects not easily done with SVG filters.
Here are the keyframes I created for the billowing effect on Ms. Witch 3 .
And now to the heart of it. The trick to fixed-interval updates using the RAF API is to:
-
Specify your interval, and
-
Use an accumulator to trigger the changes.
In this example I do that in the witch sprite's update method
:
update( deltaTime ) {
this.accumulator += deltaTime;
while( this.accumulator >= this.fixedInterval ) {
this.updateTransform();
this.accumulator -= this.fixedInterval;
}
}
Below I've created an animated diagram to illustrate how the accumulator works.
The update function gets called in sync with browser update availability per the RAF specification. On each call the time delta between the current and previous calls is added to to the accumulator. Once the accumulator matches or exceeds the specified fixed interval the update function triggers changes to occur (e.g., sprite transformations) in lock-step with the interval.
Discussion
In this article my main intent was to focus on applying the javascript RAF API to animating SVG. Working through the examples showed how RAF provides an explicit mechanism to achieve smooth animation tied to the browser rendering-cycle. The API can be used to create anything from advertisments to production-quality video games for the Internet and World Wide Web.
Indeed, requestAnimationFrame
is the absolute core of any browser-based animation loop. In video games, for example, whether it's done with SVG, HTML5 Canvas, or even WebGL/WebGPU 3D every frame that gets rendered ultimately relies on requestAnimationFrame to trigger the update and render cycle. Even sophisticated JavaScript game frameworks and engines that abstract away many low-level details for developers (like Phaser, PixiJS, Three.js, etc.) use RAF internally as their primary game-loop mechanism. They build on top of it. So having a strong feel for how it works is essential to any non-trivial animated graphics development for the Web.
De-Coupling Timing From the Animation Loop
As I hope to have shown, the RAF API de-couples timing from the animation loop in the sense that as an artist you don't have a fixed interval imposed your animation. Back in the olden days, analog animators were tied to a fixed frame-rate (e.g., 18 FPS) -- which is why all cartoon characters walked at the same rate ;D . In other words, all your key frames and tweens had to be calculated against the the fixed interval, so for cyclic animations like walk-cycles the keys had to be tied to clicks on the beat.
But as we saw, with RAF you get a chance to render everytime the browser refreshes its view. This carries with it a number of implications:
-
You have to take steps to insure movement velocity is constant across varying frame rates, and
-
You have to insure you maintain a constant interval for frame-dependent animations.
This adds a bit of complexity to your calculations for different types of animation. And introduces an important concept: the animation timeline. Deep diving into timelines will have to await future posts. But the good news is that with the extra bit of complexity using RAF at a low level you get a lot of creative freedom. Depending on what you are trying to accomplish, you can adjust your timeline independent of the frame-rate you're working with to speed up or slow down cyclic animations. Smooooth bebe!
A Few more things worth Noting
Material Thinking
Here's a tip:
Tip
As you work to develop your artistic style using SVG -- in addition to worrying about color -- think about materials.
Elsewhere I've discused line, shape, color etc. with regard to creating SVG art. Here I want you to think about light. A huge part of the art of illustration in any medium involves understanding how light behaves. In creating the animations reflecting the nature of "real world objects" you want to think about what they're made of. By way of example, consider the rotating gear.
In addition to using a flat color to create the widget I also applied a gradient to convey a sense that the object is made of a matallic material. But be forwarned, a naive approach to creating sprites might entail applying a lot of hightlights without thinking about the end-game. The goal in creating this sprite at the end of the day was to animate it. The problem is when you start using effects like gradients and highlights you have to worry about dynamic consequences. Having the gradient rotate along with the underlying shape would look unnatural and odd. Light simply doesn't behave that way. In the real world, the light source is usually fixed. When objects move the highlights and shadows stay in place, while the object shape moves beneath them. This creates the impression of consistent lighting.
Bonus Tips: Simulating Light from a Direction
To make your SVG artworks look more natural match the gradient direction to the “light source” in your scene. Consider using radialGradient
s, filter effects like feDiffuseLighting
, and even meshGradients
s to even greater effect. And in animation, decouple the animation of the shape from the gradient and associated highlights.
In other words, what if gradients in SVG (and other effects like blur, masking, etc.) were understood and structured more like virtual materials or surface shaders? Rather than binding them manually to IDs and linking them to shapes, what if your framework could introduce material descriptors -- reusable parameterized recipes that define how light behaves across a surface. And what if you had a framework and associated tooling and AI to make your life as an artist easier. Would you be interested?
Animating Sprites
In working through the creation of the billowing cape effect on the Wicked witch I briefly described the pattern where the fixed-interval for the animation was defined and managed by the sprite herself (as opposed to being implemented on the animation controller for example). Here's why this is a great idea:
-
The Single Responsibility Principle. If a sprite is in charge of its own behavior, then:
- It knows what kind of update it needs (kinetic motion, key-frame animation, AI, etc.)
- It can decide whether to use deltaTime, a fixed-step accumulator SMIL, or any other means at is disposal to update.
-
Decoupling Modular Components. Your Animation Controller shouldn't have to know if a sprite is a particle, NPC, UI element, or a black hole for that matter. Each sprite can opt-in to the animation loop simply by providing an update to its view and thereby take charge of its own destiny.
-
Scalability and Performance. Again, RAF decouples the animation time-line from the frame rate. This allows fine grained control over scalability and performance. Some sprites might animate at 24fps, others at 12. Or 60. Some sprites might not animate at all. Some might animate only while visible (room for optimization!)
For these reasons I've adopted this pattern to create tooling and augment the framework for the SVG Creators Collaborative™. Create thin controllers which notify and hand off reponsibilties to attentive observers. In essense, the controller can say: "It's time for the next tick, here's the time-delta from the last, go update yourself!" and leave it up to the sprite to figure out what to do.
To me, that's object-oriented zen. With this architecture we have the spine for something seriously dynamic. Imagine entire performances choreographed via procedural animation and AI. With this approach you get
-
AI-driven behavioral refreshers based on simple behavioral principals (c.f., separation, alignment, cohesion)
-
Shared keyframe or gesture-sync across sprites
-
Blackboard/agent models for scalable decision-making
-
AI driven behavior blending (e.g., idle + follow + animate + signal in sync)
-
Procedural rhythm/sync engines (driven by tempo, music, emotional arc)
The possibilities are endless.
Conclusion
In conclusion I hope to have shown in this article how the RAF API in combination with SVG provides a powerful set of tools for creating animated content for HTML5 powered engines. There are countless applications for such content spanning a range of categories including advertising, data visualization, video-games, simulations, interactive story telling and education just to name the broadest. So let me leave you with this thought; while animating SVG content comes with a unique set of challenges good things are coming down the pike in the form of tools and frameworks to make life easier for artists. Stay tuned for more to come!
Endnotes
-
Please see my blog post on kinematics for more details on "moveables" .
-
It's both a blessing an a curse that SVG animation can be acheived in a number of ways. When the SVG spec was first release there was a grand vision of applying SMIL for animation which I was very excited and wrote about back then . Later, when CSS3 was released, confusion arose through lack of understanding and appreciation of the creative potential afforded by SVG + SMIL. Confusion continues to this day with reports of the deprecation and demise of SMIL being greatly exaggerated. Often questions are raised, and ill-advised "answers" are still being posted favoring the use of CSS and/or javascript over SMIL. The fact is these advisements are often advanced under the rubrik of a false dichotemy. Should I use SMIL or javascript? As if these was an either/or proposition .
The right answer is both. SMIL offer numerous benefits including:
- Portability. Since SMIL tags live inside the SVG animations can be defined in-line and dropped anywhere you can drop the SVG. Bang! Good to go!
- Independent time-lines. With independent time-lines on SMIL tags you can abtract away from RAF micromanagment (consistent, among other things, with the sprite update patterns discussed in the body of this article).
- Performance. SMIL is browser-native and automatically hardware-accelarated.
And if you are wondering whether SMIL is still supported look no further than this post. The animations I've created here contain a healthy mix of both RAF and SMIL and I can assure you they work great together.
-
Creating the billowing cloak turned out to be somewhat challenging. You can't just randomly morph points to get a good effect. For those who may be interested in the details look for my upcoming book on the intersection of SVG, artwork and AI. To forshadow, the effect benefitted from the application of some simple trigonometry .