You'd think I'd be tired of
animating points by now, but it hasn't gotten old for me yet. I recently had the opportunity at
work to build some map-based animations for a video we were putting together and the first thing that came to mind was to use the excellent
deck.gl library from the
Uber data vis team.
I wanted to have a glitzy reveal of all 17,000+ public libraries in the US and ended up with the following:
This post will cover how to make something like that yourself.
If you haven't used deck.gl before, check out
the examples in their documentation or view
the code for this demo for details on how to get started.
The first approach we'll try for bringing animation to our map vis is to leverage the built-in support for
transitions
. Consider a basic
ScatterplotLayer
:
const librariesLayer = new ScatterplotLayer({
id: 'points-layer',
data: librariesData,
getPosition: d => d.position,
getFillColor: [60, 220, 255],
getRadius: 5000,
});
This will render our libraries as a bunch of points colored rgb(60, 220, 255), as shown below.
Now if we want to animate the colors of the points, we can modify our layer props to include transitions
with getFillColor
specified:
let showLibraries = false;
const librariesLayer = new ScatterplotLayer({
id: 'points-layer',
data: librariesData,
getPosition: d => d.position,
getFillColor: showLibraries ? [60, 220, 255] : [255, 255, 255],
getRadius: 5000,
transitions: {
getFillColor: 3000,
},
});
When we set showLibraries = true
and update deck.gl's layers, we'll get the following animation:
We're using the transitions shorthand here, which says when the getFillColor
value changes we should transition to the new value over 3000ms.
Note: If you're using a function to specify getFillColor
, you may also need to provide a value for updateTriggers
so deck.gl knows it needs to recompute the colors of each point:
const librariesLayer = new ScatterplotLayer({
...
updateTriggers: {
getFillColor: [showLibraries]
}
});
If we want the points to grow into place in addition to the color tween, we can animate the radius by specifying getRadius
in the transitions
object.
const librariesLayer = new ScatterplotLayer({
id: 'points-layer',
data: librariesData,
getPosition: d => d.position,
getFillColor: showLibraries ? [60, 220, 255] : [255, 255, 255],
getRadius: showLibraries ? 5000 : 0,
transitions: {
getFillColor: 3000,
getRadius: {
duration: 3000,
easing: d3.easeBackInOut,
},
},
});
This time we used the expanded transitions definition which allows us to set an
easing for the animation to follow. The effect is shown below:
That was pretty easy! Thanks deck.gl, you're the best. The effect is more noticeable with a bigger radius or higher resolution, but you get the idea.
That was cool and all, however often times things look even cooler if we can add a little delay to each point instead of having them all animate in at once. To solve that, we have to do something a bit more complex and use a custom layer.
I've created a layer I call the
DelayPointLayer
that allows you to render in points with delay specified by a function, similar to how
d3 transitions work. I won't cover the internals of how it works here, but please feel free to view
the source code and tune it to your needs.
The DelayPointLayer
requires specifying three new properties on your point layer:
- animationProgress How far through the animation you currently are (a value between 0 and 1). At 0, the animation has not started. At 1, all points are done animating.
- pointDuration How long a single point takes to animate as a proportion of the animation progress (a value between 0 and 1). A value of 0.25 means any given point will take 25% of the animation duration to completely animate (works best with linear easing on
animationProgress
). The final point begins animating when animationProgress
is at 1 - pointDuration
.
- getDelayFactor A function mapping from a data point to a value between 0 and 1 representing when this point should begin animating. A value of 0 means it will begin animating immediately and 1 means it will be the last point to animate.
Let's look at our library example:
const librariesAnimation = { enterProgress: 0 };
const longitudeDelayScale = d3.scaleLinear()
.domain(d3.extent(librariesData, d => d.position[0]))
.range([1, 0]);
const librariesLayer = new DelayedPointLayer({
id: 'points-layer',
data: librariesData,
getPosition: d => d.position,
getFillColor: [60, 220, 255],
getRadius: 50,
radiusMinPixels: 3,
animationProgress: librariesAnimation.enterProgress,
pointDuration: 0.25,
getDelayFactor: d => {
longitudeDelayScale(d.position[0])
},
});
Now as we update
enterProgress
to new values between 0 and 1 and tell deck.gl to update its layers, we will see our points animate in. A simple way to get
enterProgress
to animate with regular updates is to use
anime.js:
const animation = anime({
duration: 3000,
targets: librariesAnimation,
enterProgress: 1,
update() {
updateDeckLayers();
},
});
There's plenty of other modifications you can do with the DelayPointLayer
and I tend to just edit the shader for whatever I am currently looking to do on an ad-hoc basis. I encourage you to do the same! Or even better, make it more robust and release it as a reusable component for everyone to enjoy :)
Now to really make our points pop, we can dip into the wonderful world of blending. Additive blending is when overlapping colors have their RGB values added together to determine the color that is shown on screen. This results in brighter regions when points are close together and can sometimes feel like a nice glow effect (although typically glows use blurring in addition to this).
Luckily, it doesn't take much to turn on additive blending in deck.gl, we just provide some parameters to our layer:
import GL from '@luma.gl/constants';
const librariesLayer = new DelayedPointLayer({
...
parameters: {
[GL.DEPTH_TEST]: false,
[GL.BLEND]: true,
[GL.BLEND_SRC_RGB]: GL.ONE,
[GL.BLEND_DST_RGB]: GL.ONE,
[GL.BLEND_EQUATION]: GL.FUNC_ADD,
},
});
Boom, roasted.
After you get your fix animating points, maybe you'll move on to animating the
ArcLayer
.
There's a great starting point here. I ended up using something similar to produce the following video:
Thanks for reading! Hopefully you now have an idea of how to begin animating with
deck.gl. To finish up the videos I made, I saved my animations using
CCapture.js and added some glow to them in After Effects. If you make something after reading this, I'd love to see it— find me as
@pbesh on Twitter.
Have a great day!