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

📌 学习目标

  • 掌握添加动画图标的实现方法
  • 理解相关API的使用
  • 能够独立完成类似功能开发

🎯 核心概念

向地图添加动画图标。

💻 完 整 代 码

代码示例

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" },
    });
});

代码示例

<!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 接口实现

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;
    },
};

添加动画图标到地图

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" }
    });
});

⚙️ 参数说明

参数类型必填默认值说明
containerstring-地图容器元素的 ID
stylestring/object-地图样式 URL 或内联样式对象

StyleImageInterface 属性

属性类型必填说明
widthnumber图像宽度(像素)
heightnumber图像高度(像素)
dataUint8Array像素数据,RGBA 格式
onAddfunction图层添加时调用,初始化 Canvas
renderfunction每帧调用,返回 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实践课程系列的一部分,欢迎关注收藏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丷丩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值