矢量渲染系统
目的和范围
本文档解释 Leaflet 中的矢量渲染系统,该系统负责将矢量图形(折线、多边形、圆形等)绘制到屏幕上。系统提供两种渲染策略:基于 Canvas 的像素操作渲染和基于 DOM 元素的 SVG 渲染。关于矢量形状类本身(Polyline、Polygon、Circle)的信息,请参阅 矢量图形。
渲染系统建立在三个核心组件之上:
- Renderer 基类:管理渲染器生命周期和坐标转换
- Canvas 渲染器:基于像素的渲染,具有手动命中检测
- SVG 渲染器:基于 DOM 的渲染,使用原生浏览器事件
架构概览
Renderer 基类
Renderer 类扩展 BlanketOverlay,为 Canvas 和 SVG 实现提供基础。它管理路径图层的集合并协调它们的渲染生命周期。
核心职责
| 职责 | 实现 |
|---|---|
| 图层跟踪 | 维护 _layers 对象,将标记的图层 ID 映射到图层实例 |
| 坐标更新 | 通过 _onZoomEnd() 在缩放结束时重新计算图层坐标 |
| 渲染更新 | 在 'update' 事件上通过 _updatePaths() 触发图层更新 |
| 视图重置 | 通过在所有图层上调用 _reset() 处理视图重置 |
生命周期集成
渲染器监听地图事件并将其转换为坐标更新。当地图移动或缩放时,渲染器确保所有路径通过 _project() 重新计算其像素位置,并通过 _update() 重新绘制。
Canvas 渲染器
Canvas 渲染器使用 HTML5 Canvas 2D 上下文进行基于像素的绘制。与 SVG 不同,Canvas 不为每个形状创建单独的 DOM 元素,需要自定义实现命中检测和 Z 序管理。
容器初始化
Canvas 渲染器创建 <canvas> 元素并附加指针事件监听器:
Canvas 通过将内部 canvas 大小缩放 window.devicePixelRatio 同时保持 CSS 显示大小来处理 Retina 显示屏:
绘制顺序管理
Canvas 使用双向链表结构维护绘制顺序。每个图层接收一个包含对前一个和后一个图层引用的 _order 对象:
这种结构实现了 bringToFront() 和 bringToBack() 的高效重新排序操作:
| 操作 | 实现 | 复杂度 |
|---|---|---|
_initPath() | 追加到列表末尾 | O(1) |
_bringToFront() | 移动到 _drawLast 位置 | O(1) |
_bringToBack() | 移动到 _drawFirst 位置 | O(1) |
_removePath() | 从列表中取消链接 | O(1) |
渲染管道
Canvas 渲染器使用基于请求的重新绘制系统,具有脏矩形优化:
_redrawBounds 优化仅清除和重绘 canvas 中发生变化的部分,减少局部更新的渲染成本:
命中检测
Canvas 需要手动命中检测,因为绘制的像素没有单独的标识。渲染器按绘制顺序迭代图层并测试点包含:
_containsPoint() 方法由每个 Path 子类实现,并包括描边宽度的容差:
渲染器将指针悬停检测限制为 32ms,以避免在快速鼠标移动期间性能下降:
绘制基元
Canvas 渲染器提供两个核心绘制方法:
| 方法 | 用途 | 关键操作 |
|---|---|---|
_updatePoly(layer, closed) | 绘制折线/多边形 | ctx.beginPath(), ctx.moveTo(), ctx.lineTo(), 可选 ctx.closePath() |
_updateCircle(layer) | 绘制圆形 | ctx.arc(), 通过 ctx.scale() 处理椭圆缩放 |
两个方法都委托给 _fillStroke() 来应用样式:
SVG 渲染器
SVG 渲染器为每个矢量图层创建单独的 SVG <path> 元素,利用浏览器的原生事件处理和渲染。
容器结构
SVG 容器使用 viewBox 来定位路径而不修改它们的坐标:
Path 元素创建
当图层初始化时,SVG 渲染器创建具有适当类和交互性的 <path> 元素:
样式应用
SVG 渲染器通过直接在 path 元素上设置 SVG 属性来应用样式:
| 样式类别 | SVG 属性 |
|---|---|
| 描边 | stroke, stroke-opacity, stroke-width, stroke-linecap, stroke-linejoin |
| 虚线模式 | stroke-dasharray, stroke-dashoffset |
| 填充 | fill, fill-opacity, fill-rule |
路径数据生成
SVG 渲染器将坐标转换为 SVG 路径数据字符串:
对于圆形,渲染器使用 SVG 弧线命令绘制两个半圆:
Z-Index 管理
由于 SVG 不支持 z-index,SVG 渲染器通过重新排序 DOM 元素来管理图层顺序:
| 操作 | 实现 |
|---|---|
_bringToFront() | 使用 DomUtil.toFront() 将 path 元素移动到父元素末尾 |
_bringToBack() | 使用 DomUtil.toBack() 将 path 元素移动到父元素开头 |
事件处理
与 Canvas 不同,SVG 路径接收原生浏览器事件。SVG 渲染器使用 leaflet-interactive 类标记交互式路径,并调用 addInteractiveTarget() 与 Leaflet 的事件系统集成:
Path-Renderer 集成
Path 类通过定义良好的接口与渲染器协调:
渲染器选择
地图可以指定默认渲染器,或者单个路径可以使用显式渲染器:
// 地图级别默认渲染器
const map = new LeafletMap('map', {
renderer: new Canvas()
});
// 路径特定渲染器
const myRenderer = new SVG({ padding: 0.5 });
const line = new Polyline(coords, { renderer: myRenderer });渲染器比较
| 方面 | Canvas | SVG |
|---|---|---|
| DOM 元素 | 单个 <canvas> 元素 | 每个图层一个 <path> |
| 事件处理 | 通过 _containsPoint() 手动命中检测 | 原生浏览器事件 |
| 绘制顺序 | 双向链表 | DOM 元素顺序 |
| Z-Index 更改 | O(1) 列表操作 + 重绘 | O(1) DOM 重新排序 |
| 渲染 | 即时模式(清除 + 重绘) | 保留模式(浏览器管理) |
| 内存 | 固定 canvas 大小 | 随图层数量扩展 |
| 命中检测 | O(n) 遍历图层 | O(1) 浏览器原生 |
| 样式更新 | 需要重绘 | 仅属性更改 |
| 部分更新 | 脏矩形优化 | 逐元素失效 |
性能考虑
Canvas 优化
重绘边界:仅清除和重绘受影响的矩形
请求动画帧:将多个更新请求批处理到单个帧中
悬停节流:将指针移动处理限制为 32ms 间隔
设备像素比:以设备分辨率渲染以支持 Retina 显示屏
SVG 优化
ViewBox 更新:一起移动所有路径而不重新计算坐标
属性更新:更改样式而不完全重新渲染
原生事件:利用浏览器内置的事件处理
通用优化
两个渲染器都:
- 使用 padding 扩展容器边界以减少边缘裁剪
- 在缩放动画期间延迟更新
- 在缩放结束时批量更新坐标
坐标转换
渲染器的坐标系统使用相对于地图像素原点的像素坐标。当地图移动时,渲染器更新其变换而不是重新计算所有图层坐标:
Canvas 变换:
// 将 canvas 上下文平移以与地图边界对齐
this._ctx.setTransform(
s, 0, 0, s,
-b.min.x * s,
-b.min.y * s
);SVG ViewBox:
// 更新 viewBox 以一起移动所有路径
this._container.setAttribute('viewBox',
[b.min.x, b.min.y, size.x, size.y].join(' ')
);