Using GSAP to Animate Game UI with Canvas

Avatar of Opher Vishnia
Opher Vishnia on

The year was 1995; Toy Story hit the theaters, kids were obsessively collecting little cardboard circles and Kiss From a Rose was being badly sung by everyone. I was a gangly ten-year-old, and like any other relatively tall kid I was often addressed to by “you must be so good at basketball!”. So I practiced and practiced spending hours on the court of my elementary school. Eventually, I realized, much to the dismay of aunts and other cheek-pinchers alike, that while occupying vertical real estate might give you an advantage in the art of basketball, it does not ensure it.

Fast forward 21 years later. Now a tall and gangly developer, still bad at basketball, I was faced with a project: Designing and implementing a full motion video web basketball game for the NBA’s Detroit Pistons. Throwing balls around is one thing; throwing pixels around — now that’s finally a basketball challenge I can ace!

While developing the game I used many neat things like canvas, SVG and CSS animations, gesture recognition and a video stream that’s dynamically constructed on the fly. It’s really amazing what we can do with just a browser these days. Go ahead, give it a spin.

In this article, I want to focus and show you how I implemented the animation for the Superpower Gauge using vanilla JS in conjunction with GSAP. This is the motion reference I used while implementing the animation, created in After Effects:

In 1on1, once the user succeeds making a move, they’re awarded combo points. The gauge sits at the top left corner of the screen, and its task is to convey to the user the amount of their combo points as denoted by the number of red segments. At certain times in the game, the Superpower Gauge becomes active, notifying the user they can click it to make their in-game avatar perform a special move.

The basic structure of the Superpower is achieved with one Canvas element and a bit of simple geometry:

See the Pen Pistons Superpower: Structure by Opher Vishnia (@OpherV) on CodePen.

Essentially, there are two main components here — the central image and the gauge segments. The image is the easy part, it’s just a trivial use of canvas’ drawImage. The gauge segments is where things get interesting. I defined a general options defined with some properties to play with later on like the number of segments, radius, width and so on. Then I iterate over an array of segment objects and use their properties (strokeStyle, lineWidth) to draw the actual segments with the canvas arc function. So far so good — but where’s the animation?

I was debating whether to use a canvas animation framework but ultimately decided against it. This is because I needed to use several types of animations in the project: Canvas, SVG and CSS/DOM, and no one framework does it all. In addition, all of the animations had to run smoothly on top of playing video, on both desktop and mobile with varying capabilities and network conditions. This means that performance was nothing if not paramount, and I wanted to know exactly which code powers the animation. Luckily GSAP (aka Greensock AKA TweenMax AKA TweenLite) allows me to do just that.

GSAP is cool. It enables you to animate pretty much anything! The trick is that the animation API accepts not only DOM/SVG objects but also arbitrary JS data structures, whose properties you can then “animate”.

Animate all the thing! Meme

The basic idea is that you use GSAP to change the properties of these objects over time. These values specify how the UI looks at any given point in time. On each requestAnimationFrame you make a draw call to the canvas to draw the state of the UI based on those objects.

function render() { 
 //draw the animation state
 drawComboGui();
 
 //draw the image
 ctx.drawImage(...);
 //render on the next frame as well
 window.requestAnimationFrame(render)
}
render();

Here’s a breakdown of the different animations implemented:

Gauge fills up

See the Pen Pistons Superpower: Gauge fill by Opher Vishnia (@OpherV) on CodePen.

Let’s discuss the anatomy of this animation. At idle state, all yet-to-be filled segments of the gauge are gray and thin, and filled segments are red and slightly thicker. Once the next segment of the gauge fills up, all previous active segments change color to white, grow in thickness and start glowing. The following segment is then filled, and lastly, all the segments stop glowing and return to their original, active width.

Remember that array of segment objects? Here’s where they come into play with GSAP. The function addActiveSegment is the heart of the magic, where we use TweenMax.fromTo to animate properties like lineWidth and anglePercent. The GSAP colorProps plugin allows us to make smooth transition in color properties like strokeStyle and activeStrokeStyle. I’m using the delay property to time the various components of this animation.

TweenMax.fromTo(segments[index], expandAnimLength, {
  anglePercent: 0,
  colorProps:{strokeStyle: options.activeStrokeStyle},
}, 
{
  anglePercent: 1,
  colorProps:{strokeStyle: options.activeStrokeStyle},
  ease: Power0.easeIn,
  delay: growAnimLength
});

Like I mentioned earlier, the render function then calls drawComboGui on each requestAnimationFrame, ideally 60 times a second. In drawComboGui first we clear the canvas from any previous data drawn onto it before drawing the current state.

To create the glow effect, I drew two segments on top of each other. The bottom one uses shadowBlur on the canvas path, and the top one has no shadow blur. This makes the blurred element “peek” behind the non-blurred one, resulting in the glow effect.

There are several extra animations for the Superpower gauge. The Superpower enabled and Superpower disabled are very simple in concept to the gauge fill up discussed here. They are implemented by animating the width of the image and the active segments.

superpower charged
Superpower charged
Superpower discharged
Superpower discharged

The Superpower charged and Superpower discharged animation require other techniques like animating image sprites and applying a blur-on-the-fly filter. It’s a bit out-of-scope right now, but it will be discussed in a future article!

There’s one major gotcha when it comes to implementing UI for games. Games are extremely stateful. In any given moment the state of the game, and by proxy, its UI can change. In the Superpower Gauge’s particular case  —  it means that at every moment the gauge might fill up, become enabled, be discharged or become disabled. This can happen even while in the middle of an animation! What do you do when that happens, though?

You have two options — one is to stop whatever animation is currently playing and abruptly transition to the new animation. The problem with this approach is that the experience for the user is very jarring, detaching them from the game and ultimately conveying more noise than information. This is the exact opposite of what a good interface should do.

The other option is to queue up the animations, so each animation is fired before the last one starts. This gets a little tricky, since an animation might be comprised of smaller sub-animations, but thanks to GSAP’s Timeline feature, the task of herding all these states and animations becomes much more manageable. Instead of calling TweenMax.to, you initialize a Timeline instance object and use it to make to calls. By default, these to calls define new animations to start at the end of the timeline forming an animation queue, but this is highly configurable! You could, for example, define an animation to start at an offset relative to the timeline end, or at an absolute position on the timeline. This also allows you avoid having to use the delay property to calculate queuing of animations, which tends to get cumbersome when dealing with multiple animations.

Here’s an implementation of the gauge fill up animation using GSAP’s Timeline. Try to click the “Add Segment” button while an animation is already in progress.

See the Pen Pistons Superpower: Gauge fill with Timeline by Opher Vishnia (@OpherV) on CodePen.

I hope this helps you tackle some challenges and issues you encounter in your game/site/project. If you have any questions or if you’d like to know how I tackled other UI elements in the game feel free to hit me up on Twitter!

Epilogue

While I still get assaulted once in awhile by low hanging branches and my hoop-shooting skills leave much to be desired,  when it comes to quickly pressing key combos, Andre Drummond has nothing on me.