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.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:动画飞线
要点:二次贝塞尔弧线、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.