英文:
Smoothly close an SVG path with JavaScript
问题
Sure, here's the translated part:
我正在处理一些代码,其中用户放下一个数字或点/标记,然后通过它们绘制贝塞尔曲线的SVG线条(因此平滑的线连接所有点)。它基于 https://codepen.io/francoisromain/pen/dzoZZj
svg.innerHTML = svgPath(points, bezierCommand)
我创建的一个示例路径如下:
<svg width="100px" height="100px" viewBox="100 200 500 400">
<path d="M 346,186.5 C 374,208.875 452,237.875 458,276 C 464,314.125 433.75,340.5 370,339 C 306.25,337.5 253,307.75 203,270 C 153,232.25 134.75,209 170,188 C 205.25,167 300.5,186.5 344,186">
</svg>
到目前为止,一切都运行良好,直到我要关闭线条。如果我添加 Z,它会突然连接这些点。
如果需要关闭线条,关闭应该是一个曲线,以便最后一个点到第一个点以及第二个点之间有一个平滑的过渡...也就是整个路径成为一个“平滑”的环。
我尝试添加贝塞尔曲线,但连接点最终成为一个尖锐的凸起。我还尝试在第一个点附近添加另一个点,然后使用 Z,但同样出现尖锐凸起。
应该如何做到这一点呢?
英文:
I'm working on some code where a user drops a number or points/markers and a Bezier curved SVG line is drawn through them (so a smooth line connects all the points). It's based off
https://codepen.io/francoisromain/pen/dzoZZj
svg.innerHTML = svgPath(points, bezierCommand)
An example path that I've created is:
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-html -->
<svg width="100px" height="100px" viewBox="100 200 500 400">
<path d="M 346,186.5 C 374,208.875 452,237.875 458,276 C 464,314.125 433.75,340.5 370,339 C 306.25,337.5 253,307.75 203,270 C 153,232.25 134.75,209 170,188 C 205.25,167 300.5,186.5 344,186">
</svg>
<!-- end snippet -->
So far it's working well till I get to closing the line. If I add Z it abruptly joins the points.
The closing needs to be (if necessary) a curve so that there's a gentle transition from the last point to the first and on to the second... i.e. the whole path becomes a "smooth" loop.
I tried adding a Bezier curve but the join ends up being a sharp bump. And I tried adding another point close to the first and then using Z but again I get a sharp bump.
How can this be done?
答案1
得分: 1
我修改了原始函数,以返回一个 pathData 数组(根据 SVG 2 SVGPathData 接口草案),而不是一个 d
属性字符串。
这样,我们可以轻松地操作和排序命令。
我还改变了坐标结构,使我们使用 点对象。
[
{ x: 344, y: 186 }
]
而不是
[
[ 344, 186 ]
]
这非常方便,因为大多数原生方法,如 getPointAtLength()
或属性,如 points
,也返回坐标对象。
通过附加前2个点来扩展折线
points = [
{ x: 344, y: 186 },
{ x: 458, y: 276 },
{ x: 370, y: 339 },
{ x: 203, y: 270 },
{ x: 170, y: 188 },
// 从开头复制的点
{ x: 344, y: 186 },
{ x: 458, y: 276 }
];
如果是闭合路径,附加前2个点:
if (closed) {
points = points.concat(points.slice(0, 2));
}
这样函数将在第一个/最后一个和第二个顶点之间创建一个平滑曲线。
显然,这个路径有一个重叠的曲线段,我们需要删除它。此外,第一个 C
需要一些调整。
复制最后的 C
命令的第一个控制点并删除最后的命令
我们可以复制最后一个命令的第一个控制点到第一个 C
命令。
// 复制最后一个命令的第一个控制点到第一个 curveto
if (closed) {
let comLast = pathData[pathData.length - 1];
let valuesLastC = comLast.values;
let valuesFirstC = pathData[1].values;
pathData[1] = {
type: "C",
values: [valuesLastC[0], valuesLastC[1], valuesFirstC.slice(2)].flat()
};
// 删除最后一个 curveto
pathData = pathData.slice(0, pathData.length - 1);
}
英文:
I modified the original function to return a pathData (according to the SVG 2 SVGPathData interface draft) the array instead of a d
attribute string.
This way, we can easily manipulate and sort commands.
I've also changed the coordinate structure so we're working with point objects.
[
{ x: 344, y: 186 }
]
instead of
[
[ 344, 186 ]
]
This pretty handy, since most native methods like getPointAtLength()
or properties like points
also return coordinates as objects
Extend the polyline by appending the first 2 point to the point array
points = [
{ x: 344, y: 186 },
{ x: 458, y: 276 },
{ x: 370, y: 339 },
{ x: 203, y: 270 },
{ x: 170, y: 188 },
//duplicated points from the beginning
{ x: 344, y: 186 },
{ x: 458, y: 276 },
];
// append first 2 points for closed paths
if (closed) {
points = points.concat(points.slice(0, 2));
}
This way the function will create a smooth curve between first/last and second vertice.
Obviously, this path has an overlapping curve segment, we need to remove. Besides, the first C
and needs some adjustment.
Copy last C
commands 1. control point and delete last command
We can copy the last command's 1st control point to the first C
command.
// copy last commands 1st controlpoint to first curveto
if (closed) {
let comLast = pathData[pathData.length - 1];
let valuesLastC = comLast.values;
let valuesFirstC = pathData[1].values;
pathData[1] = {
type: "C",
values: [valuesLastC[0], valuesLastC[1], valuesFirstC.slice(2)].flat()
};
// delete last curveto
pathData = pathData.slice(0, pathData.length - 1);
}
<!-- begin snippet: js hide: false console: false babel: false -->
<!-- language: lang-js -->
let points = [
{ x: 344, y: 186 },
{ x: 458, y: 276 },
{ x: 370, y: 339 },
{ x: 203, y: 270 },
{ x: 170, y: 188 }
];
let smoothing = 0.3;
let pathData = getCurvePathData(points, smoothing, true);
// serialize pathData to d attribute string
let d = pathDataToD(pathData, 1);
path.setAttribute("d", d);
// Render the svg <path> element
function getCurvePathData(points, smoothing = 0.2, closed=true){
// append first 2 points for closed paths
if (closed) {
points = points.concat(points.slice(0, 2));
}
// Properties of a line
const line = (pointA, pointB) => {
const lengthX = pointB.x - pointA.x;
const lengthY = pointB.y - pointA.y;
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
};
};
// Position of a control point
const controlPoint = (current, previous, next, reverse) => {
const p = previous || current;
const n = next || current;
const o = line(p, n);
const angle = o.angle + (reverse ? Math.PI : 0);
const length = o.length * smoothing;
const x = current.x + Math.cos(angle) * length;
const y = current.y + Math.sin(angle) * length;
return { x, y };
};
let pathData = [];
pathData.push({ type: "M", values: [points[0].x, points[0].y] });
for (let i = 1; i < points.length; i++) {
let point = points[i];
const cp1 = controlPoint(points[i - 1], points[i - 2], point);
const cp2 = controlPoint(point, points[i - 1], points[i + 1], true);
//console.log( i, 'a', a)
const command = {
type: "C",
values: [cp1.x, cp1.y, cp2.x, cp2.y, point.x, point.y]
};
pathData.push(command);
}
// copy last commands 1st controlpoint to first curveto
if (closed) {
let comLast = pathData[pathData.length - 1];
let valuesLastC = comLast.values;
let valuesFirstC = pathData[1].values;
pathData[1] = {
type: "C",
values: [valuesLastC[0], valuesLastC[1], valuesFirstC.slice(2)].flat()
};
// delete last curveto
pathData = pathData.slice(0, pathData.length - 1);
}
return pathData;
};
// convert pathdata to d attribute string
function pathDataToD(pathData, decimals=3){
let d = pathData
.map((com) => {
return `${com.type}${com.values.map(value=>{return +value.toFixed(decimals)}).join(" ")}`;
})
.join(" ");
return d;
}
<!-- language: lang-html -->
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" id="svg">
<path id="pathPoly" fill="none" stroke="green"></path>
<path id="path" fill="none" stroke="#000"></path>
</svg>
<!-- end snippet -->
See original post by François Romain: Smooth a Svg path with cubic bezier curves)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论