开源一个高性能(上万个图标丝滑缩放)的平面地图引擎,支持 react/vue

4,520 阅读3分钟

背景

前段时间写了一个高性能的 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());

比如这里实现了黑色半透明的图层。

image.png

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)