AntV-G-Path 的实现和拾取
简介
path 是 G适应性最广的图形,其绘制和拾取都需要特别处理,本章介绍Path 的绘制和拾取,同时介绍一些优化方案。
SVG Path 简介
G 中支持的 Path 符合 SVG 标准,支持的命令有:
- M = move to
- L = line to
- H = horizontal line to
- V = vertical line to
- C = curve to
- S = smooth curve to
- Q = quadratic Belzier curve
- T = smooth quadratic Belzier curve to
- A = elliptical Arc
- Z = closepath
其中一些命令可以相互转换:
- H、V 可以转换成 L
- S 可以转换成 C
- T 可以转换成 Q
这些命令都有小写形式,表示相对路径:m,l,h,v,c,s,q,t,a,z
更多相关的资料可以参考:
- https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths
- https://www.w3.org/TR/SVG/paths.html
G 中的实现
G 中的 Path 图形支持 SVG 的所有的命令,大小写都支持,但是在内部对所有的命令都进行了处理:
- 将所有小写相对的路径都改成绝对路径
- 仅保留 M 、L、C、Q、A 和 Z 这 6 种命令,其他命令做响应的转换
Path 的命令和Canvas 的对应方法
大多数命令都有直接的对应方法:
- M 对应 moveTo
- L 对应 lineTo
- C 对应 bezierCurveTo
- Q 对应 quadraticCurveTo
- Z 对应 closePath
但是 A 命令并没有对应直接的 Canvas 方法, A 命令本质上是绘制可旋转的椭圆,我们有几种方案可以绘制 A:
- 使用贝塞尔曲线模拟实现,但是不是很精准
- 有些浏览器支持 ellipse 接口
ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
- 这个接口存在兼容性问题
- 需要将终止的 起始点和终点转换成 startAngle 和 endAngle
- 使用 arc 绘制圆弧的接口进行模拟
const r = rx > ry ? rx : ry;
const scaleX = rx > ry ? 1 : rx / ry;
const scaleY = rx > ry ? ry / rx : 1;
context.translate(cx, cy);
context.rotate(xRotation);
context.scale(scaleX, scaleY);
context.arc(0, 0, r, startAngle, endAngle, 1 - sweepFlag);
context.scale(1 / scaleX, 1 / scaleY);
context.rotate(-xRotation);
context.translate(-cx, -cy);
- 平移、旋转、压缩 绘制后再恢复
绘制实现
所以最终来绘制 path 的代码如下:
function drawPath(context, path, arcParamsCache) {
let currentPoint = [0, 0]; // 当前图形
let startMovePoint = [0, 0]; // 开始 M 的点,可能会有多个
context.beginPath();
for (let i = 0; i < path.length; i++) {
const params = path[i];
const command = params[0];
// V,H,S,T 都在前面被转换成标准形式
switch (command) {
case 'M':
context.moveTo(params[1], params[2]);
startMovePoint = [params[1], params[2]];
break;
case 'L':
context.lineTo(params[1], params[2]);
break;
case 'Q':
context.quadraticCurveTo(params[1], params[2], params[3], params[4]);
break;
case 'C':
context.bezierCurveTo(params[1], params[2], params[3], params[4], params[5], params[6]);
break;
case 'A': {
const arcParams = EllipseMath.getArcParams(currentPoint, params);
const { cx, cy, rx, ry, startAngle, endAngle, xRotation, sweepFlag } = arcParams;
// 直接使用椭圆的 api
if (context.ellipse) {
context.ellipse(cx, cy, rx, ry, xRotation, startAngle, endAngle, 1 - sweepFlag);
} else {
const r = rx > ry ? rx : ry;
const scaleX