RackPull

Scrolling map animation with Mapbox and skrollr

This is an example of how to apply a skrollr animation to a Mapbox map. As you scroll down and up, the map pans along a red line which can represent a flight path from New York City to London.

Dependencies

  • Mapbox.js#1.6.4
  • skrollr#0.6.26
  • arc.js#0.1.0, used to calculate coordinates on great circle paths; in the example, 123 of these coordinates between New York City and London are calculated, and then connected in a Mapbox.js polyline to look like a curve
  • jQuery#1.11.1, used in the example only for convenience; jQuery is not needed for the animation itself

HTML

<div id="map1"></div>

<script src="https://api.tiles.mapbox.com/mapbox.js/v1.6.4/mapbox.js"></script>
<script src="/lib/js/skrollr.min.js"></script>
<script src="/lib/js/arc.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="scrolling-map-animation.js"></script>

JavaScript

(function ($, L, skrollr) {
  // coords of the start and the end of panning: [latitude, longitude]
  var startCoords = [40.7127, -74.0059]; // New York City
  var endCoords = [51.507222, -0.1275]; // London

  // number of coords between startCoords and endCoords that can be panned to
  var numCoords = 123;

  // where to start and stop panning (pixels from the top of the document)
  var startPanTop = $('#map1').offset().top - 200;
  var endPanTop = startPanTop + 400;

  // Mapbox zoom level
  var zoom = 5;


  // set the height of the map for the sake of the example to better see the
  // animation; note this height doesn't changed when the window gets resized
  $('#map1').height($(window).height() - 100);


  // calculate great circle coords
  var latLngs = (function (startCoords, endCoords, numCoords) {
    // convert the coord arrays to the format of arc.js: x is longitude and y is
    // latitude
    startCoords = {
      'x': startCoords[1],
      'y': startCoords[0]
    };
    endCoords = {
      'x': endCoords[1],
      'y': endCoords[0]
    };

    // use arc.js to calculate coords for the great circle path between the two
    // given coords:
    // 1. create great circle
    var generator = new arc.GreatCircle(startCoords, endCoords);
    // 2. generate a line arc
    var line = generator.Arc(numCoords);
    // 3. get coords in the line arc
    var coords = line.geometries[0].coords;

    // convert the arc.js coords to L.LatLng objects
    return coords.map(function (coord) {
      return L.latLng(coord[1], coord[0]);
    });
  }(startCoords, endCoords, numCoords));


  // create the Mapbox map
  var map = L.mapbox.map('map1', 'examples.map-i86nkdio', {
    'zoomControl': false // disable the zoom controls
  });

  // center the map on the starting coords
  map.setView(startCoords, zoom);

  // disable dragging and zooming
  map.dragging.disable();
  map.touchZoom.disable();
  map.doubleClickZoom.disable();
  map.scrollWheelZoom.disable();

  // disable the tap handler, if present
  if (map.tap) {
    map.tap.disable();
  }

  // add a polyline to the map, which will appear as the flight path
  var polyline = L.polyline([startCoords], {
    'color': '#f00'
  }).addTo(map);


  // setup skrollr
  skrollr.init({
    'render': (function () {
      // for the given scroll position, return the corresponding index of the
      // latLngs array
      var topToLatLngIndex = (function (startPanTop, endPanTop, numCoords) {
        // helper function to map values between two arrays, from
        // http://rosettacode.org/wiki/Map_range#JavaScript
        var mapRange = function(from, to, s) {
          return to[0] + (s - from[0]) * (to[1] - to[0]) / (from[1] - from[0]);
        };

        var topRange = [startPanTop, endPanTop];
        var latLngIndexRange = [0, numCoords - 1];

        return function (top) {
          return Math.floor(mapRange(topRange, latLngIndexRange, top));
        };
      }(startPanTop, endPanTop, numCoords));

      return function (data) {
        // optional index of the latLngs array; if set, pan to the corresponding
        // latLng
        var latLngIndex = null;

        if (data.curTop <= startPanTop &&
            map.getCenter().distanceTo(latLngs[0]) > 10000) {
          // when above the top scroll position, make sure the map pans to the
          // start latLng (and doesn't get stuck elsewhere)
          latLngIndex = 0;
        } else if (data.curTop > startPanTop && data.curTop < endPanTop) {
          // standard case
          latLngIndex = topToLatLngIndex(data.curTop);
        } else if (data.curTop >= endPanTop &&
            map.getCenter().distanceTo(latLngs[numCoords - 1]) > 10000) {
          // when below the bottom scroll position, make sure the map pans to
          // the end latLng
          latLngIndex = numCoords - 1;
        }

        if (latLngIndex !== null) {
          // reset the polyline to contain only the latLngs up to the current
          // index; in other words, redraw the red line
          polyline.setLatLngs(latLngs.slice(0, latLngIndex + 1));

          // pan the map to the new latLng
          map.panTo(latLngs[latLngIndex], {
            'animate': false
          });
        }
      };
    }())
  });
}(jQuery, L, skrollr));

Another way to animate

It is possible to use Mapbox’s native pan animation as well. We have to add a short delay between each pan to prevent the animations from overriding each other. See it in action below.

I found that this implementation doesn’t behave nearly as smoothly, and the map can get stuck when scrolling quickly.