MapLibre GL JS第47课:添加动画图标

📌 学习目标

  • 掌握添加动画图标的实现方法
  • 理解相关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 秒完成一次脉冲循环
  • 交互功能: 支持鼠标拖拽、滚轮缩放等标准交互

动画原理:

  1. render() 方法每帧被调用
  2. 使用 performance.now() 计算动画进度
  3. 动态计算外圈半径和透明度
  4. 通过 map.triggerRepaint() 触发地图重绘

💡 常 见 问 题

Q1: StyleImageInterface 是什么?

A: 这是一个接口,允许开发者创建动态生成的图像。通过实现 onAdd()render() 方法,可以在运行时生成动画图标。

Q2: 为什么需要返回 true?

A: render() 方法返回 true 告诉地图图像已更新,需要重新渲染。返回 false 则跳过重绘。

Q3: 性能影响如何?

A: 每帧都会调用 render()triggerRepaint(),对于复杂动画可能影响性能。建议优化渲染逻辑或降低动画帧率。

Q4: 可以创建多个动画图标吗?

A: 可以。为每个动画图标定义不同的 ID,或者使用相同的图像对象创建多个图层。

📝 练习任务

  1. 基础练习:修改动画周期和颜色,创建不同的脉冲效果
  2. 进阶挑战:实现多个不同位置的脉冲图标,每个有不同的动画周期
  3. 拓展思考:如何实现图标沿路径移动的动画?

🌟 最佳实践

  1. 性能优化 : 避免在 render() 中进行复杂计算,考虑预计算或缓存
  2. 内存管理: 对于临时图像,使用后及时清理
  3. 像素比例 : 使用 pixelRatio 参数适配高分辨率屏幕
  4. 动画控制: 提供启动/停止动画的机制
  5. 测试验证: 在不同设备上测试动画性能
  6. 降级方案: 为不支持 Canvas 的环境提供备用方案

🔗 延伸阅读


本文是MapLibre GL JS实践课程系列的一部分,欢迎关注收藏

相关推荐
快乐的哈士奇2 小时前
【Next.js实战①】Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback
开发语言·javascript·ecmascript
云水一下2 小时前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
kmblack12 小时前
javascript计算年龄
开发语言·javascript·ecmascript
Dick5072 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人
黄敬峰4 小时前
从 XMLHttpRequest 到 JSON 模拟:打通前后端通信的任督二脉
javascript
weixin_471383034 小时前
Taro-03-页面生命周期
前端·javascript·taro
Asize4 小时前
数组数据结构底层:从灵活到陷阱
前端·javascript·算法
十九画生4 小时前
Ajax 入门:用 XHR 理解前后端异步请求
前端·javascript·后端