Search code examples
cssreactjsanimationsvgcss-animations

Animate gradient pulse along SVG path


I want to animate a small line segment across a SVG path, with a gradient across the line segment. Similar to the "pulsing" effects on this site: https://www.authkit.com/

In my case, I want this type of gradient moving left to right:

enter image description here

I have a basic version working in code, but I don't know how to display a gradient. The image below shows my SVG path and the white segment does animate properly from left to right with the css that is below the image.

enter image description here

.animate-graph{
  stroke-dasharray: 100 4350;
  stroke-dashoffset: 0;
  animation: dash 3s infinite linear;
}

@keyframes dash {
  from { stroke-dashoffset: 0; }
  to { stroke-dashoffset: 2000; }
}

I inspected the AuthKit website I linked earlier and I understand that they use rotating conical gradients with masking. However, I tried it here and due to a more complex path, it doesn't look so good. How can I display a gradient on the pulsing line segment that follows the actual SVG?


Solution

  • Here I combine the animation of a circle along a path with the animation of a stroke dash array along the same path.

    There are limits to how "curve'y" you can make the path. You can see the issue on the smaller middle curve. So, the less curve'y and the faster, the better.

    let p1 = document.getElementById('p1');
    let u1 = document.getElementById('u1');
    
    var u1Keyframes = new KeyframeEffect(
      u1, [{
          strokeDasharray: `100 ${p1.getTotalLength(p1)}`,
          strokeDashoffset: "100"
        },
        {
          strokeDasharray: `100 ${p1.getTotalLength(p1)}`,
          strokeDashoffset: `-${p1.getTotalLength(p1) - 100}`
        }
      ], {
        duration: 5000,
        iterations: Infinity
      }
    );
    
    var a1 = new Animation(u1Keyframes, document.timeline);
    a1.play();
    <svg viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <radialGradient id="g1">
          <stop offset="0%" stop-color="white" />
          <stop offset="25%" stop-color="orange" />
          <stop offset="95%" stop-opacity="0" stop-color="orange" />
        </radialGradient>
        <mask id="m1">
          <use id="u1" href="#p1" 
        stroke="white" stroke-width="4" fill="none"
        stroke-dasharray="100 200" stroke-linecap="round" />
        </mask>
        <path id="p1" d="M 0 0 q 68 -78 84 -59 Q 116 -24 129 -59
          Q 147 -113 162 -56 Q 174 -8 210 -60 Q 245 -111 300 -56 L 358 0" />
      </defs>
      <rect width="300" height="100" fill="black" />
      <use href="#p1" transform="translate(0 100)" stroke="orange"
        stroke-width=".3" fill="#222" opacity=".6" />
      <g transform="translate(0 100)" mask="url(#m1)">
        <circle r="50" fill="url(#g1)">
          <animateMotion dur="5000ms" repeatCount="indefinite">
            <mpath href="#p1" />
          </animateMotion>
        </circle>
      </g>
    </svg>

    Here is an alternative where I have switched around the circle and the stroke, so that the circle in now the mask. Now, the line can be longer, but the limitation is that it can only have one color -- not a gradient color like the first example. The curve'y'ness of the path is not an issue, because the gradient is only in the short tail of the line.

    let p1 = document.getElementById('p1');
    let u1 = document.getElementById('u1');
    let u2 = document.getElementById('u2');
    
    var u1Keyframes = new KeyframeEffect(
      u1, [{
          strokeDasharray: `20 ${p1.getTotalLength(p1)}`,
          strokeDashoffset: "0"
        },
        {
          strokeDasharray: `20 ${p1.getTotalLength(p1)}`,
          strokeDashoffset: `-${p1.getTotalLength(p1)}`
        }
      ], {
        duration: 5000,
        iterations: Infinity
      }
    );
    
    var u2Keyframes = new KeyframeEffect(
      u2, [{
          strokeDasharray: `70 ${p1.getTotalLength(p1)}`,
          strokeDashoffset: "-20"
        },
        {
          strokeDasharray: `70 ${p1.getTotalLength(p1)}`,
          strokeDashoffset: `-${p1.getTotalLength(p1)+20}`
        }
      ], {
        duration: 5000,
        iterations: Infinity
      }
    );
    
    var a1 = new Animation(u1Keyframes, document.timeline);
    var a2 = new Animation(u2Keyframes, document.timeline);
    a1.play();
    a2.play();
    <svg viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <radialGradient id="g1">
          <stop offset="0%" stop-color="black" />
          <stop offset="95%" stop-color="white" />
        </radialGradient>
        <mask id="m1">
          <circle r="20" fill="url(#g1)">
          <animateMotion dur="5000ms" repeatCount="indefinite">
            <mpath href="#p1" />
          </animateMotion>
        </circle>
        </mask>
        <path id="p1" d="M 0 0 q 68 -78 84 -59 Q 116 -24 129 -59
          Q 147 -113 162 -56 Q 174 -8 210 -60 Q 245 -111 300 -56 L 358 0" />
      </defs>
      <rect width="300" height="100" fill="black" />
      <use href="#p1" transform="translate(0 100)" stroke="orange"
        stroke-width=".3" fill="#222" opacity=".6" />
      <use id="u2" href="#p1" transform="translate(0 100)"
        stroke="white" stroke-width="3" fill="none"
        stroke-dasharray="50 500" stoke-dashoffset="-20"
        stroke-linecap="round" marker-end="url(#arrow)" />
        <use id="u1" href="#p1" transform="translate(0 100)" mask="url(#m1)"
        stroke="white" stroke-width="3" fill="none"
        stroke-dasharray="50 500" stroke-linecap="round" />
    </svg>