矢量图形由路径定义几何,再由描边与填充决定外观。与位图按像素保存颜色不同,矢量路径保存的是参数化的几何描述,显示时需要把几何转换为屏幕上可绘制的线段集合。这一步涉及两类映射:几何本身的离散化,以及样式参数在不同坐标系之间的换算。
视图缩放时,路径的几何与线宽未必按同一规则变化;若两者的换算关系没有理清,就会出现放大后线条粗细与预期不符、虚线疏密反常等现象。下面从路径的表示开始,逐层说明这些映射。
路径的表示
一条路径由若干首尾相接的段构成,每段都有参数化定义:
| 段类型 | 参数 | 说明 |
|---|---|---|
| 直线段 | 起点、终点 | 一次参数化,可直接绘制 |
| 圆弧 | 圆心、半径、起止角、方向 | 由三角函数定义,可保留解析形式 |
| 二次贝塞尔 | 起点、控制点、终点 | 二次多项式 |
| 三次贝塞尔 | 起点、两个控制点、终点 | 三次多项式 |
| 椭圆弧 | 圆心、长短轴、起止角、旋转角 | 可由圆弧经非等比缩放得到 |
| 样条 | 控制点、节点向量、权值 | 有理参数曲线 |
多数实现采用分层处理:曲线统一转换为若干基础段,圆弧既可保留解析表示,也可离散为折线。保留解析表示的好处是可以在任意缩放下按需细分,避免折线在小比例下露出多边形轮廓。
曲线离散化与容差
离散化的目标是让折线在视觉上与被替代的曲线一致。常用判据是弓高,即折线弦与曲线之间的垂距上界不超过给定容差。对半径为 R 的圆弧,单段对应的圆心角 θ 与弓高的关系约为 s = R(1 − cos(θ/2)),据此可由容差反解 θ,进而得到段数:
// 按弓高容差估算圆弧需要离散成多少段
function arcSegments(radius, sweepRad, tol) {
if (tol <= 0 || radius <= 0) return 1;
// 由 s = R(1 - cos(theta/2)) 反解单段圆心角上限
const theta = 2 * Math.acos(Math.max(-1, 1 - tol / radius));
return Math.max(1, Math.ceil(Math.abs(sweepRad) / theta));
}
容差可以定义在世界坐标单位下,也可以定义在设备像素下。前者在缩放后屏幕上的偏差会随之变化;后者需要在每次视图变化后重新离散。多数实现把容差设在设备空间,以得到较稳定的视觉质量,代价是缩放时需要重建几何缓存。
线宽在世界坐标与设备坐标下的区别
线宽既可以作为世界单位下的长度,也可以作为设备像素单位的宽度,两者的缩放行为不同:
| 定义方式 | 缩放行为 | 典型用途 |
|---|---|---|
| 世界坐标单位 | 屏幕上的粗细随缩放同步变化 | 线宽本身是图纸语义的一部分 |
| 设备像素 | 屏幕上的粗细保持不变 | 界面标注、辅助线与选中高亮 |
把世界单位线宽换算为设备像素,需要乘以视图的缩放系数。若视图在两个轴上的缩放系数不同,就不存在单一的换算系数,这正是下一节讨论的畸变来源。
非等比缩放与线宽畸变
严格意义的描边是把路径沿法线方向向两侧扩展一定宽度。若视图在 X、Y 方向的缩放不一致,圆在屏幕上会变成椭圆;等宽的描边经过同样的变换后,其轮廓也不再等宽,因为法线方向被拉伸或压缩了。
多数实现在三种处理方式中选择其一:
- 取视图的一个标量缩放系数(例如两轴缩放系数中较小者或几何平均)换算线宽,畸变被舍弃,但线宽量级可控
- 先把路径变换到设备坐标,再按固定的像素宽度扩展轮廓,此时描边不随缩放变化
- 把描边轮廓当作几何对象一并参与变换,线宽随缩放变化,且畸变被完整保留
选择哪一种,取决于线宽在业务上表达的是图纸语义还是界面语义。
线型图案与相位
虚线、点划线等线型图案由「划长加间隔」的重复序列定义。渲染时需要沿路径按弧长推进,逐段判断当前位置落在划内还是间隔内。要处理的核心问题是相位,也就是图案从路径起点的哪个位置开始铺排。若每条路径都各自从零开始,折线顶点处会出现图案断裂或两段重叠。
// 沿路径按弧长推进虚线图案,返回起始段与段内偏移
function dashCursor(pattern, phase = 0) {
const total = pattern.reduce((a, b) => a + b, 0);
let pos = ((phase % total) + total) % total;
let idx = 0;
while (pos >= pattern[idx]) {
pos -= pattern[idx];
idx = (idx + 1) % pattern.length;
}
return { idx, consumed: pos };
}
对闭合路径,多数实现会让图案总长与周长对齐,即微调图案比例,使起点与终点的衔接处保持连续。图案的推进一般以世界单位的弧长计量,因此视图缩放时需要通过线型比例做补偿,否则屏幕上的划长会随缩放变化。
连接样式、端点样式与抗锯齿
当路径由多段构成时,顶点处的连接样式决定外侧如何补角:斜接延长两条边直到相交,夹角过小时通常回退为切角;圆接以圆弧填充缺口;切角直接以直线连接两侧轮廓。端点样式则决定开放路径两端的形状,常见的有平头、方头与圆头,三者在端点到轮廓的延伸量上不同。
抗锯齿在 GPU 上通常借助多重采样或覆盖率计算完成。使用覆盖率时,片段着色器输出 0 到 1 的透明度,其值与路径边缘到像素中心的距离相关。对宽度小于一个像素的细线,覆盖率近似会让线条显得偏淡,部分实现改为按对齐到屏幕像素的粗细绘制,或为线宽设定一个像素级下限。若几何本身已经离散为折线,线宽与容差的关系会一起影响观感,这部分与图层管理与坐标系变换中讨论的变换链配合生效。
小结
矢量路径的渲染可以拆成两条并行的链路:几何链路把解析曲线离散为可绘制的线段,其精度由容差控制;样式链路把线宽、线型与端点样式映射到设备空间,其正确性取决于变换在哪个空间施加。两条链路的耦合点是视图变换。当两轴缩放不一致,或需要在缩放时保持屏幕粗细不变时,就必须明确线宽与容差究竟定义在哪个坐标系中,否则同一份几何在不同缩放下的表现会彼此矛盾。