📌 学习目标
- 掌握添加动画图标的实现方法
- 理解相关API的使用
- 能够独立完成类似功能开发
🎯 核心概念
向地图添加动画图标。
💻 完 整 代 码
代码示例
js
const map = new maplibregl.Map({
container: "map",
style: "https://demotiles.maplibre.org/style.json",
});
const size = 200;
const pulsingDot = {
width: size,
height: size,
data: new Uint8Array(size * size * 4),
onAdd() {
const canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext("2d");
},
render() {
const duration = 1000;
const t = (performance.now() % duration) / duration;
const radius = (size / 2) * 0.3;
const outerRadius = (size / 2) * 0.7 * t + radius;
const context = this.context;
context.clearRect(0, 0, this.width, this.height);
context.beginPath();
context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2);
context.fillStyle = `rgba(255, 200, 200, ${1 - t})`;
context.fill();
context.beginPath();
context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2);
context.fillStyle = "rgba(255, 100, 100, 1)";
context.strokeStyle = "white";
context.lineWidth = 2 + 4 * (1 - t);
context.fill();
context.stroke();
this.data = context.getImageData(0, 0, this.width, this.height).data;
map.triggerRepaint();
return true;
},
};
map.on("load", () => {
map.addImage("pulsing-dot", pulsingDot, { pixelRatio: 2 });
map.addSource("points", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [{ type: "Feature", geometry: { type: "Point", coordinates: [0, 0] } }],
},
});
map.addLayer({
id: "points",
type: "symbol",
source: "points",
layout: { "icon-image": "pulsing-dot" },
});
});
代码示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>向地图添动态图标</title>
<meta property="og:description" content="向地图添加使用 Canvas API 在运行时生成的动画图标。" />
<meta property="og:created" content="2025-06-25" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css" />
<script src="https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: "map",
style: "https://demotiles.maplibre.org/style.json",
});
const size = 200;
const pulsingDot = {
width: size,
height: size,
data: new Uint8Array(size * size * 4),
onAdd() {
const canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext("2d");
},
render() {
const duration = 1000;
const t = (performance.now() % duration) / duration;
const radius = (size / 2) * 0.3;
const outerRadius = (size / 2) * 0.7 * t + radius;
const context = this.context;
context.clearRect(0, 0, this.width, this.height);
context.beginPath();
context.arc(this.width / 2, this.height / 2, outerRadius, 0, Math.PI * 2);
context.fillStyle = `rgba(255, 200, 200, ${1 - t})`;
context.fill();
context.beginPath();
context.arc(this.width / 2, this.height / 2, radius, 0, Math.PI * 2);
context.fillStyle = "rgba(255, 100, 100, 1)";
context.strokeStyle = "white";
context.lineWidth = 2 + 4 * (1 - t);
context.fill();
context.stroke();
this.data = context.getImageData(0, 0, this.width, this.height).data;
map.triggerRepaint();
return true;
},
};
map.on("load", () => {
map.addImage("pulsing-dot", pulsingDot, { pixelRatio: 2 });
map.addSource("points", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [{ type: "Feature", geometry: { type: "Point", coordinates: [0, 0] } }],
},
});
map.addLayer({
id: "points",
type: "symbol",
source: "points",
layout: { "icon-image": "pulsing-dot" },
});
});
</script>
</body>
</html>
🔍 代码解析
初始化地图
使用 new maplibregl.Map() 创建地图实例,配置基本参数。本示例的核心特色是展示如何使用 StyleImageInterface 接口创建动态动画图标。
关键配置项
- container: 地图容器的 DOM 元素 ID
- style : 使用 MapLibre 官方样式
https://demotiles.maplibre.org/style.json
StyleImageInterface 接口实现
javascript
const pulsingDot = {
width: size,
height: size,
data: new Uint8Array(size * size * 4),
onAdd() {
const canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
this.context = canvas.getContext("2d");
},
render() {
const duration = 1000;
const t = (performance.now() % duration) / duration;
// 绘制外圈脉冲效果
const radius = (size / 2) * 0.3;
const outerRadius = (size / 2) * 0.7 * t + radius;
// 更新图像数据并触发重绘
this.data = context.getImageData(0, 0, this.width, this.height).data;
map.triggerRepaint();
return true;
},
};
添加动画图标到地图
javascript
map.on("load", () => {
map.addImage("pulsing-dot", pulsingDot, { pixelRatio: 2 });
map.addSource("points", {
type: "geojson",
data: { type: "FeatureCollection", features: [...] }
});
map.addLayer({
id: "points",
type: "symbol",
source: "points",
layout: { "icon-image": "pulsing-dot" }
});
});
⚙️ 参数说明
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| container | string | 是 | - | 地图容器元素的 ID |
| style | string/object | 是 | - | 地图样式 URL 或内联样式对象 |
StyleImageInterface 属性
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| width | number | 是 | 图像宽度(像素) |
| height | number | 是 | 图像高度(像素) |
| data | Uint8Array | 是 | 像素数据,RGBA 格式 |
| onAdd | function | 是 | 图层添加时调用,初始化 Canvas |
| render | function | 是 | 每帧调用,返回 true 表示图像已更新 |
🎨 效果说明

运行代码后,地图上会在坐标 [0, 0] 处显示一个脉冲动画图标:
- 内圈: 固定大小的红色圆点,带白色描边
- 外圈: 脉冲扩散效果,从中心向外逐渐扩大并淡出
- 动画周期: 1 秒完成一次脉冲循环
- 交互功能: 支持鼠标拖拽、滚轮缩放等标准交互
动画原理:
render()方法每帧被调用- 使用
performance.now()计算动画进度 - 动态计算外圈半径和透明度
- 通过
map.triggerRepaint()触发地图重绘
💡 常 见 问 题
Q1: StyleImageInterface 是什么?
A: 这是一个接口,允许开发者创建动态生成的图像。通过实现 onAdd() 和 render() 方法,可以在运行时生成动画图标。
Q2: 为什么需要返回 true?
A: render() 方法返回 true 告诉地图图像已更新,需要重新渲染。返回 false 则跳过重绘。
Q3: 性能影响如何?
A: 每帧都会调用 render() 和 triggerRepaint(),对于复杂动画可能影响性能。建议优化渲染逻辑或降低动画帧率。
Q4: 可以创建多个动画图标吗?
A: 可以。为每个动画图标定义不同的 ID,或者使用相同的图像对象创建多个图层。
📝 练习任务
- 基础练习:修改动画周期和颜色,创建不同的脉冲效果
- 进阶挑战:实现多个不同位置的脉冲图标,每个有不同的动画周期
- 拓展思考:如何实现图标沿路径移动的动画?
🌟 最佳实践
- 性能优化 : 避免在
render()中进行复杂计算,考虑预计算或缓存 - 内存管理: 对于临时图像,使用后及时清理
- 像素比例 : 使用
pixelRatio参数适配高分辨率屏幕 - 动画控制: 提供启动/停止动画的机制
- 测试验证: 在不同设备上测试动画性能
- 降级方案: 为不支持 Canvas 的环境提供备用方案
🔗 延伸阅读
-
下一课预告:将继续学习地图图层的基础知识
本文是MapLibre GL JS实践课程系列的一部分,欢迎关注收藏