【问题标题】:Draw along SVG path沿 SVG 路径绘制
【发布时间】:2021-09-09 05:33:42
【问题描述】:

我有一个使用贝塞尔曲线的 SVG 路径,例如:

m 776,2226 c 0,0 258.61385,-173.7593 289.34025,-325.8576 57.158,-282.9367 15.5277,-622.2212 50.8732,-933.13714 12.8345,-112.89946 104.2775,-278.6582 22.2568,-340.66923 -50.5144,-38.19103 -158.97817,99.97904 -158.97817,99.97904

在我的应用程序中,我想从初始点(x=776,y=2226)开始,慢慢绘制路径。例如,当用户按下按钮时,路径会显示更多。

我想用 HTML 画布来做这个。

请注意,此路径不是封闭路径。

想过用Canvas的isPointInPath()函数,从初始点开始,逐个绘制像素。但是,如何找到路径中的所有点?

另外的方法是什么?

【问题讨论】:

标签: algorithm svg canvas html5-canvas


【解决方案1】:

也许是stroke-dashoffset?有一个很好的小解释器和demo on CSS-Tricks.

【讨论】:

    【解决方案2】:

    就像在 SVG 中一样,Canvas2D API 具有 dash-offset 和 stroke-dasharray 选项,分别通过 lineDashOffset 属性和 setLineDash 方法。

    但是,这个 API 仍然缺乏测量路径长度的正确方法(早期有关于扩展 Path2D API 的讨论,但还没有具体的内容)。
    你可以自己计算那个长度,但路径越复杂,你出错的可能性就越大...... 所以最简单的可能实际上是使用 SVGGeometry 元素,它确实公开了一个 .getTotalLength() 方法。

    const declaration = `M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z`;
    
    const geom = document.createElementNS( "http://www.w3.org/2000/svg", "path" );
    geom.setAttribute( "d", declaration );
    const length = geom.getTotalLength();
    
    const path = new Path2D( declaration );
    const canvas = document.querySelector( "canvas" );
    const ctx = canvas.getContext( "2d" );
    // [ dash - hole ]
    ctx.setLineDash( [ length, length ] );
    
    const duration = 2000;
    const start = performance.now();
    requestAnimationFrame( draw );
    
    ctx.fillStyle = "green";
    ctx.strokeStyle = "red";
    
    function draw(now) {
      ctx.clearRect( 0, 0, canvas.width, canvas.height );
      const delta = (now % duration) / duration;
      ctx.lineDashOffset = length - (length * delta);
      ctx.stroke( path );
      requestAnimationFrame( draw );
    }
    <canvas width="100" height="100"></canvas>

    对于那些为了标题而来到这里并想要沿路径移动形状的人,我们可以继续使用我们的 SVGGeometryElement 及其getPointAtLength 方法:

    const declaration = `M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z`;
    
    const geom = document.createElementNS( "http://www.w3.org/2000/svg", "path" );
    geom.setAttribute( "d", declaration );
    const length = geom.getTotalLength();
    
    const path = new Path2D( declaration );
    const canvas = document.querySelector( "canvas" );
    const ctx = canvas.getContext( "2d" );
    
    const duration = 2000;
    const start = performance.now();
    requestAnimationFrame( draw );
    
    ctx.fillStyle = "green";
    ctx.strokeStyle = "red";
    
    function draw(now) {
      ctx.clearRect( 0, 0, canvas.width, canvas.height );
      const delta = (now % duration) / duration;
      const point = geom.getPointAtLength( length * delta );
      ctx.fillRect( point.x - 10, point.y -10, 20, 20 );
      ctx.stroke( path );
      requestAnimationFrame( draw );
    }
    <canvas width="100" height="100"></canvas>

    现在,在您的情况下,我想您仍然需要做一些工作才能正确缩放返回的值,就像您在画布上绘制时可能缩放路径一样,但我将把它作为练习(没什么太复杂了)。

    【讨论】:

      【解决方案3】:

      我对贝塞尔曲线的数学以及它们在 SVG 中的表示方式进行了一些研究。

      我的路径使用m 命令表示路径的起点,使用c 命令表示具有相对控制点和终点的贝塞尔曲线。这条曲线有多个段。

      一个示例路径

      m 1,2 c 3,4 5,6 7,8 9,10 11,12 13,14
      
      

      意思是:

      • 路径的起点是 (1,2)
      • 然后是一条有 2 段的曲线
      • 第一段从 (1,2) 开始,它的第一个控制点在 (1+3,2+4)=(4,6),第二个控制点在 (1+5,2+6)= (6,8) 结束于 (1+7,2+8)=(8,10)
      • 第二段从最后一段的终点 (8,10) 开始。它的第一个控制点是(8+9,10+10)=(18,19),第二个控制点是(8+11,10+13)=(18,24),终点是(8+13) ,10+14)=(18,27)

      使用贝塞尔曲线的数学,我编写了这段代码来返回具有多段的贝塞尔曲线上的点。请注意,它只处理路径以m 开头然后以c 命令继续的情况。

      const POINTS_IN_SEGMENT = 6;
      
          class Point {
              x;
              y;
      
              constructor(xStr, yStr) {
                  this.x = parseInt(xStr, 10);
                  this.y = parseInt(yStr, 10);
              }
      
              add(other) {
                  return new Point(this.x + other.x, this.y + other.y);
              }
          }
      
          class BezierCurveSegment {
              startPoint;
              controlPoint1;
              controlPoint2;
              endPoint;
      
              constructor(startPoint, controlPoint1, controlPoint2, endPoint) {
                  this.startPoint = startPoint;
                  this.controlPoint1 = controlPoint1;
                  this.controlPoint2 = controlPoint2;
                  this.endPoint = endPoint;
              }
      
              getPointsOnSegment(accuracy) {
                  const points = [];
                  for (let i = 0; i < 1; i += accuracy) {
                      const p = bezier(i, this.startPoint, this.controlPoint1, this.controlPoint2, this.endPoint);
                      points.push(new Point(p.x, p.y));
                  }
                  return points;
              }
          }
      
          class BezierCurve {
              startPoint;
              segments;
      
              constructor(startPoint, segments) {
                  this.startPoint = startPoint;
                  this.segments = segments;
              }
      
              getPointsOnCurve(pointCount) {
                  const accuracy = 1 / pointCount * this.segments.length;
                  let points = [];
                  for (let segment of this.segments) {
                      let pointsOnSegment = segment.getPointsOnSegment(accuracy);
                      points = points.concat(pointsOnSegment);
                  }
                  return points;
              }
          }
      
          // https://stackoverflow.com/questions/16227300/
          function bezier(t, p0, p1, p2, p3) {
              // console.log("bezier", t, p0, p1, p2, p3)
              var cX = 3 * (p1.x - p0.x),
                      bX = 3 * (p2.x - p1.x) - cX,
                      aX = p3.x - p0.x - cX - bX;
      
              var cY = 3 * (p1.y - p0.y),
                      bY = 3 * (p2.y - p1.y) - cY,
                      aY = p3.y - p0.y - cY - bY;
      
              var x = (aX * Math.pow(t, 3)) + (bX * Math.pow(t, 2)) + (cX * t) + p0.x;
              var y = (aY * Math.pow(t, 3)) + (bY * Math.pow(t, 2)) + (cY * t) + p0.y;
      
              return {x: x, y: y};
          }
      
          function createCurveFromSVGPath(pathStr) {
              pathStr = pathStr.replaceAll(',', ' ');
              let items = pathStr.split(' ');
      
              if (items[0] !== "m") {
                  throw "First item in the path is not 'm' command (only relative is supported)";
              }
              if (items[3] !== "c") {
                  throw "Third item in the path is not 'c' command (only relative is supported)";
              }
      
              let startPoint = new Point(items[1], items[2]);
      
      
              // done with "m x,y c" items
              items = items.slice(4);
      
              // divide by the number of points each segment has
              let segmentCount = items.length / POINTS_IN_SEGMENT;
      
              let segments = [];
      
              for (let i = 0; i < segmentCount; i++) {
                  let lastSegmentEndPoint;
                  if (i === 0) {
                      lastSegmentEndPoint = startPoint;
                  } else {
                      lastSegmentEndPoint = segments[i - 1].endPoint;
                  }
      
                  // noinspection PointlessArithmeticExpressionJS
                  const segment = new BezierCurveSegment(
                          // all these numbers are relative to previous segment's end point
                          // see https://stackoverflow.com/questions/26675960/
                          lastSegmentEndPoint,
                          lastSegmentEndPoint.add(new Point(items[i * POINTS_IN_SEGMENT + 0], items[i * POINTS_IN_SEGMENT + 1])),
                          lastSegmentEndPoint.add(new Point(items[i * POINTS_IN_SEGMENT + 2], items[i * POINTS_IN_SEGMENT + 3])),
                          lastSegmentEndPoint.add(new Point(items[i * POINTS_IN_SEGMENT + 4], items[i * POINTS_IN_SEGMENT + 5])),
                  );
                  segments.push(segment);
              }
      
              return new BezierCurve(startPoint, segments);
          }
      

      为了验证它是否正常工作,我在画布上绘制点,并使用 Path2D 绘制相同的路径。

          function drawPoints(ctx, points) {
              ctx.moveTo(points[0].x, points[0].y);
              for (let point of points) {
                  ctx.lineTo(point.x, point.y);
                  ctx.moveTo(point.x, point.y);
              }
              ctx.stroke();
          }
      
          let pathStr = 'm 776.65415,2226.6574 c 0,0 258.61385,-173.7593 289.34025,-325.8576 57.158,-282.9367 15.5277,-622.2212 50.8732,-933.13714 12.8345,-112.89946 104.2775,-278.6582 22.2568,-340.66923 -50.5144,-38.19103 -158.97817,99.97904 -158.97817,99.97904';
      
          const curve = createCurveFromSVGPath(pathStr);
      
          let pointsOnCurve = curve.getPointsOnCurve(100000);
      
          const canvas = document.getElementById('canvas');
          const ctx = canvas.getContext('2d');
      
          drawPoints(ctx, pointsOnCurve);
      
          ctx.strokeStyle = "#FF0000";
          ctx.stroke(new Path2D(pathStr););
      
      

      【讨论】:

        猜你喜欢
        • 2018-11-06
        • 1970-01-01
        • 2019-05-02
        • 2011-04-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多