我正在寻找一种解决方案,将由大量 auf LineTo 段组成的徒手绘制的用户绘制的 SVG 路径转换为更平滑的路径。
首选语言是 JavaScript,但欢迎任何建议。
首先,我建议使用一个好的图形库,例如raphael。它将简化实际使用 javascript 执行绘图的过程。
一种非常简单的平滑方法是将所有 lineto 命令转换为等效的 curveto 命令,并根据每条线段的角度计算一些控制点。例如,
<svg width="1000" height="1000" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="
M250 150
L150 350
L350 350
L250 150
" />
</svg>
成为
<svg width="1000" height="1000" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="
M250 150
C250 150 150 350 150 350
C150 350 350 350 350 350
C350 350 250 150 250 150
" />
</svg>
这两个都应该画一个等边三角形
下一步是计算控制点的位置。通常,您希望平滑角两侧的控制点落在穿过顶点的假想线上。对于等边三角形的顶点,这将是水平线。经过一些操作,你可以得到这样的东西:
<svg width="1000" height="1000" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="
M250 150
C230 150 140 333 150 350
C160 367 340 367 350 350
C360 333 270 150 250 150
" />
</svg>
棘手的部分是计算控制点,但这只不过是一个简单的三角问题。正如我之前提到的,这里的目标是将两个控制点放在平分角顶点的线上。例如,假设我们有两条线段:
A. (0,0) to (3,2)
B. (0,0) to (1,-4)
the absolute angle of A is arctan(2/3) = 33.69 deg
the absolute angle of B is arctan(-4/1) = -75.96 deg
the bisection angle of AB is (33.69 + -75.96)/2 = -21.135
the tangent angle is AB is (-21.135 + 90) = 68.865
知道了切线角度,我们就可以计算出控制点位置
smoothness = radius = r
tangent angle = T
Vertex X = Xv
Vertex Y = Yv
Control Point 1:
Xcp1 = cos(T)*r
Ycp1 = sin(T)*r
Control Point 2:
Xcp2 = cos(T)*(-r)
Ycp2 = sin(T)*(-r)
最后一个问题是在实际的 curveTo 命令中将每个控制点放在哪里:
CX1 Y1 X2 Y2 X3 Y3
X3 和 Y3 定义顶点位置。 X1 Y1 和 X2 Y2 定义控制点。您可以将 X1 Y1 视为定义如何进入顶点的向量,将 X2 Y2 视为定义如何离开顶点的向量。现在您已经有了必须决定的两个控制点
CXcp1 Ycp1 Xcp2 Ycp2 0 0
或
CXcp2 Ycp2 Xcp1 Ycp1 0 0
这是一个重要的决定。如果你把它们倒过来,形状就会看起来像一个循环。至此,您应该能够确定如何做出此决定...
这又是一个非常简单的解决方案,但它对于手绘路径来说往往看起来不错。更好的解决方案可能更进一步,将交点向内移动到每个线段交点的凹部分。这更具挑战性。
让我们想象用户绘图是一个元组数组,我们可以做类似的事情
const points = [[100, 50], [50, 15], [5, 60], [10, 20], [20, 10], [30, 190], [40, 10], [50, 60], [60, 120], [70, 10], [80, 50], [90, 50], [120, 10], [150, 80], [160, 10] ]
const lineProperties = (pointA, pointB) => {
const lengthX = pointB[0] - pointA[0]
const lengthY = pointB[1] - pointA[1]
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
}
}
const controlPointCalc = (current, previous, next, reverse) => {
const c = current
const p = previous ? previous : c
const n = next ? next : c
const smoothing = 0.2
const o = lineProperties(p, n)
const rev = reverse ? Math.PI : 0
const x = c[0] + Math.cos(o.angle + rev) * o.length * smoothing
const y = c[1] + Math.sin(o.angle + rev) * o.length * smoothing
return [x, y]
}
const svgPathRender = points => {
const d = points.reduce((acc, e, i, a) => {
if (i > 0) {
const cs = controlPointCalc(a[i - 1], a[i - 2], e)
const ce = controlPointCalc(e, a[i - 1], a[i + 1], true)
return `${acc} C ${cs[0]},${cs[1]} ${ce[0]},${ce[1]} ${e[0]},${e[1]}`
} else {
return `${acc} M ${e[0]},${e[1]}`
}
},'')
return `<path d="${d}" fill="none" stroke="black" />`
}
const svg = document.querySelector('.svg')
svg.innerHTML = svgPathRender(points)
<svg viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" class="svg">
</svg>
详细解释在这篇文章。
我也遇到了同样的问题,在查看 paperjs 示例时,我发现他们有一个路径简化的示例,隐藏其背后的算法,您可以在这里看到它:https://github.com/paperjs/paper。 js/blob/master/src/path/PathFitter.js
它是简化路径的算法,是一项学术研究的 js 版本(带优化),名为 “自动拟合数字化曲线的算法”。
我正在致力于提取该算法,并且可能会将其作为插件公开到 svg.js。