背景
前段时间写了一个高性能的 web 版原神地图:让 web 再次伟大——用 canvaskit 实现超级丝滑的原神地图(已开源) - 掘金 (juejin.cn),其中用到的地图引擎是针对非地理场景重新开发的一个独立的库,现已整理好并开源。
为什么不用现成的地图引擎而是重新造一个轮子?
- 现有的地图引擎几乎都不支持笛卡尔坐标系,有时候我们的场景只是平面地图(比如游戏地图,私有域地图),并不需要引入一个庞大的地理地图库。
- 为了追求极致的性能与体验(主要是手势动画),重新实现能更好地做针对性优化。
- 在写这个 web 版平面地图引擎之前,我已经写过一版 flutter 平面地图引擎,架构、部分代码可以移植。
- 不是为了造轮子而造轮子,基于 flutter 版,用好现有轮子(canvaskit、use-gesture、popmotion),重新实现一个 web 版平面地图引擎并不需要花多少时间。
- 对我而言这是一个不错的实践和学习的过程,相信也能对想要了解 canvas 绘制,手势处理的人有所启发。
特性
- 使用笛卡尔坐标系,用于平面、非地理场景
- 基于 webgl,高性能、操作丝滑
- 也可以做为一个大图浏览器
功能用法
安装
npm i @canvaskit-map/core canvaskit-wasm
快速上手
展示如何初始化地图,以及 TileLayer 的简单使用。
import { CanvaskitMap, MarkerLayer, TileLayer } from "@canvaskit-map/core";
import initCanvaskit from "canvaskit-wasm";
// 首先初始化 canvaskit,默认会从当前路径加载 canvaskit.wasm,也可以指定 wasm 地址:
// const canvaskit = await initCanvaskit({
// locateFile() {
// return 'https://cdn.staticfile.org/canvaskit-wasm/0.39.1/canvaskit.wasm';
// },
// });
const canvaskit = await initCanvaskit();
const map = new CanvaskitMap(canvaskit, {
element: "#map", // 容器
size: [17408, 17408], // 地图大小,单位是像素
origin: [3568 + 5888, 6286 + 2048], // 地图原点,地图坐标计算以原点作为偏移
maxZoom: 1, // 允许的最大缩放级别,默认 0,表示最大可放大至原始尺寸,这里取 1 表示最大缩放可以比原始尺寸大 1 级
});
map.addLayer(
new TileLayer({
minZoom: 10,
maxZoom: 13,
offset: [-5888, -2048], // 瓦片图层偏移,一般不需要设置
getTileUrl(x, y, z) {
return `https://assets.yuanshen.site/tiles_twt40/${z}/${x}_${y}.png`;
},
})
);
显示图标(MarkerLayer)
得益于 canvaskit drawAtlas 方法,同时绘制一万个图标也能丝滑流畅,但必须是图片,不能用 dom 作为图标。不过好在有 html-to-image 之类的库可以把 dom 预渲染成 image。
import { toCanvas } from "html-to-image";
const element = document.createElement("div");
element.innerHTML = `
<div class="marker">
<img src="${icon}" cross-origin="">
</div>
`;
const image = element.querySelector("img");
image.addEventListener("load", async () => {
const image = await toCanvas(element, size);
map.addLayer(new MarkerLayer({ items: [{ x, y }], image }));
});
显示图片覆盖物(ImageLayer)
ImageLayer 按 bounds 覆盖在地图上,随地图一起缩放,如果底图不大一个 ImageLayer 就可以代替 TileLayer。
const origin = [1849, 1779];
const size = [4096, 4096];
map.addLayer(
new ImageLayer({
image: "https://uploadstatic.mihoyo.com/ys-obc/2022/01/05/75379475/d258137dc0e84fc8acbf77b7dc7115da_1941568151557226408.jpeg",
bounds: [-origin[0], -origin[1], size[0] - origin[0], size[1] - origin[1]],
}),
);
自定义图层
如果内置的 Layers 无法满足需求,还可以继承 Layer
实现自定义图层,在 draw 方法里用 canvaskit 丰富的绘制接口,实现高性能绘制。
import { Layer } from "@canvaskit-map/core";
class MaskLayer extends Layer {
_paint?: Paint;
constructor() {
super({ zIndex: 0 });
}
async init() {
this._paint = new this.canvaskit!.Paint();
this._paint.setColor(this.canvaskit!.Color(0, 0, 0, 0.7));
}
draw(canvas: Canvas) {
canvas.drawRect(this.map!.rect, this._paint!);
}
}
map.addLayer(new MaskLayer());
比如这里实现了黑色半透明的图层。
React/Vue 组件
为了方便 react/vue 开发,提供了相应的组件,以下以 react 为例。
安装
npm i @canvaskit-map/react
快速上手
import React, { useState, useEffect } from "react";
import { CanvaskitMap, TileLayer } from "@canvaskit-map/react";
import initCanvaskit from "canvaskit-wasm";
function Example() {
const [canvaskit, setCanvaskit] = useState();
useEffect(() => {
initCanvaskit().then(setCanvaskit);
}, []);
if (!canvaskit) {
return null;
}
const tileOffset = [-5888, -2048];
return (
<CanvaskitMap
canvaskit={canvaskit}
className="fixed w-full h-full left-0 top-0"
size={[17408, 17408]}
origin={[3568 - tileOffset[0], 6286 - tileOffset[1]]}
maxZoom={1}
>
<TileLayer
minZoom={10}
maxZoom={13}
offset={tileOffset}
getTileUrl={(x, y, z) => {
return `https://assets.yuanshen.site/tiles_twt40/${z}/${x}_${y}.png`;
}}
/>
</CanvaskitMap>
);
}
更易用的 MarkerLayer
react MarkerLayer 在 core 的基础上做了一些优化,集成了 html-to-image,可以很自然地用 ReactNode 写图标。
<MarkerLayer items={[{ x, y }]} anchor={[0, 1]} className="p-1">
<div className="w-6 h-6 flex justify-center items-center rounded-full border border-solid border-white bg-gray-700">
<img className="w-11/12 h-11/12 object-cover" src={icon} />
</div>
</MarkerLayer>
最后
仓库地址:qiuxiang/canvaskit-map: 基于 canvaskit,高性能、丝滑的平面地图引擎。 (github.com)