Search code examples
svgsvg-animate

svg animate marker-end/marker-start with curved path animation


i have curved paths that animate using the animate element. it does animate the path in combination with @keyframes, but when i add markers inside the curved path, the markers do not animate. i tested one using a simple path by changing the path values attribute. it worked perfectly using a simple path but how do i get the marker elements animate with the curved path?

codepen demo: https://codepen.io/tfss/pen/yZoBLo using simple and curved paths


Solution

  • Since what you have is a Bézier curve, you can calculate the points to draw the curve from the origin to the new position. To understand what comes next you need to understand what is a Bézier curve.

    In the next example I'm using an input type range to change the curve. You may animate it instead. Please read the comments in the code.

    //the points used to draw the final curve
    let points = [[308.7, 34.9],[381.3, 37.4],[444.3, 78],[478.7,137.5]];
    //the position of the final point of the actual curve on the final curve.
    let t = 0.5;
    //the points for the actual curve
    let newPoints = getBezierPoints(t);
    drawCBezier(newPoints, the_bezier);
    
    // on input recalcalculate the points for the curve and the curve
    T.addEventListener("input", function() {
      t = this.value;
      newPoints = getBezierPoints(t);
      drawCBezier(newPoints, the_bezier);
    });
    
    
    function getBezierPoints(t) {
      let helperPoints = [];
    
      // helper points 0,1,2
      for (let i = 1; i < 4; i++) {
        //points.length must be 4 !!!
        let p = lerp(points[i - 1], points[i], t);
        helperPoints.push(p);
      }
    
      // helper points 3,4
      helperPoints.push(lerp(helperPoints[0], helperPoints[1], t));
      helperPoints.push(lerp(helperPoints[1], helperPoints[2], t));
    
      // helper point 5 is where the first Bézier ends and where the second Bézier begins
      helperPoints.push(lerp(helperPoints[3], helperPoints[4], t));
    
      // points for the dynamic bézier
      let firstBezier = [
        points[0],
        helperPoints[0],
        helperPoints[3],
        helperPoints[5]
      ];
      
      return firstBezier;
    }
    
    function lerp(A, B, t) {
      // a virtual line from A to B
      // get the position of a point on this line
      // if(t == .5) the point in in the center of the line
      // 0 <= t <= 1
      let ry = [
        (B[0] - A[0]) * t + A[0], //x
        (B[1] - A[1]) * t + A[1] //y
      ];
      return ry;
    }
    
    function drawCBezier(points, path) {
      let d = `M${points[0][0]},${points[0][1]} C`;
      // points.length == 4
      for (let i = 1; i < 4; i++) {
        d += ` ${points[i][0]},${points[i][1]}`;
      }
      
      path.setAttributeNS(null, "d", d);
    }
    svg {
      border: 1px solid;
      width:100vh
    }
    <input type="range" value=".5" min="0" max="1" step=".01" id="T">
    <svg viewBox="290 0 200 150">
      <defs>
      <marker
        id="arrow"
        orient="auto-start-reverse"
        viewBox="0 0 7.1 11.5"
        markerWidth="7.1"
        markerHeight="11.5"
        markerUnits="strokeWidth"
        refX="5" refY="5.75">
        <path d="M1 11.5L0 10.4L5.1 5.7L0 1L1 0L7.1 5.7L1 11.5" fill="#00897b"></path> 
      </marker>
      
      <marker id="circle" viewBox="0 0 6 6" refX="1" refY="3"
    			markerUnits="userSpaceOnUse" orient="auto"
    			markerWidth="6" markerHeight="6">
    			<circle cx="3" cy="3" r="3" fill="#4caf50"/>
       </marker>
    </defs>
    
      
      <path id="the_bezier" d=""
        marker-start="url(#circle)"
        marker-end="url(#arrow)"
        stroke-width="2"
        fill="none"
        stroke="blue" />
    </svg>

    You may find this post about Bézier curves useful: How to add a point to a Cubic Bézier Curve In SVG

    For a different approach you may like to read this answer: Stroke animation, how to attach another path to the appearing stroke?

    I hope this helps.