Atlaslog
Atlaslog / Examples / Leaflet / Flyline
Leaflet · Runnable Example

Flyline

Runnable Leaflet example: Flyline. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.

Open standalone → Chinese notes All Leaflet examples
/demos/leaflet-flyline.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/leaflet-flyline.html
<!--
  示例:动画飞线
  要点:二次贝塞尔弧线、requestAnimationFrame 推进、滑动窗口拖尾、循环与错峰启动
  依赖:Leaflet 1.9.4 · jsDelivr CDN + Esri 瓦片
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>动画飞线</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<style>
  html,body{height:100%;margin:0}
  body{display:flex;flex-direction:column;overflow:hidden;
       font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif}
  .bar{flex:0 0 auto;padding:8px 12px;background:#0f172a;color:#cbd5e1;
       display:flex;gap:12px;align-items:center;font-size:12px}
  .bar .spacer{flex:1 1 auto}
  #map{flex:1 1 auto;min-height:0;background:#0b1220}
  .city{font-size:11px;color:#e2e8f0;background:rgba(15,23,42,.8);
        border:1px solid rgba(148,163,184,.35);border-radius:3px;
        padding:1px 5px;white-space:nowrap}
  .fly-dot{width:8px;height:8px;border-radius:50%;background:#fff;
        box-shadow:0 0 8px 2px currentColor}
</style>
</head>
<body>
<div class="bar">
  <span>北京 → 五个城市 · 弧线飞行</span>
  <span class="spacer"></span>
  <span>轨迹按 60 帧推进,尾迹保留 14 个采样点</span>
</div>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
  'use strict';

  var cities = [
    { name: '北京', lat: 39.9042, lng: 116.4074 },
    { name: '上海', lat: 31.2304, lng: 121.4737 },
    { name: '广州', lat: 23.1291, lng: 113.2644 },
    { name: '成都', lat: 30.5728, lng: 104.0668 },
    { name: '西安', lat: 34.3416, lng: 108.9398 },
    { name: '哈尔滨', lat: 45.8038, lng: 126.5349 }
  ];

  var colors = ['#38bdf8', '#f472b6', '#fbbf24', '#34d399', '#a78bfa'];
  var origin = cities[0];

  var map = L.map('map', { zoomControl: true });
  L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
    maxZoom: 19,
    attribution: 'Tiles © Esri'
  }).addTo(map);

  /* 深色底衬,让高亮飞线更清楚 */
  var shade = L.rectangle([[-85, -180], [85, 180]], {
    stroke: false,
    fillColor: '#0b1220',
    fillOpacity: 0.55,
    interactive: false
  }).addTo(map);

  function buildArc(a, b, bend) {
    var dx = b.lng - a.lng;
    var dy = b.lat - a.lat;
    var len = Math.sqrt(dx * dx + dy * dy) || 1;

    /* 中点的法线方向,统一取北向,弧线朝上拱起 */
    var nx = -dy / len;
    var ny = dx / len;
    if (ny < 0) { nx = -nx; ny = -ny; }

    var cx = (a.lng + b.lng) / 2 + nx * len * bend;
    var cy = (a.lat + b.lat) / 2 + ny * len * bend;

    var pts = [];
    var N = 64;
    for (var i = 0; i <= N; i++) {
      var t = i / N;
      var u = 1 - t;
      pts.push(L.latLng(
        u * u * a.lat + 2 * u * t * cy + t * t * b.lat,
        u * u * a.lng + 2 * u * t * cx + t * t * b.lng
      ));
    }
    return pts;
  }

  var arcs = [];

  cities.slice(1).forEach(function (city, idx) {
    var color = colors[idx % colors.length];
    var pts = buildArc(origin, city, 0.18);

    /* 底层弧线用虚线淡淡地铺出来 */
    L.polyline(pts, {
      color: color,
      weight: 1,
      opacity: 0.28,
      dashArray: '3 6',
      interactive: false
    }).addTo(map);

    var tail = L.polyline([], {
      color: color,
      weight: 2.5,
      opacity: 0.95,
      interactive: false
    }).addTo(map);

    var dot = L.marker(pts[0], {
      interactive: false,
      icon: L.divIcon({
        className: '',
        html: '<i class="fly-dot" style="color:' + color + '"></i>',
        iconSize: [8, 8],
        iconAnchor: [4, 4]
      })
    }).addTo(map);

    arcs.push({
      pts: pts,
      tail: tail,
      dot: dot,
      head: 0,
      wait: idx * 650,
      hold: 0
    });
  });

  cities.forEach(function (c) {
    L.marker([c.lat, c.lng], {
      interactive: false,
      icon: L.divIcon({
        className: '',
        html: '<span class="city">' + c.name + '</span>',
        iconSize: [1, 1],
        iconAnchor: [0, 18]
      })
    }).addTo(map);
  });

  map.fitBounds(L.latLngBounds(cities.map(function (c) {
    return [c.lat, c.lng];
  })), { padding: [50, 50] });

  var SPEED = 26;      /* 每秒推进的采样点数 */
  var TAIL = 14;       /* 尾迹长度 */
  var last = 0;

  function frame(ts) {
    var dt = last ? Math.min(0.05, (ts - last) / 1000) : 0.016;
    last = ts;

    arcs.forEach(function (a) {
      if (a.wait > 0) {
        a.wait -= dt * 1000;
        return;
      }
      if (a.hold > 0) {
        a.hold -= dt * 1000;
        if (a.hold <= 0) {
          a.head = 0;
          a.tail.setLatLngs([]);
          a.dot.setLatLng(a.pts[0]);
        }
        return;
      }

      a.head += SPEED * dt;
      if (a.head >= a.pts.length - 1) {
        a.head = a.pts.length - 1;
        a.hold = 900;
      }

      var end = Math.floor(a.head);
      var start = Math.max(0, end - TAIL);
      a.tail.setLatLngs(a.pts.slice(start, end + 1));
      a.dot.setLatLng(a.pts[end]);
    });

    requestAnimationFrame(frame);
  }

  requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.

More Leaflet examples

Leaflet · EXAMPLEFullscreen Leaflet · EXAMPLEGeoJSON Layer Leaflet · EXAMPLEGraticule