loading...
Cover image for Creating custom webmap animations

Creating custom webmap animations

haakseth profile image John Wika Haakseth Originally published at haakseth.com on ・3 min read

Micro animations in web applications are useful to guide users’ attention. They can even give the users extra information! 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.

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.

Discussion

pic
Editor guide
Collapse
jdnichollsc profile image
Juan David Nicholls Cardona

Interesting by using mapboxgl