Dotted Globe
A globe made of thousands of dots turns slowly, its far side showing faintly through. Arcs lift off the surface between cities, land with a ripple and drain away. Drag it to spin it faster or turn it back; it eases into its steady turn when you let go.
How to use it
- Paste the snippet just before
</head>. - Add
data-dotted-globeto an empty element and give it a height. The globe fits the smaller side. - To show your own flights, list them in
data-routesasFrom>Topairs separated by commas.
Options
Add any of these attributes to the same element.
data-routes | Arcs to show, in turn, as From>To pairs. Default: random flights between big cities on the side facing you. |
data-colors | Arc colours, separated by commas. Default #e879f9,#22d3ee,#a78bfa. |
data-dot | Colour of the land dots. Default #a78bfa. |
data-speed | How fast it turns, in degrees a second. Default 10. |
data-tilt | How far the North Pole leans towards you, in degrees. Default 18. |
data-interval | Average time between arcs. Default 900 ms. |
data-static | Turn off dragging. |
Places are city names (London, New York, Tokyo, Sydney, Dubai and about 40 more) or a latitude and longitude such as 48.86 2.35. It is drawn on a canvas, 320px tall unless you set a height, and pauses off screen. It looks best on a dark background.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Dotted Globe: a globe made of dots turns slowly on a canvas. Glowing
// arcs lift off its surface between cities and land with a pulse, and
// markers ripple where they touch down. The far side shows faintly
// through. Drag it to spin it; it eases back to its steady turn.
// Usage:
// <div data-dotted-globe style="height: 360px"></div>
// Optional: data-routes (comma-separated "From>To" pairs; places are city
// names from the list below or "lat lon", e.g. "48.86 2.35"; default
// random flights between big cities), data-colors (arc colours,
// default #e879f9,#22d3ee,#a78bfa), data-dot (land dot colour, default
// #a78bfa), data-speed (degrees a second, default 10), data-tilt (degrees
// the north pole leans towards you, default 18), data-interval (ms
// between arcs, default 900), data-static (no drag).
var DEFAULTS = { colors: '#e879f9,#22d3ee,#a78bfa', dot: '#a78bfa', speed: 10, tilt: 18, interval: 900 };
var BIG = ['London', 'New York', 'Tokyo', 'Dubai', 'Singapore', 'Sydney', 'Los Angeles', 'São Paulo', 'Paris',
'Hong Kong', 'Mumbai', 'Cape Town', 'Toronto', 'Mexico City', 'Nairobi', 'Seoul', 'Moscow', 'Lagos', 'Buenos Aires', 'Cairo'];
var RAD = Math.PI / 180;
// Simplified coastlines: polygons separated by |, as "lon lat" pairs.
var LAND = '-168 66 -162 70 -156 71.3 -141 69.6 -128 70 -115 68.8 -95 72 -82 73.5 -80 63 -94 59 -92 57 -82 55 -79 51.5 -76 56 -78 62.5 -70 61 -64 60 -61 56 -56 52 -60 47 -66 45 -70 43.5 -70 41.5 -74 40.5 -76 38 -76 35 -81 31.5 -80 27 -80.5 25.2 -82.5 27.5 -84 30 -89 30.2 -94 29.6 -97.5 27 -97.5 22 -95 19 -91 18.6 -90.5 21 -87 21.5 -88 16 -84 15.5 -83.5 11 -79.5 9 -77.5 8.5 -80 7.5 -83 8.5 -86 11.5 -92 14.5 -96 15.8 -101 17.5 -105.5 20.5 -105.5 23.5 -109 26 -112.8 31.3 -114.7 30 -112 25 -109.5 23 -110.5 24.5 -114 28 -115 30.5 -117 32.5 -120.5 34.5 -122.5 37.5 -124 40.5 -124 46 -123 49 -127.5 50.5 -131 54 -134 58 -140 59.8 -147 60.5 -152 59 -158 56.5 -164 54.6 -158 58.5 -162 60 -165 62.5 -164.5 64.5|-73 78 -60 82 -30 83.5 -18 81 -20 75 -22 70 -32 68 -40 65 -43 60 -48 61 -52 65 -54 69 -58 75.5 -68 76.5|-80 73.5 -68 70 -62 66.5 -65 63 -72 63.5 -78 67 -90 71 -85 73.8|-95 74 -80 76 -75 79 -62 82 -90 82.5 -105 79 -118 76 -110 73 -100 72.5|-118 69 -102 68.5 -101 73 -115 73.5 -125 71.8|-77.5 8.5 -72 12 -64 10.8 -60 8.5 -57 6 -52 5 -50 1.8 -48 -1 -44 -2.5 -38 -4 -35 -7 -35 -9 -39 -13.5 -39 -17.5 -40 -20.5 -42 -23 -46 -24 -48.5 -26 -48.5 -28.5 -53 -33.5 -57 -36 -57 -38.5 -62 -39 -65 -41 -65 -45 -67.5 -46.5 -66 -48 -69 -51 -68.5 -53 -71 -54 -74 -52 -75.5 -48 -73.5 -43 -73.5 -37 -71.5 -31 -70.3 -18.5 -75.5 -15 -78 -10 -81.2 -5.5 -80 -2 -80 1 -77 4 -77.5 6.5|-9.5 37 -9 43 -2 43.5 -1.5 46.5 -4.7 48.5 -1.5 48.7 1.5 50.5 4 51.5 5 53.3 8.5 53.8 8.5 57 10.5 57.7 10.5 54.5 14 54 19 54.5 21.5 57.5 24 57.3 23.5 59.3 29 60 22.5 60.5 21.5 63 25.5 65 22 65.8 17.5 62.5 19 60 16.5 56.5 13 55.5 11 58.5 8 58 5.5 58.8 5 62 10 64 14 67.5 19 70 25 71 31 70 40 67.5 41 66 34 64.6 37 64.2 44 66 44 68.5 53 68.5 59 68.5 67 69 69 72.8 73 72.5 72 66.5 78 72.5 83 70.5 87 73.8 100 76 105 77.7 113 73.7 127 73.5 131 71 140 72.5 150 71.5 160 70.6 170 70 180 69 180 65 178.5 64.5 175 62 170 60 163 59.8 163.5 56 156.7 51 155.5 57.5 162 61.7 155 59.3 143 59.4 136 54.5 141 52.5 140 48 135 43.5 131 42.6 129.5 40.5 129.5 36 126.5 34.5 126 37.7 124.5 39.8 121.5 39 122 40.5 118 39.2 119 37 122.5 37 120 34.5 121.8 31.5 122 29 118.5 24.5 113.5 22.2 109.5 21.5 108 21.5 106.5 20 105.8 18.5 108.5 15 109.3 11.5 105 8.6 104.7 10.5 100.5 13.5 99.2 10 100.5 7.2 103.5 1.3 101 2.8 98.5 8 98.3 12.5 97.5 16.5 94.5 16 94 19 92 22 90 22 86.5 20 85 19.5 80.3 15.5 80 10.3 77.5 8 76 10 73 17 72.5 21 69 22.5 67 24.5 62 25.2 57 25.7 56.3 27 54 26.8 51.5 27.9 50 30 48 30 48.5 28 51.5 24.5 55 22.5 58.8 22.5 59.8 22 57 18.8 52 16 45 12.8 43 13 42.6 15.5 39 21.5 35.5 27.5 32.6 30 34.5 31.8 35 32.5 36 35.8 32 36.2 29 36.7 26.5 39.5 29 41 28 42 28.5 43.5 30 45.5 33.5 44.5 36 45.3 39 47 38 45 41.5 41.5 36 41.5 33 42 29 41.2 26 40.8 23 40.2 24 38 22 36.5 21 38.5 19.5 41.5 19 42.5 13.5 45.5 12.3 44.5 16 41.5 18.5 40.2 16 38 15.7 40 12 42 10 44 8.5 44.4 6 43.1 3 43.3 3.3 41.9 0 39.5 -0.5 38 -2 36.7 -5.3 36 -6.5 36.9 -8.8 37|-6 35.8 -2 35.1 3 36.8 10 37.3 11 35.5 10 33.8 15 32.3 20 30.8 20 32.5 25 31.8 29 30.9 32.3 31.3 34 27.5 37 22 38.5 18 43 12.6 44 10.4 51.2 11.8 51 10.4 49 6 46 2 42 -1 40 -3 39 -6.5 40.5 -10.5 40.5 -15 37 -17.5 35.5 -21 35.5 -24 32.8 -26 32.5 -28.6 30 -31.3 27 -33.6 22 -34.1 18.5 -34.2 18 -31.5 15 -27 14.5 -22.5 11.8 -17 13.5 -12 12 -6 9 -1 9.5 3.8 8 4.4 5.5 4.2 3 6.3 -2 4.8 -7.5 4.4 -12 7 -15 10.8 -17.1 14.7 -16 19 -16.5 22 -13 27.5 -10 29.5 -9.5 32.5|44 -25 47 -25 50.5 -15.5 49.3 -12 44.5 -16 43.3 -22|-5.7 50 1.5 51.2 1.7 52.7 -0.5 54.5 -2 55.9 -1.8 57.6 -3 58.6 -5 58.6 -6.2 56.5 -5 55 -3 54 -4.6 53.3 -5 51.6|-10 51.6 -6 52 -6 54 -7.5 55.3 -10 54.2|-24 65.5 -22 66.4 -15 66.5 -13.5 65 -18 63.4 -22.5 63.8|130 31 131.5 31.5 132 33.8 135 33.5 136.8 34.5 140 35 141 38 142 40 141.4 41.4 140 40.3 139.8 38 137 37 133 35.5 130.9 34|140 41.5 141.8 42.5 145.5 43.3 142 45.5|79.8 6 81.8 7.5 80 9.8|95.3 5.6 98 4 104 -2 106 -5.8 102 -4 98.5 1|109 2 111 1 116.5 7 119 5 117.5 1 116.3 -3.5 114.5 -4 110.5 -3|105.5 -6.5 114.5 -7.7 114.4 -8.7 108 -7.8|119 -5.5 120.5 0.7 125 1.5 121 -1 123 -4.5|131 -1.3 134 -0.8 138 -1.7 145 -4.5 151 -10.5 147 -10.2 143 -9 141 -9.2 138 -8.3 137.5 -5 133 -4|120 18.5 122.3 18.4 124.3 12.5 126.5 7 125.5 5.6 122 7 123.5 10 121 13.5 120.5 16|113.5 -22 114 -26 115 -34 118 -35 123.5 -33.8 129 -31.6 131.5 -31.5 135 -34.8 138 -35.5 140 -37.8 144 -38.5 146.3 -39 150 -37.5 151.3 -34 153.5 -28.5 153 -25.5 149 -21 145.5 -15 143.5 -14 142.5 -10.7 141.5 -13 141.5 -16.5 140 -17.5 136 -15 136.8 -12.2 132.5 -11.5 130 -13.3 129.5 -15 127 -14 124 -16.3 122 -18 119 -20 116 -21|144.6 -40.7 148.3 -40.9 148 -43 146 -43.6|172.7 -34.5 178.5 -37.7 176.8 -40 174.8 -41.4 173 -39.3 174.5 -37|172.5 -40.6 174 -41.7 171 -44.3 169 -46.7 166.5 -46 168.3 -44|-85 21.8 -81 23.1 -77 21 -74.2 20.2 -77.5 19.9 -80.5 21.8|-74.4 18.5 -72.8 19.9 -68.4 19 -70 18.2';
var CITIES = {
'London': [-0.13, 51.5], 'Paris': [2.35, 48.86], 'Berlin': [13.4, 52.52], 'Madrid': [-3.7, 40.42],
'Rome': [12.5, 41.9], 'Amsterdam': [4.9, 52.37], 'Stockholm': [18.07, 59.33], 'Moscow': [37.6, 55.75],
'Istanbul': [28.98, 41.01], 'Reykjavik': [-21.9, 64.15], 'Cairo': [31.24, 30.04], 'Lagos': [3.38, 6.52],
'Nairobi': [36.82, -1.29], 'Johannesburg': [28.05, -26.2], 'Cape Town': [18.42, -33.92], 'Dubai': [55.27, 25.2],
'Mumbai': [72.88, 19.08], 'Delhi': [77.2, 28.6], 'Bangkok': [100.5, 13.75], 'Singapore': [103.82, 1.35],
'Jakarta': [106.85, -6.2], 'Hong Kong': [114.17, 22.32], 'Shanghai': [121.47, 31.23], 'Beijing': [116.4, 39.9],
'Seoul': [126.98, 37.57], 'Tokyo': [139.69, 35.69], 'Sydney': [151.21, -33.87], 'Melbourne': [144.96, -37.81],
'Auckland': [174.76, -36.85], 'Honolulu': [-157.86, 21.3], 'Vancouver': [-123.12, 49.28], 'Seattle': [-122.33, 47.6],
'San Francisco': [-122.42, 37.77], 'Los Angeles': [-118.24, 34.05], 'Mexico City': [-99.13, 19.43], 'Chicago': [-87.63, 41.88],
'Toronto': [-79.38, 43.65], 'New York': [-74.0, 40.71], 'Miami': [-80.19, 25.76], 'Bogotá': [-74.07, 4.71],
'Lima': [-77.04, -12.05], 'São Paulo': [-46.63, -23.55], 'Rio de Janeiro': [-43.17, -22.9], 'Buenos Aires': [-58.38, -34.6]
};
function plain(t) { return t.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); }
// A city name from the list above, or "lat lon" such as "48.86 2.35".
// Returns [lon, lat, name] or null.
function place(text) {
text = text.trim();
var m = /^(-?[\d.]+)[\s,]+(-?[\d.]+)$/.exec(text);
if (m) return [+m[2], +m[1], ''];
for (var k in CITIES) if (plain(k) === plain(text)) return [CITIES[k][0], CITIES[k][1], k];
return null;
}
var css =
':where(.dgl){position:relative;width:100%;height:320px}' +
'.dgl{touch-action:pan-y;cursor:grab}' +
'.dgl.dgl-drag{cursor:grabbing}' +
'.dgl-canvas{position:absolute;inset:0;width:100%;height:100%;display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
var polys;
function onLand(lon, lat) {
if (lat < -70) return true;
if (!polys) {
polys = LAND.split('|').map(function (p) {
var n = p.split(' ').map(Number), pts = [];
for (var i = 0; i < n.length; i += 2) pts.push([n[i], n[i + 1]]);
return pts;
});
}
var inside = false;
for (var q = 0; q < polys.length; q++) {
var p = polys[q];
for (var i = 0, j = p.length - 1; i < p.length; j = i++) {
if ((p[i][1] > lat) !== (p[j][1] > lat) && lon < (p[j][0] - p[i][0]) * (lat - p[i][1]) / (p[j][1] - p[i][1]) + p[i][0]) inside = !inside;
}
}
return inside;
}
function vec(lon, lat) {
var c = Math.cos(lat * RAD);
return [c * Math.sin(lon * RAD), Math.sin(lat * RAD), c * Math.cos(lon * RAD)];
}
// Dots spread evenly over the sphere (a Fibonacci lattice), land only.
var dotCache = {};
function landDots(n) {
if (dotCache[n]) return dotCache[n];
var out = [], golden = Math.PI * (3 - Math.sqrt(5));
for (var i = 0; i < n; i++) {
var y = 1 - (i + 0.5) / n * 2, r = Math.sqrt(1 - y * y), th = golden * i;
var lat = Math.asin(y) / RAD, lon = (Math.atan2(Math.sin(th) * r, Math.cos(th) * r) / RAD);
if (onLand(lon, lat)) out.push(vec(lon, lat));
}
return (dotCache[n] = out);
}
function setup(el) {
var d = el.dataset;
var s = (el._dgl = { visible: false, raf: 0, rot: 0, vel: 0, arcs: [], rings: [], n: 0, c: 0 });
el.classList.add('dgl');
s.colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
s.dot = d.dot || DEFAULTS.dot;
s.speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
s.tilt = (d.tilt !== undefined ? +d.tilt : DEFAULTS.tilt) * RAD;
s.interval = +d.interval || DEFAULTS.interval;
s.routes = [];
(d.routes || '').split(',').forEach(function (r) {
var ends = r.split('>');
if (ends.length !== 2) return;
var a = place(ends[0]), b = place(ends[1]);
if (a && b) s.routes.push([a, b]);
});
s.rot = s.routes.length ? -s.routes[0][0][0] + 20 : 10;
s.canvas = document.createElement('canvas');
s.canvas.className = 'dgl-canvas';
s.canvas.setAttribute('aria-hidden', 'true');
el.appendChild(s.canvas);
s.ctx = s.canvas.getContext('2d');
el.setAttribute('role', 'img');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'A turning globe made of dots, with arcs flying between cities');
resize(el);
new ResizeObserver(function () {
if (Math.abs(el.clientWidth - s.w) > 2 || Math.abs(el.clientHeight - s.h) > 2) { resize(el); if (!s.raf) draw(el); }
}).observe(el);
if (d.static === undefined) drag(el);
}
function resize(el) {
var s = el._dgl, dpr = Math.min(2, window.devicePixelRatio || 1);
s.w = el.clientWidth;
s.h = el.clientHeight;
s.canvas.width = Math.round(s.w * dpr);
s.canvas.height = Math.round(s.h * dpr);
s.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
s.R = Math.max(20, Math.min(s.w, s.h) * 0.43);
s.gap = Math.max(2.6, s.R / 52);
var n = Math.round(4 * Math.PI * s.R * s.R / (s.gap * s.gap) / 500) * 500;
s.dots = landDots(Math.min(24000, Math.max(3000, n)));
}
function drag(el) {
var s = el._dgl, lastX = 0, lastT = 0;
el.addEventListener('pointerdown', function (e) {
s.dragging = true;
s.touched = true;
lastX = e.clientX;
lastT = performance.now();
el.classList.add('dgl-drag');
try { el.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
});
el.addEventListener('pointermove', function (e) {
if (!s.dragging) return;
var now = performance.now(), dx = e.clientX - lastX, deg = dx / s.R * 60;
s.rot += deg;
s.vel = deg / Math.max(8, now - lastT) * 1000;
lastX = e.clientX;
lastT = now;
if (!s.raf) draw(el);
});
function up() { s.dragging = false; el.classList.remove('dgl-drag'); }
el.addEventListener('pointerup', up);
el.addEventListener('pointercancel', up);
}
// Rotate a unit vector by the globe's spin and tilt, into screen space.
function view(s, v) {
var a = s.rot * RAD, ca = Math.cos(a), sa = Math.sin(a);
var x = v[0] * ca + v[2] * sa, z = -v[0] * sa + v[2] * ca, y = v[1];
var ct = Math.cos(s.tilt), st = Math.sin(s.tilt);
return [x, y * ct - z * st, y * st + z * ct];
}
function slerp(a, b, t) {
var dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2], om = Math.acos(Math.max(-1, Math.min(1, dot)));
if (om < 1e-4) return a.slice();
var sa = Math.sin((1 - t) * om) / Math.sin(om), sb = Math.sin(t * om) / Math.sin(om);
return [a[0] * sa + b[0] * sb, a[1] * sa + b[1] * sb, a[2] * sa + b[2] * sb];
}
function nextRoute(s) {
if (s.routes.length) return s.routes[s.n++ % s.routes.length];
var pick = function () { return place(BIG[Math.floor(Math.random() * BIG.length)]); };
var a = pick(), b = pick(), tries = 0;
// Prefer flights on the side facing us.
function bad() {
var va = vec(a[0], a[1]), vb = vec(b[0], b[1]), dot = va[0] * vb[0] + va[1] * vb[1] + va[2] * vb[2];
return b[2] === a[2] || dot < 0.05 || view(s, va)[2] < 0.1;
}
while (bad() && tries++ < 40) { a = pick(); b = pick(); }
return [a, b];
}
function spawn(s, done) {
var r = nextRoute(s), a = vec(r[0][0], r[0][1]), b = vec(r[1][0], r[1][1]);
var om = Math.acos(Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2])));
var steps = Math.max(12, Math.round(om * 40)), pts = [];
for (var i = 0; i <= steps; i++) {
var t = i / steps, p = slerp(a, b, t), lift = 1 + Math.sin(Math.PI * t) * (0.03 + Math.min(om, 1.8) * 0.1);
pts.push([p[0] * lift, p[1] * lift, p[2] * lift]);
}
s.arcs.push({ pts: pts, color: s.colors[s.c++ % s.colors.length], t: done ? 1 : 0, tail: 0, dur: 1.1 + om * 0.7, from: a, to: b, landed: !!done });
if (!done) s.rings.push({ v: a, t: 0, color: s.arcs[s.arcs.length - 1].color, small: true });
}
function hidden(p) { return p[2] < 0 && p[0] * p[0] + p[1] * p[1] < 1; }
function draw(el) {
var s = el._dgl, ctx = s.ctx, R = s.R, cx = s.w / 2, cy = s.h / 2;
ctx.clearRect(0, 0, s.w, s.h);
// Atmosphere and body.
var g = ctx.createRadialGradient(cx, cy, R * 0.9, cx, cy, R * 1.22);
g.addColorStop(0, 'rgba(168,85,247,.22)');
g.addColorStop(1, 'rgba(168,85,247,0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(cx, cy, R * 1.22, 0, Math.PI * 2); ctx.fill();
var body = ctx.createRadialGradient(cx - R * 0.35, cy - R * 0.4, R * 0.1, cx, cy, R);
body.addColorStop(0, '#2b1b4f');
body.addColorStop(1, '#120a26');
ctx.fillStyle = body;
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.fill();
// Dots, bucketed by depth so each bucket is a single path.
var buckets = [[], [], [], [], [], []], size = Math.max(1, s.gap * 0.42);
for (var i = 0; i < s.dots.length; i++) {
var p = view(s, s.dots[i]);
var b = p[2] < 0 ? 0 : 1 + Math.min(4, Math.floor(p[2] * 5));
buckets[b].push(cx + p[0] * R, cy - p[1] * R);
}
ctx.fillStyle = s.dot;
buckets.forEach(function (list, bi) {
ctx.globalAlpha = bi === 0 ? 0.12 : 0.35 + bi * 0.13;
var r = bi === 0 ? size * 0.8 : size * (0.75 + bi * 0.07);
ctx.beginPath();
for (var k = 0; k < list.length; k += 2) { ctx.moveTo(list[k] + r, list[k + 1]); ctx.arc(list[k], list[k + 1], r, 0, Math.PI * 2); }
ctx.fill();
});
ctx.globalAlpha = 1;
// Arcs.
var lw = Math.max(1.4, R / 110);
ctx.lineCap = 'round';
s.arcs.forEach(function (a) {
var n = a.pts.length - 1, from = Math.floor(a.tail * n), to = Math.ceil(a.t * n);
ctx.strokeStyle = a.color;
for (var pass = 0; pass < 2; pass++) {
ctx.lineWidth = pass ? lw : lw * 3.5;
ctx.globalAlpha = pass ? 1 : 0.22;
ctx.beginPath();
var pen = false;
for (var k = from; k <= to; k++) {
var q = view(s, a.pts[k]);
if (hidden(q)) { pen = false; continue; }
var x = cx + q[0] * R, y = cy - q[1] * R;
if (pen) ctx.lineTo(x, y); else ctx.moveTo(x, y);
pen = true;
}
ctx.stroke();
}
if (a.t < 1) {
var hq = view(s, a.pts[to]);
if (!hidden(hq)) {
ctx.globalAlpha = 1;
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(cx + hq[0] * R, cy - hq[1] * R, lw * 1.6, 0, Math.PI * 2); ctx.fill();
}
}
});
// Markers and ripples.
s.rings.forEach(function (r) {
var q = view(s, r.v);
if (q[2] < 0) return;
var x = cx + q[0] * R, y = cy - q[1] * R;
ctx.globalAlpha = (1 - r.t) * 0.9;
ctx.strokeStyle = r.color;
ctx.lineWidth = lw;
ctx.beginPath(); ctx.arc(x, y, lw * 2 + r.t * R * (r.small ? 0.07 : 0.14), 0, Math.PI * 2); ctx.stroke();
ctx.globalAlpha = Math.min(1, (1 - r.t) * 2);
ctx.fillStyle = r.color;
ctx.beginPath(); ctx.arc(x, y, lw * 1.7, 0, Math.PI * 2); ctx.fill();
});
ctx.globalAlpha = 1;
// A soft highlight on the rim.
ctx.strokeStyle = 'rgba(196,181,253,.35)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.stroke();
}
function frame(el, now) {
var s = el._dgl;
if (!s.visible) { s.raf = 0; return; }
var dt = Math.min(0.05, s.last ? (now - s.last) / 1000 : 0);
s.last = now;
if (!s.dragging) {
s.vel += (s.speed - s.vel) * Math.min(1, dt * 1.6);
s.rot += s.vel * dt;
}
s.spawnIn = (s.spawnIn || 0) - dt * 1000;
if (s.spawnIn <= 0 && s.arcs.length < 7) { spawn(s); s.spawnIn = s.interval * (0.7 + Math.random() * 0.6); }
s.arcs = s.arcs.filter(function (a) {
if (a.t < 1) a.t = Math.min(1, a.t + dt / a.dur);
else {
if (!a.landed) { a.landed = true; s.rings.push({ v: a.to, t: 0, color: a.color }); }
a.hold = (a.hold || 0) + dt;
if (a.hold > 0.4) a.tail = Math.min(1, a.tail + dt / 0.8);
}
return a.tail < 1;
});
s.rings = s.rings.filter(function (r) { r.t += dt / (r.small ? 0.8 : 1.3); return r.t < 1; });
draw(el);
s.raf = requestAnimationFrame(function (t) { frame(el, t); });
}
function init() {
var still = reduced();
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._dgl;
s.visible = e.isIntersecting;
if (s.visible && !s.raf) { s.last = 0; s.raf = requestAnimationFrame(function (t) { frame(e.target, t); }); }
});
}, { threshold: 0.1 });
document.querySelectorAll('[data-dotted-globe]').forEach(function (el) {
if (el._dgl) return;
setup(el);
if (still) {
var s = el._dgl;
for (var i = 0; i < Math.max(4, s.routes.length); i++) spawn(s, true);
draw(el);
return;
}
draw(el);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-dotted-globe style="height: 400px; background: #0b0618"
data-routes="London>New York, London>Dubai, Dubai>Singapore"></div>