使用 JavaScript 平滑关闭 SVG 路径

Smoothly close an SVG path with JavaScript

提问人:Andrew Foulds 提问时间:7/17/2023 最后编辑:Andrew Foulds 更新时间:7/20/2023 访问量:165

问:

我正在处理一些代码,其中用户删除一个数字或点/标记,然后通过它们绘制一条贝塞尔曲线 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,但我再次得到了一个尖锐的颠簸。

这怎么能做到呢?

JavaScript SVG 平滑 贝塞尔

评论

1赞 mplungjan 7/17/2023
请访问帮助中心,参观内容和提问方式做一些研究 - 搜索 SO 寻找答案。如果您遇到困难,请发布一个最小的可重复示例,并使用 [<>] 片段编辑器记录输入和预期输出。
1赞 Mike 'Pomax' Kamermans 7/17/2023
但是在 JS 代码说明中,不要设置顶级 SVG。找到 path 元素并将其属性设置为需要显示的路径。如果你需要一条闭合曲线,而不是一条线,你必须创建你自己的“最终 Q 或 C 曲线,该曲线的终点与你的起始坐标相同,然后是你的 Z。innerHTMLd
0赞 Andrew Foulds 7/18/2023
我搜索了答案,但我找不到任何我可以使用的东西。10 年前的一篇文章提供了链接到不再存在的页面的答案。我确实用谷歌搜索过,但我找到的几个答案是用我无法解释的编码语言。我可以通过添加额外的 C 曲线来关闭曲线,但问题仍然存在,它不能顺利连接到起点 - 我得到了一个突然的变化。我正在尝试创建一条闭合贝塞尔曲线,该曲线贯穿一组点,但没有闭合曲线,在它与起点相交的地方产生突然变化(角度凸起)。
0赞 herrstrietzel 7/18/2023
我想我已经找到了解决方案。我强烈建议将帖子标题更改为“使用立方贝塞尔曲线平滑闭合的 Svg 路径”——目前有点模棱两可

答:

1赞 herrstrietzel 7/18/2023 #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 },
       //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));
  }

这样,该函数将在第一/最后一个顶点和第二个顶点之间创建一条平滑的曲线。

polyline smoothed

显然,这条路径有一个重叠的曲线段,我们需要删除。此外,第一个需要一些调整。C

复制最后的命令 1。控制点和删除最后一个命令C

我们可以将最后一个命令的第一个控制点复制到第一个命令。C

  // 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); 
  }

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;
}
<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>

请参阅FrançoisRomain的原始帖子:使用三次贝塞尔曲线平滑Svg路径)

评论

0赞 Andrew Foulds 7/20/2023
谢谢,我想我让它运行良好。另一个问题想到了。SVG 中多边形的原因是否用作点坐标的存储?也就是说,如果我可以从数据库中获取/设置坐标,那么我可以省去多边形吗?
0赞 herrstrietzel 7/20/2023
请原谅我,多边形元素只是为了说明使用点变量的好处,例如而不是 .添加一个带有坐标的坐标,例如从数据库中检索到的坐标可以方便地获得数据的第一个可视化并计算适当的 .所以不,你不需要多边形 - 我会把它从 eaxmple 中删除。但请考虑按照建议修改帖子标题(尤其是术语“循环”通常用于编程循环)。{x:10, y:50}[10, 50]<polygon>viewBox