block by jeremycflin d6247a5743e265c48b44ee21e19af705

US Flights animation (Canvas2D)

Full Screen

Animating 3000 Flights using Canvas2D

Source code for animation used in #AirplaneMode

Orange dots = landing
Blue dots = takeoff

Config settings

var landingColor = '#fe761e';
var takeOffColor = '#3081dd';
var lineColor = '#5297ee';
var fps = 60;
var curveFactor = 50;
var speed = 0.99; // between 0 & 1 
var drawLines = true;
var batchSize = 12;

forked from bradoyler‘s block: US Flights animation (Canvas2D)

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.2/topojson.min.js"></script>
  <script src="routes.js"></script>
  <script src="helpers.js"></script>
  <script src="bundle.js"></script>
<style>
 body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
    
.states {
  fill: #696969;
  stroke: #6cb0e0;
  stroke-width: 0.5px;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.airports {
  fill: #000;
  stroke: transparent;
  stroke-width: 0.5px;
  stroke-linecap: round;
  stroke-linejoin: round;
}

</style>
</head>

<body>
  <div id="flightMap"></div>
  <div>Blue dots = takeoff
  <div>Orange dots = landing</div>
  <script>
    initMap('#flightMap')
  </script>
</body>

bundle.js

'use strict';

var landingColor = '#fe761e';
var takeOffColor = '#3081dd';
var lineColor = '#5297ee';
var fps = 40;
var curveFactor = 40;
var speed = 0.9; // between 0 & 1 
var drawLines = true;
var batchSize = 12;


function initMap(selector) {
  var airportMap = {};
  var $flightmap = d3.select(selector).node();
  var width = 950;
  var height = 500;
  var mapScale = 1050;
  var projection = d3.geoAlbersUsa()
  .scale(mapScale).translate([width / 2, height / 2]);

  var path = d3.geoPath().pointRadius(1.2).projection(projection);

  var svg = d3.select(selector).append('svg')
  .attr('preserveAspectRatio', 'xMidYMid')
  .attr('viewBox', '0 0 ' + width + ' ' + height)
  .attr('width', width).attr('height', height);

  var canvasEl = d3.select(selector)
  .append('canvas')
  .attr('style', 'position:absolute; top:0; left:0')
  .attr('width', width)
  .attr('height', height);

  var canvas = canvasEl.node();
  var ctx = canvas.getContext('2d');
  var points = [];
  var batchCount = 0;

  function animate() {
    draw();
    // request another frame
    setTimeout(function () {
      window.requestAnimationFrame(animate);
    }, 1000 / fps);
  }

  // draw the current frame based on sliderValue
  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < points.length; i++) {
      if (i > batchCount) {
        // for progressive animation
        batchCount += batchSize;
        break;
      }

      var point = points[i],
          start = point.start,
          end = point.end,
          progress = point.progress;

      var wayPoint = helpers.getWaypointXY(start, end, 90);
      var controlPt = { x: wayPoint.x, y: wayPoint.y - curveFactor };
      ctx.beginPath();
      var percentFactor = progress / 100;
      var xy = helpers.getQuadraticBezierXY(start, controlPt, end, percentFactor);
      point.progress += speed;

      // when to draw lines
      if (drawLines && progress < 101) {
        helpers.drawLine(ctx, start, end, controlPt, lineColor);
      }

      // when to draw dots
      if (progress > 0 && progress < 10) {
        helpers.drawDot(ctx, xy, 2.5, takeOffColor, takeOffColor);
      } else if (progress < 90) {
        helpers.drawDot(ctx, xy, 4);
      } else if (progress > 90 && progress < 101) {
        helpers.drawDot(ctx, xy, 2.5, landingColor, landingColor);
      } else {
        point.progress = 0; // reset progress
      }
    }
  }

  function ready(error, topo, airports) {
    if (error) throw error;

    svg.append('g').attr('class','states').selectAll('path')
      .data(topojson.feature(topo, topo.objects.states).features).enter()
      .append('path').attr('d', path);

    svg.append('g').attr('class', 'airports').selectAll('path')
      .data(topojson.feature(airports, airports.objects.airports).features).enter()
      .append('path').attr('id', function (d) {
      return d.id;
    }).attr('d', path);

    var geos = topojson.feature(airports, airports.objects.airports).features;
    geos.forEach(function (geo) {
      airportMap[geo.id] = geo.geometry.coordinates;
    });

    points = helpers.generatePointsFromMap(airportMap, window.routes, projection);
    animate();
  }

  d3.queue()
    .defer(d3.json, 'https://nodeassets.nbcnews.com/cdnassets/projects/2017/08/airplane-mode/us-states.json')
    .defer(d3.json, 'https://nodeassets.nbcnews.com/cdnassets/projects/2017/08/airplane-mode/us-airports-major.topo.json')
    .await(ready);
  
}

helpers.js

window.helpers = {}

helpers.drawDot = function drawDot(ctx, point, size) {
  var fill = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '#ccc';
  var stroke = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : '#ccc';

  ctx.fillStyle = fill;
  ctx.strokeStyle = stroke;
  ctx.lineWidth = 0.2;
  ctx.beginPath();
  ctx.arc(point.x, point.y, size, 0, Math.PI * 2, false);
  // var p = new window.Path2D('M10 10 h 80 v 80 h -80 Z')
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}

helpers.drawLine = function drawLine(ctx, start, end, controlPt, color) {
  var lineWidth = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0.2;

  ctx.moveTo(start.x, start.y);
  ctx.quadraticCurveTo(controlPt.x, controlPt.y, end.x, end.y); // curved lines
  // ctx.lineTo(end.x, end.y) // straight lines
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  ctx.strokeStyle = color;
  ctx.lineWidth = lineWidth;
  ctx.stroke();
}

helpers.generatePointsFromMap = function generatePointsFromMap(map, routes, projection) {
  var points = [];
  routes.forEach(function (route, idx) {
    var origin = map[route[0]];
    var dest = map[route[1]];
    if (origin && dest && origin.length && dest.length) {
      var startXY = projection(origin) || [0, 0];
      var endXY = projection(dest) || [0, 0];
      var start = { x: startXY[0], y: startXY[1] };
      var end = { x: endXY[0], y: endXY[1] };
      var name = route;
      var onMap = end.x > 0 && start.x > 0;
      if (onMap) {
        points.push({ idx: idx, name: name, start: start, end: end, progress: 0 });
      }
    }
  });
  console.log('points:', points.length)
  return points;
}

helpers.getWaypointXY = function getWaypointXY(startPoint, endPoint, percent) {
  var factor = percent / 100;
  var x = startPoint.x + (endPoint.x - startPoint.x) * factor;
  var y = startPoint.y + (endPoint.y - startPoint.y) * factor;
  return { x: x, y: y };
}

// quadratic bezier: percent is 0-1
helpers.getQuadraticBezierXY = function getQuadraticBezierXY(startPt, controlPt, endPt, percent) {
  var x = Math.pow(1 - percent, 2) * startPt.x + 2 * (1 - percent) * percent * controlPt.x + Math.pow(percent, 2) * endPt.x;
  var y = Math.pow(1 - percent, 2) * startPt.y + 2 * (1 - percent) * percent * controlPt.y + Math.pow(percent, 2) * endPt.y;
  return { x: x, y: y };
}