Mapjar - WebGL2 地图引擎
基于 WebGL2 的高性能地图渲染引擎,支持 EPSG:3857 投影。
✨ 功能特性
核心渲染
- ✅ WebGL2 渲染:高性能 GPU 加速渲染
- ✅ EPSG:3857 投影:Web Mercator 标准投影
- ✅ 高 DPR 支持:完美适配 Retina 和高分辨率屏幕,自动 DPI 缩放
- ✅ 跨世界渲染:水平无限循环,无缝拼接
相机系统
- ✅ 平移、缩放、旋转:流畅的交互体验
- ✅ 平滑动画:flyTo、fitBounds 带缓动效果
- ✅ 纬度限制:自动锁定在 ±85.05° 范围内
- ✅ 右键旋转:支持地图旋转(可选)
图层系统
- ✅ 瓦片图层 (TileLayer):支持任意瓦片源,Web Worker 并发加载,LRU 缓存,智能取消机制
- ✅ 矢量图层 (VectorLayer):点、线、面要素渲染,圆形点,Earcut 多边形三角化
- ✅ GeoJSON 图层 (GeoJSONLayer):完整支持 GeoJSON 格式数据加载和渲染
- ✅ 图像图层 (ImageLayer):渲染单张带地理坐标的图像,支持历史地图叠加、卫星影像等
- ✅ 风场图层 (WindLayer):基于 WebGL2 的高性能风场可视化,粒子系统实时动画
- ✅ 热力图层 (HeatmapLayer):温度、降水等连续数值场可视化,支持自定义颜色映射
- ✅ 覆盖层图层 (OverlayLayer):在地图上叠加 HTML 元素,支持自定义样式和交互
- ✅ Canvas 图层 (CanvasLayer):纯 Canvas 2D 渲染,适合自定义绘制
交互与事件
- ✅ 鼠标/触摸交互:拖拽、滚轮、双击放大(带平滑动画)
- ✅ 点击事件:监听地图点击,获取经纬度坐标
- ✅ 鼠标移动事件:实时获取鼠标位置的经纬度(可选启用)
- ✅ 事件系统:统一的事件发射器,类型安全的事件订阅/发布
高级功能
- ✅ 文字渲染:为矢量要素添加文字标注,支持自定义字体、颜色、描边、偏移等
- ✅ 数据驱动样式:根据要素属性动态设置样式
- ✅ 空间查询:点查询、范围查询、最近邻查询
- ✅ 视锥剔除:自动剔除视口外的要素,提升性能
- ✅ 批量渲染:减少 GPU 状态切换,提升渲染效率
- ✅ 资源管理:自动管理 WebGL 资源,防止内存泄漏
🚀 快速开始
安装依赖
bun add mapjar
npm install mapjar
📖 使用指南
基础示例
import { MapEngine, TileLayer, VectorLayer } from 'mapjar';
const engine = new MapEngine('#map', {
center: [116.4074, 39.9042],
zoom: 10,
rotation: 0,
enableRotation: true
});
const tileLayer = new TileLayer(
'osm',
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{
tileScale: 1.0,
wrapX: true,
fadeInDuration: 200
}
);
engine.addLayer(tileLayer);
const vectorLayer = new VectorLayer('vector', {
fillColor: [0.2, 0.6, 1.0, 0.4],
strokeColor: [0.0, 0.4, 0.8, 1.0],
strokeWidth: 2.0,
pointSize: 10.0
});
engine.addLayer(vectorLayer);
vectorLayer.addFeature({
type: 'point',
coordinates: [116.4074, 39.9042],
properties: { name: '北京' }
});
engine.on('click', (event) => {
console.log('点击位置:', event.lon, event.lat);
});
setTimeout(() => {
engine.flyTo(121.4737, 31.2304, 10, { duration: 2000 });
}, 2000);
核心概念
1. 投影系统
使用 EPSG:3857 (Web Mercator) 投影:
import { WebMercatorProjection } from 'mapjar';
const pos = WebMercatorProjection.lonLatToMeters(116.4074, 39.9042);
const lonLat = WebMercatorProjection.metersToLonLat(pos.x, pos.y);
const tile = WebMercatorProjection.getTileCoord(116.4074, 39.9042, 10);
2. 相机控制
const camera = engine.getCamera();
camera.setCenterLonLat(116.4074, 39.9042);
camera.setZoom(10);
camera.pan(100, 100);
camera.zoomTo(1, screenPos);
engine.flyTo(116.4074, 39.9042, 10, { duration: 1500 });
engine.fitBounds(
{
minLon: 73.5,
minLat: 18.2,
maxLon: 135.0,
maxLat: 53.5
},
{ duration: 2000, padding: 50 }
);
3. 瓦片图层 (TileLayer)
const tileLayer = new TileLayer(
'osm',
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{
tileScale: 1.0,
wrapX: true,
fadeInDuration: 200
}
);
engine.addLayer(tileLayer);
tileLayer.setTileScale(1.5);
4. 图像图层 (ImageLayer)
import { ImageLayer } from 'mapjar';
const imageLayer = new ImageLayer('custom-overlay', {
url: 'https://example.com/historical-map.png',
bounds: {
minLon: 116.2,
minLat: 39.8,
maxLon: 116.6,
maxLat: 40.1,
},
useMipmap: true,
});
imageLayer.setOpacity(0.7);
imageLayer.setZIndex(10);
engine.addLayer(imageLayer);
await imageLayer.loadFromURL();
const imageLayer2 = new ImageLayer('overlay2', {
image: imageBitmap,
bounds: { minLon: 116.2, minLat: 39.8, maxLon: 116.6, maxLat: 40.1 }
});
5. 矢量图层 (VectorLayer)
const vectorLayer = new VectorLayer('vector', {
fillColor: [0.2, 0.6, 1.0, 0.4],
strokeColor: [0.0, 0.4, 0.8, 1.0],
strokeWidth: 2.0,
pointSize: 10.0,
textField: 'name',
textFont: '14px Arial',
textColor: [0, 0, 0, 1],
textHaloColor: [1, 1, 1, 1],
textHaloWidth: 2,
textOffset: [0, -15],
textAnchor: 'center'
});
vectorLayer.addFeature({
type: 'point',
coordinates: [116.4074, 39.9042],
properties: { name: '北京' }
});
vectorLayer.addFeature({
type: 'line',
coordinates: [[116.4074, 39.9042], [121.4737, 31.2304]],
properties: { name: '路线' }
});
vectorLayer.addFeature({
type: 'polygon',
coordinates: [[[116, 39], [117, 39], [117, 40], [116, 40], [116, 39]]],
properties: { name: '区域' }
});
vectorLayer.addFeature({
type: 'polygon',
coordinates: [
[[116, 39], [117, 39], [117, 40], [116, 40], [116, 39]],
[[116.3, 39.3], [116.7, 39.3], [116.7, 39.7], [116.3, 39.7], [116.3, 39.3]]
],
properties: { name: '带洞区域' }
});
const nearbyFeatures = vectorLayer.queryNearby([116.4, 39.9], 10000);
const featuresInBounds = vectorLayer.queryBBox({ minX: 116, minY: 39, maxX: 117, maxY: 40 });
6. GeoJSON 图层 (GeoJSONLayer)
import { MapEngine, GeoJSONLayer, StyleFunction } from 'mapjar';
const engine = new MapEngine('#map', {
center: [105, 35],
zoom: 4,
});
const geoJSONLayer = new GeoJSONLayer('geojson', {
url: 'https://example.com/data.geojson',
style: {
fillColor: [0.2, 0.6, 1.0, 0.4],
strokeColor: [0.0, 0.4, 0.8, 1.0],
strokeWidth: 2.0,
pointSize: 10.0,
textField: 'name',
textFont: '14px Arial'
}
});
engine.addLayer(geoJSONLayer);
await geoJSONLayer.loadFromURL();
const geoJSONLayer2 = new GeoJSONLayer('geojson2', {
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [116.4074, 39.9042]
},
properties: { name: '北京', population: 21540000 }
}
]
}
});
geoJSONLayer.setDataDrivenStyle({
fillColor: StyleFunction.createPropertyColorMap(
'type',
{
'residential': [0.8, 0.8, 0.6, 0.5],
'commercial': [1.0, 0.6, 0.6, 0.5],
'park': [0.4, 0.8, 0.4, 0.5],
},
[0.5, 0.5, 0.5, 0.5]
)
});
7. 风场动画图层 (WindLayer)
import { MapEngine, WindLayer } from 'mapjar';
const engine = new MapEngine('#map', {
center: [105, 35],
zoom: 4,
});
const windLayer = new WindLayer('wind', {
particleCount: 5000,
particleAge: 100,
speedFactor: 0.5,
lineWidth: 1.0,
fadeOpacity: 0.97,
wrapX: true,
colorRamp: [
'#3288bd',
'#66c2a5',
'#abdda4',
'#e6f598',
'#fee08b',
'#fdae61',
'#f46d43',
'#d53e4f',
],
});
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
const uv = new Float32Array(canvas.width * canvas.height * 2);
const alpha = new Float32Array(canvas.width * canvas.height);
for (let i = 0; i < canvas.width * canvas.height; i++) {
const r = pixels[i * 4] / 255;
const g = pixels[i * 4 + 1] / 255;
const a = pixels[i * 4 + 3] / 255;
alpha[i] = a;
if (a === 0) {
uv[i * 2] = 0;
uv[i * 2 + 1] = 0;
} else {
uv[i * 2] = minU + r * (maxU - minU);
uv[i * 2 + 1] = minV + g * (maxV - minV);
}
}
windLayer.setData({
uv,
alpha,
width: canvas.width,
height: canvas.height,
minU: -12.35,
maxU: 22.81,
minV: -22.71,
maxV: 14.65,
bounds: {
minLon: 55,
minLat: 1,
maxLon: 155,
maxLat: 57,
},
});
};
img.src = 'wind-data.png';
const windData = {
uv: new Float32Array([...]),
width: 100,
height: 50,
minU: -10,
maxU: 10,
minV: -10,
maxV: 10,
alpha: new Float32Array([...]),
bounds: {
minLon: 73.5,
minLat: 18.0,
maxLon: 135.0,
maxLat: 53.5,
},
};
windLayer.setData(windData);
engine.addLayer(windLayer);
8. 热力图层 (HeatmapLayer)
import { MapEngine, HeatmapLayer } from 'mapjar';
const engine = new MapEngine('#map', {
center: [105, 35],
zoom: 4,
});
const heatmapLayer = new HeatmapLayer('temperature', {
colorRamp: [
{ value: 0.0, color: '#313695' },
{ value: 0.5, color: '#ffffbf' },
{ value: 1.0, color: '#a50026' },
],
wrapX: true,
});
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = async () => {
const bitmap = await createImageBitmap(img);
heatmapLayer.setData({
image: bitmap,
bounds: {
minLon: 55,
minLat: 1,
maxLon: 155,
maxLat: 57,
},
});
engine.addLayer(heatmapLayer);
};
img.src = 'temperature.png';
heatmapLayer.setData({
values: new Float32Array([...]),
width: 100,
height: 50,
min: -10,
max: 40,
alpha: new Float32Array([...]),
bounds: {
minLon: 55,
minLat: 1,
maxLon: 155,
maxLat: 57,
},
});
heatmapLayer.setColorRamp([
{ value: 0.0, color: '#0000FF' },
{ value: 0.5, color: '#00FF00' },
{ value: 1.0, color: '#FF0000' },
]);
颜色映射格式:
colorRamp: ['#0000FF', '#00FF00', '#FF0000']
colorRamp: [
{ value: 0.0, color: '#0000FF' },
{ value: 0.3, color: '#00FF00' },
{ value: 1.0, color: '#FF0000' },
]
应用场景:
- 温度场可视化
- 降水量分布
- 气压场显示
- 污染物浓度
- 海拔高度图
- 任何连续数值场
9. 覆盖层图层 (OverlayLayer)
import { OverlayLayer } from 'mapjar';
const overlayLayer = new OverlayLayer('overlay');
engine.addLayer(overlayLayer);
const element = document.createElement('div');
element.innerHTML = `
<div style="
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
font-family: Arial;
">
<h3 style="margin: 0 0 5px 0;">北京</h3>
<p style="margin: 0; color: #666;">中国首都</p>
</div>
`;
overlayLayer.setOverlay({
element: element,
position: {
lon: 116.4074,
lat: 39.9042,
offset: [0, -20],
anchor: [0.5, 1.0],
},
properties: { name: '北京' },
});
overlayLayer.updateOverlay({
position: {
lon: 116.5,
lat: 40.0,
}
});
const newElement = document.createElement('div');
newElement.textContent = '新内容';
overlayLayer.updateOverlay({
element: newElement
});
overlayLayer.updateOverlay({
element: newElement,
position: { lon: 116.5, lat: 40.0 },
visible: false
});
overlayLayer.clearOverlay();
位置配置:
interface OverlayPosition {
lon: number;
lat: number;
offset?: [number, number];
anchor?: [number, number];
}
更新参数:
overlayLayer.updateOverlay({
element?: HTMLElement,
position?: OverlayPosition,
visible?: boolean,
properties?: Record<string, unknown>
});
应用场景:
- 地图标注(Marker)
- 信息弹窗(Popup)
- 自定义控件
- 复杂的交互式标签
- 富文本内容展示
- 图片、视频等媒体内容
- 动态更新的内容展示
10. Canvas 图层 (CanvasLayer)
import { CanvasLayer } from 'mapjar';
class MyCanvasLayer extends CanvasLayer {
constructor(id: string) {
super(id, 512, 512);
}
render(gl: WebGL2RenderingContext, viewMatrix: Float32Array): void {
if (!this.visible) return;
const ctx = this.getContext();
this.clear();
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(100, 100, 200, 200);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.strokeRect(150, 150, 100, 100);
}
}
const canvasLayer = new MyCanvasLayer('custom');
engine.addLayer(canvasLayer);
11. 数据驱动样式
根据要素属性动态设置样式:
import { VectorLayer, StyleFunction } from 'mapjar';
const layer = new VectorLayer('vector');
layer.setDataDrivenStyle({
fillColor: StyleFunction.createPropertyColorMap(
'type',
{
'residential': [0.8, 0.8, 0.6, 0.5],
'commercial': [1.0, 0.6, 0.6, 0.5],
'park': [0.4, 0.8, 0.4, 0.5],
},
[0.5, 0.5, 0.5, 0.5]
)
});
layer.setDataDrivenStyle({
fillColor: StyleFunction.createNumericColorScale(
'population',
[
[0, [0.2, 0.4, 1.0, 0.5]],
[5000, [0.4, 0.8, 0.8, 0.5]],
[10000, [0.8, 0.8, 0.4, 0.5]],
[20000, [1.0, 0.4, 0.2, 0.5]],
],
[0.5, 0.5, 0.5, 0.5]
)
});
layer.setDataDrivenStyle({
fillColor: (properties) => {
const value = properties.temperature as number;
if (value < 0) return [0.2, 0.4, 1.0, 0.6];
if (value < 20) return [0.4, 0.8, 0.4, 0.6];
if (value < 30) return [1.0, 0.8, 0.2, 0.6];
return [1.0, 0.2, 0.2, 0.6];
},
pointSize: (properties) => {
const importance = properties.importance as number || 1;
return importance * 2;
}
});
样式函数工具:
createPropertyColorMap - 基于分类属性的颜色映射
createNumericColorScale - 基于数值范围的颜色插值
createNumericSizeScale - 基于数值范围的大小插值
createConditionalStyle - 基于条件的样式选择
应用场景:
- 人口密度热力图
- 交通流量可视化
- POI 分类显示
- 建筑高度可视化
12. 图层管理
const layer = engine.getLayer('osm');
layer?.setVisible(false);
layer?.setOpacity(0.5);
layer?.setZIndex(10);
engine.removeLayer('osm');
13. 事件系统
engine.on('click', (event) => {
console.log('点击:', event.lon, event.lat);
});
engine.on('mousemove', (event) => {
console.log('鼠标:', event.lon, event.lat);
});
engine.once('click', (event) => {
console.log('只触发一次');
});
const removeListener = engine.on('click', handler);
removeListener();
engine.off('click', handler);
engine.removeAllListeners('click');
engine.removeAllListeners();
type MapEventMap = {
click: MapClickEvent;
mousemove: MapMouseMoveEvent;
};
交互操作
鼠标操作
- 拖拽平移:按住鼠标左键拖动
- 滚轮缩放:鼠标滚轮上下滚动(每次 1 级,带动画)
- 双击放大:双击鼠标左键(Shift + 双击缩小,带动画)
触摸操作
- 单指拖拽:单指按住拖动
- 双指轻触:快速连续轻触两次放大(带动画)
动画效果
- 平滑过渡:所有缩放操作都带有流畅的动画效果
- 缓动函数:使用 easeOutQuad 缓动,柔和自然
- flyTo 动画:平滑飞行到目标位置和缩放级别
- fitBounds 动画:自动适配边界并平滑过渡
- 可自定义:支持自定义动画时长和缓动函数
🎯 性能优化
Web Worker 瓦片加载
| 首屏加载 | 2.5s | 1.8s | 28% ↓ |
| 主线程阻塞 | 150ms | 20ms | 87% ↓ |
| 帧率下降 | 15 fps | 3 fps | 80% ↓ |
瓦片清晰度优化
| 纹理上传次数 | 每帧 | 仅一次 | 99% ↓ |
| 渲染时间 | 8ms | 3ms | 62% ↓ |
| 视觉质量 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 显著提升 |
优化建议
- 瓦片图层:使用 CDN 加速瓦片加载,启用 Worker 并发加载
- 瓦片缩放:根据需求调整
tileScale,平衡清晰度和性能
- 跨世界渲染:全球地图启用
wrapX: true,区域地图禁用
- 矢量图层:避免添加过多要素(建议 < 10000)
- 渲染循环:不需要时调用
engine.stop() 停止渲染
- 图层层级:合理设置 zIndex,减少重绘
🌐 浏览器兼容性
WebGL2 支持
- ✅ Chrome 56+
- ✅ Firefox 51+
- ✅ Safari 15+
- ✅ Edge 79+
覆盖 97%+ 的浏览器
📚 API 参考
MapEngine
flyTo(lon, lat, zoom?, options?)
平滑飞行到指定位置和缩放级别。
参数:
lon (number): 目标经度
lat (number): 目标纬度
zoom (number, 可选): 目标缩放级别,默认保持当前缩放
options (object, 可选):
duration (number): 动画时长(毫秒),默认自动计算
maxDuration (number): 最大动画时长(毫秒),默认 3000
示例:
engine.flyTo(116.4074, 39.9042, 10);
engine.flyTo(121.4737, 31.2304, 12, { duration: 2000 });
engine.flyTo(113.2644, 23.1291);
fitBounds(bounds, options?)
自动适配到指定边界,并平滑过渡。
参数:
bounds (object): 边界对象
minLon (number): 最小经度
minLat (number): 最小纬度
maxLon (number): 最大经度
maxLat (number): 最大纬度
options (object, 可选):
duration (number): 动画时长(毫秒),默认自动计算
maxDuration (number): 最大动画时长(毫秒),默认 3000
padding (number): 边界填充(像素),默认 50
示例:
engine.fitBounds({
minLon: 73.5,
minLat: 18.2,
maxLon: 135.0,
maxLat: 53.5
});
engine.fitBounds(
{
minLon: 110,
minLat: 30,
maxLon: 120,
maxLat: 40
},
{ padding: 100, duration: 1500 }
);
Camera
flyTo(lon, lat, zoom?, options?)
相机级别的 flyTo 方法,与 MapEngine.flyTo 相同。
fitBounds(bounds, options?)
相机级别的 fitBounds 方法,与 MapEngine.fitBounds 相同。
📁 项目结构
mapjar/
├── lib/ # 库源代码
│ ├── animation/ # 动画系统
│ │ ├── Easing.ts # 缓动函数
│ │ ├── FlyToAnimation.ts # 飞行动画
│ │ └── ZoomAnimation.ts # 缩放动画
│ ├── core/ # 核心模块
│ │ ├── Camera.ts # 相机系统
│ │ └── WebGL2Renderer.ts # WebGL2 渲染器
│ ├── layer/ # 图层系统
│ │ ├── Layer.ts # 图层基类
│ │ ├── RasterLayer.ts # 栅格图层基类
│ │ ├── TileLayer.ts # 瓦片图层
│ │ ├── ImageLayer.ts # 图像图层
│ │ ├── VectorLayer.ts # 矢量图层
│ │ ├── GeoJSONLayer.ts # GeoJSON 图层
│ │ ├── WindLayer.ts # 风场动画图层
│ │ ├── HeatmapLayer.ts # 热力图层
│ │ ├── OverlayLayer.ts # 覆盖层图层
│ │ └── CanvasLayer.ts # Canvas 图层
│ ├── math/ # 数学工具
│ │ ├── Vec2.ts # 二维向量
│ │ └── Projection.ts # 投影系统
│ ├── spatial/ # 空间查询
│ │ └── SpatialQuery.ts # 空间查询工具
│ ├── style/ # 样式系统
│ │ └── StyleFunction.ts # 数据驱动样式
│ ├── utils/ # 工具类
│ │ ├── BatchRenderer.ts # 批量渲染器
│ │ ├── EventEmitter.ts # 事件发射器
│ │ ├── FrustumCulling.ts # 视锥剔除
│ │ ├── Loader.ts # 资源加载器
│ │ ├── ResourceManager.ts # 资源管理器
│ │ ├── TextRenderer.ts # 文字渲染器
│ │ └── WebGLUtils.ts # WebGL 工具函数
│ ├── workers/ # Web Workers
│ │ ├── TileLoader.worker.ts # 瓦片加载 Worker
│ │ └── TileLoader.worker.factory.ts # Worker 工厂
│ ├── MapEngine.ts # 地图引擎主类
│ └── main.ts # 导出入口
├── examples/ # 示例应用
│ ├── views/ # 示例页面
│ │ ├── Home.vue # 首页
│ │ ├── TileLayerExample.vue # 瓦片图层示例
│ │ ├── VectorLayerExample.vue # 矢量图层示例
│ │ ├── GeoJSONLayerExample.vue # GeoJSON 图层示例
│ │ ├── ImageLayerExample.vue # 图像图层示例
│ │ ├── WindLayerExample.vue # 风场图层示例
│ │ ├── WindLayerExample2.vue # 风场图层示例 2
│ │ ├── HeatmapLayerExample.vue # 热力图层示例
│ │ ├── OverlayLayerExample.vue # 覆盖层图层示例
│ │ └── CombinedExample.vue # 综合示例
│ ├── router/ # 路由配置
│ ├── components/ # 公共组件
│ ├── App.vue # 应用根组件
│ └── main.ts # 应用入口
└── index.d.ts # TypeScript 类型声明
❓ 常见问题
Q: 瓦片加载失败?
A: 检查瓦片 URL 是否正确,是否需要 API Key,是否有跨域问题(CORS)。
Q: 矢量要素不显示?
A: 确保坐标在可见范围内,检查图层的 zIndex 是否被其他图层遮挡,检查样式的透明度是否为 0。
Q: 性能问题?
A: 减少矢量要素数量(建议 < 10000),使用瓦片图层代替大量矢量数据,调整 tileScale 平衡清晰度和性能,启用视锥剔除。
Q: 瓦片显示模糊?
A: 瓦片已自动启用 Mipmap 和各向异性过滤。如果文字太小,可以增加 tileScale 参数(1.0 - 3.0)。
Q: 地图边界有缝隙?
A: 确保瓦片服务器支持跨越 180° 经线的瓦片,或禁用 wrapX 选项。
Q: 如何添加自定义瓦片源?
A: 只需提供符合 {z}/{x}/{y} 格式的 URL 模板即可。
Q: 风场/热力图数据如何准备?
A: 推荐使用图片格式:风场数据用 R/G 通道存储 U/V 分量,A 通道存储有效性;热力图直接使用灰度图或彩色图。
Q: 移动端显示太小?
A: 引擎已自动适配高 DPI 屏幕(通过 Camera.getResolution() 的 DPI 缩放),无需额外配置。
Q: 如何自定义图层?
A: 继承 Layer、RasterLayer 或 CanvasLayer 基类,实现 render() 方法即可。
🗺️ 开发路线图
Phase 1: 基础渲染 ✅
Phase 2: 地图基础 ✅
Phase 3: 高级功能 ✅
Phase 4: 科学可视化 ✅
Phase 5: 标注和覆盖层 ✅
🛠️ 技术栈
- TypeScript
- WebGL2
- Web Workers
- ImageBitmap API
- earcut - 多边形三角化
- Vue 3
- Vite
📄 License
MIT
🤝 贡献
欢迎提交 Issue 和 Pull Request!