Micro animations in web applications are useful to guide users’ attention. They can even give the users extra information and make your app more compelling. Web map libraries come with a fixed set of built-in animations, like panning, zooming, and sometimes fading styles. What if we want to do more? Using requestAnimationFrame(), we can create our own, custom animations.
I did a talk on this topic at Kortdage 2019, you can find the slides on my talks page.
This post is split into three parts:
- This post. Adding animation to a map feature.
- Controlling the animation’s duration.
- Adding easing, to make the animation more pleasant.
Result
This is where we’ll end up in these posts. The idea is an app that shows the user a route on the map. In stead of just having the route appear, we can have it grow from start to stop. Not only is this more pleasing and delightful for the user, it also makes the route direction obvious.
Awesome, right? Let’s go through the process of developing this animation step-by-step. This example is using Mapbox GL, but the same technique can be used in OpenLayers or Leaflet as well.
The base app
So our app simply fetches a route for the user and shows it to the user. The examples in this post are simplified to not take moving the map into consideration for the animation and only focus on the route itself.
This is what we’re starting with. Pressing the button simply puts the route on the map. If the users blink they might not even notice something changed.
Adding motion using requestAnimationFrame
Let’s add some motion. In stead of just adding the route itself to the map, we want to store it in a variable, and add it coordinate by coordinate to the map. But how do we know how often to add coordinates?
requestAnimationFrame() is good for this. The usage is simple: Define a method to run for each frame. To start simply call requestAnimationFrame with your method as a callback. Then, in your method have a clause that says whether it should be called again, if so do it!
Say we want to animate something 10 times:
let count = 0;
function yourMethod() {
count++;
// Do stuff
if (count > 10) {
requestAnimationFrame(yourMethod);
} else {
count = 0;
}
}
requestAnimationFrame(yourMethod);
In the our base example above, we simply add our feature by calling:
map.getSource("line-animation").setData(routeFeature);
To animate it, we define an animateLine() method that:
- Counts the number of coordinates in our route LineFeature and how many times it has been called.
- In addition to our route feature, we define an animationFeature, without coordinates in it.
- Add another coordinate to the animationfeature and calls
requestAnimationFrame(animateLine)
until we’re finished.
It will look something like this:
let animationFeature = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "LineString",
coordinates: []
}
}
]
};
let progress = 0;
const animateLine = () => {
const numberOfPoints = route.features[0].geometry.coordinates.length;
if (progress < numberOfPoints) {
// append next coordinate pair to the lineString
animationFeature.features[0].geometry.coordinates.push(
route.features[0].geometry.coordinates[progress]
);
map.getSource("line-animation").setData(animationFeature);
progress++;
// Request the next frame of the animation.
requestAnimationFrame(animateLine);
} else {
progress = 0;
}
};
Nice! This works pretty well. However, we’re not quite finished. Since we’re adding a single coordinate for each frame, the duration of the animation will vary greatly depending on how long our feature is. Take a look at what happens if we want to show a longer route:
Probably not what we want! Also, since there will be more points where the line curves, the speed of the animation will slow down there, which might be undesirable. Go on to part 2 of this series to see how we can work with that.
If you enjoyed this post, or have any feedback, feel free to contact me on twitter. This post is also posted on dev.to, so you can also comment there.