如何在SVG中绘制分段三次样条



我从我的布局引擎(ELK)收到一个点列表,我想将其转换为SVG路径。

我得到了以下内容:

  • 起始点
  • 终点
  • 必须解释为分段三次样条的控制点的弯曲点列表

当我正好收到两个弯曲点时,我可以将其转换为SVG中具有两个控制点的三次贝塞尔曲线:

<svg width="400" height="100">
<g stroke="black" fill="black">
<!--Start point-->
<circle cx="10" cy="10" r="2" />
<!--Bend points-->
<circle cx="90" cy="60" r="1" />
<circle cx="210" cy="60" r="1" />
<!--End point-->
<circle cx="290" cy="10" r="2" />  
</g>
<!--Resulting path-->
<path d="M 10 10 C 90 60, 210 60, 290 10" stroke="blue" fill="none" />
</svg>

但是当我收到超过2个控制点时,我很难理解结果路径应该是什么。例如有4个控制点:

<svg width="400" height="100">
<g stroke="black" fill="black">
<!--Start point-->
<circle cx="10" cy="10" r="2" />
<!--Bend points-->
<circle cx="50" cy="60" r="1" />
<circle cx="90" cy="60" r="1" />
<circle cx="210" cy="60" r="1" />
<circle cx="250" cy="60" r="1" />
<!--End point-->
<circle cx="290" cy="10" r="2" />  
</g>
<!--Resulting path?-->
</svg>

那么我如何转换一个"分段三次样条"与可变数量控制点到SVG路径?

根据文本,听起来您正在处理一个相当简单的"每个省略的点正好位于控制点之间",这意味着您的点应该被解释为:

on-curve: 10,10
control1: 50, 60
control2: 90, 60
on-curve: MID-POINT OF PREVIOUS AND NEXT CONTROL POINTS
control1: 210,60
control2: 250,60
on-curve: 290, 10

这意味着每个缺失的曲线点都是用(前一个控制2 +后一个控制1)/2计算的,所以在这种情况下,缺失的点是(90 + 210) /2, (60 + 60) / 2=150, 60

<svg width="400" height="100">
<g stroke="black" fill="black">
<!--Start point-->
<circle cx="10" cy="10" r="2" />
<!--control points-->
<circle cx="50" cy="60" r="1" />
<circle cx="90" cy="60" r="1" />
<!-- implicit point -->
<circle cx="150" cy="60" r="2" />
<!--control points-->
<circle cx="210" cy="60" r="1" />
<circle cx="250" cy="60" r="1" />
<!--End point-->
<circle cx="290" cy="10" r="2" />  
</g>
<path stroke="blue" fill="none" 
d="M 10 10
C 50 60, 90 60, 150 60
210 60, 250 60, 290 10"/>
</svg>

当然一般来说,在伪代码中:

# First, remove the start point from the list
start <- points.shift
# Then build the missing points, which requires running
# through the point list in reverse, so that data
# at each iteration is unaffected by previous insertions.
i <- points.length - 3
while i >= 2:
points.insert(i, (points[i-1] + points[i])/2 )
i <- i - 2
# Now we can walk through the completed point set.
moveTo(start)
for each (c1,c2,p) in points:
cubicCurveTo(c1, c2, p)

我从来没有从ELK团队那里得到一个明确的答案,尽管他们给我指出了他们在vscode扩展和Java应用程序中使用的代码。所以基于这个答案,我最终使用了这个代码(JavaScript)。我不能说这是正确的,但无论收到多少点,我都能画出像样的样条曲线:

function getBezierPathFromPoints(points) {
const [start, ...controlPoints] = points;
const path = [`M ${ptToStr(start)}`];
// if only one point, draw a straight line
if (controlPoints.length === 1) {
path.push(`L ${ptToStr(controlPoints[0])}`);
}
// if there are groups of 3 points, draw cubic bezier curves
else if (controlPoints.length % 3 === 0) {
for (let i = 0; i < controlPoints.length; i = i + 3) {
const [c1, c2, p] = controlPoints.slice(i, i + 3);
path.push(`C ${ptToStr(c1)}, ${ptToStr(c2)}, ${ptToStr(p)}`);
}
}
// if there's an even number of points, draw quadratic curves
else if (controlPoints.length % 2 === 0) {
for (let i = 0; i < controlPoints.length; i = i + 2) {
const [c, p] = controlPoints.slice(i, i + 2);
path.push(`Q ${ptToStr(c)}, ${ptToStr(p)}`);
}
}
// else, add missing points and try again
// https://stackoverflow.com/a/72577667/1010492
else {
for (let i = controlPoints.length - 3; i >= 2; i = i - 2) {
const missingPoint = midPoint(controlPoints[i - 1], controlPoints[i]);
controlPoints.splice(i, 0, missingPoint);
}
return getBezierPathFromPoints([start, ...controlPoints]);
}
return path.join(' ');
}
function midPoint(pt1, pt2) {
return {
x: (pt2.x + pt1.x) / 2,
y: (pt2.y + pt1.y) / 2,
};
}
function ptToStr({ x, y }) {
return `${x} ${y}`;
}

说明:我们设定起始点,取剩余点。然后:

  • 如果只剩下一个点,我们画一条直线
  • 如果我们有3个点的倍数,那么绘制贝塞尔曲线
  • 如果我们有偶数个点,我们画二次曲线
  • 否则,我们添加这个答案中描述的中点,然后我们再次尝试(递归)

最新更新