0%

mapbox篇-动态闪烁点与聚类动画效果

Mapbox GL 原生支持动态图标渲染,本篇通过一个最小可运行 HTML 示例展示了以下功能:

  • 自定义动画图标:动态闪烁的脉冲点
  • 动态点聚类效果:颜色与半径随数量变化
  • 纯原生 HTML 和 JavaScript,无需框架;
  • 支持海量点渲染与动画共存;

🔍 核心功能解读

🔴 动态闪烁点图标(Pulsing Dot)

使用 map.addImage 添加一个动态生成的 Canvas 图标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const pulsingDot = {
width: size, height: size,
data: new Uint8Array(size * size * 4),
onAdd: function() {
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext('2d');
},
render: function() {
const t = (performance.now() % 1000) / 1000;
const radius = size / 2 * 0.3;
const outerRadius = size / 2 * 0.7 * t + radius;
const ctx = this.context;
ctx.clearRect(0, 0, this.width, this.height);

// 扩散波纹
ctx.beginPath();
ctx.arc(size/2, size/2, outerRadius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 200, 200, ${1 - t})`;
ctx.fill();

// 中心圆点
ctx.beginPath();
ctx.arc(size/2, size/2, radius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 100, 100, 1)';
ctx.strokeStyle = 'white';
ctx.lineWidth = 2 + 2 * (1 - t);
ctx.fill();
ctx.stroke();

this.data = ctx.getImageData(0, 0, this.width, this.height).data;
map.triggerRepaint();
return true;
}
};

这种方式可以动态渲染每一帧,并自动通知 Mapbox 重绘。

🌐 动态聚类样式(点数越多越大/越亮)
通过 GeoJSON 聚类功能自动生成 point_count,再使用 step() 表达式实现样式渐变:

1
2
3
4
5
6
7
8
9
10
paint: {
'circle-color': [
'step', ['get', 'point_count'],
'#51bbd6', 10, '#f1f075', 30, '#f28cb1'
],
'circle-radius': [
'step', ['get', 'point_count'],
15, 10, 25, 30, 35
]
}

⚙️ 性能优化建议
避免地图上同时使用太多 pulsingDot 实例(推荐 ≤ 100);

聚类图层内部自动管理聚合点数量,可放心用于 10K 以上数据;

若图层间数据动态更新频繁,推荐分图层渲染、定时更新;

🧠 适用场景
实时定位、物联网监控、车流分布

雷达波、脉冲扫描动画模拟

高并发数据展示场景(热力图替代方案)

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mapbox GL 动画 MVP</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.js"></script>
<style>
body,html { margin:0; padding:0; height:100% }
#map { position:absolute; top:0; bottom:0; width:100% }
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';
const map = new mapboxgl.Map({
container: 'map', style: 'mapbox://styles/mapbox/streets-v11',
center: [116.397, 39.907], zoom: 10
});

// 动态闪烁点:pulsing dot 图标
const size = 100;
const pulsingDot = {
width: size, height: size,
data: new Uint8Array(size * size * 4),
onAdd: function() {
const canvas = document.createElement('canvas');
canvas.width = this.width; canvas.height = this.height;
this.context = canvas.getContext('2d');
},
render: function() {
const t = (performance.now() % 1000) / 1000;
const radius = size / 2 * 0.3;
const outerRadius = size / 2 * 0.7 * t + radius;
const ctx = this.context;
ctx.clearRect(0, 0, this.width, this.height);

// 外圈波纹
ctx.beginPath();
ctx.arc(size/2, size/2, outerRadius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 200, 200, ${1 - t})`;
ctx.fill();

// 中心闪烁点
ctx.beginPath();
ctx.arc(size/2, size/2, radius, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 100, 100, 1)';
ctx.strokeStyle = 'white';
ctx.lineWidth = 2 + 2 * (1 - t);
ctx.fill();
ctx.stroke();

this.data = ctx.getImageData(0, 0, this.width, this.height).data;
// 每帧重绘图标
map.triggerRepaint();
return true;
}
};

map.on('load', () => {
map.addImage('pulsing-dot', pulsingDot, { pixelRatio: 2 });
map.addSource('dot-point', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type:'Feature',
geometry: { type:'Point', coordinates:[116.397,39.907] }
}]
}
});
map.addLayer({
id: 'layer-with-pulsing-dot',
type: 'symbol',
source: 'dot-point',
layout: { 'icon-image': 'pulsing-dot' }
});

// 群集点示例:动态聚类大小与颜色
map.addSource('random-points', {
type: 'geojson',
data: generateRandomPoints(),
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'random-points',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',['get','point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'
],
'circle-radius': [
'step',['get','point_count'], 15, 10, 25, 30, 35
]
}
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'random-points',
filter: ['has', 'point_count'],
layout: { 'text-field': ['get','point_count'], 'text-size': 12 }
});
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'random-points',
filter: ['!', ['has','point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
});

// 随机生成若干个点
function generateRandomPoints() {
const features = [];
for (let i = 0; i < 2000; i++) {
const lon = 116.2 + (Math.random() * 0.4);
const lat = 39.8 + (Math.random() * 0.4);
features.push({
type:'Feature', geometry:{type:'Point', coordinates:[lon,lat]}
});
}
return { type:'FeatureCollection', features };
}
</script>
</body>
</html>