Thursday, 19 February 2026

Web Tutorial: Year of the Horse SVG Animation (Part 3/3)

In this final part, we will use what we did for the grassy ground, to make more background. This one will be hills.

For that, we create a g tag.
</circle>

<g>  
</g>  


<g>    
  <path d="M0 350
    Q100 340 200 350
    Q300 345 350 350
    Q500 340 600 350
    Q700 340 800 350
    Q900 345 950 350
    Q1100 340 1200 350
    L1200 450
    L0 450
    Z"

    stroke-dasharray="2,3"
    stroke="url(#grassGradient)"
    stroke-width="5"
    fill="url(#grassGradient)"
  />

  <animateTransform
   attributeName="transform"
   type="translate"
   from="0,0"
   to="-600,0"
   dur="2.5s"
   repeatCount="indefinite"
  />
</g>


In there, we have several path tags. Each of these will be filled using hillGradient.
<g>  
  <path d="M0 350
    L40 300
    L60 330
    L90 250
    L120 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M200 350
    L240 280
    L280 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M280 350
    L300 300
    L320 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M350 350
    L430 310
    L460 330
    L490 300
    L520 330
    L550 310
    L600 350
    Z"

    fill="url(#hillGradient)"
  />

</g>


In the defs tag, we add a new linearGradient tag, hillGradient.
<linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">
  <stop offset="0%" stop-color="rgb(0, 0, 50)">
    <animate
     attributeName="stop-color"
     values="rgb(0, 0, 50);rgb(50, 150, 255);rgb(150, 20, 0);rgb(0, 0, 50)"
     dur="10s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgb(50, 50, 250)">
    <animate
     attributeName="stop-color"
     values="rgb(50, 50, 250);rgb(250, 250, 255);rgb(250, 150, 0);rgb(50, 50, 250)"
     dur="10s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>
</linearGradient>

<linearGradient id="hillGradient" x1="0%" y1="0%" x2="0%" y2="80%">

</linearGradient>


<linearGradient id="grassGradient" x1="0%" y1="0%" x2="0%" y2="50%">
  <stop offset="0%" stop-color="rgb(80, 100, 80)">
    <animate
     attributeName="stop-color"
     values="rgb(80, 100, 80);rgb(100, 150, 100);rgb(120, 200, 120);rgb(100, 150, 100);rgb(80, 100, 80)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgb(0, 20, 0)">
    <animate
     attributeName="stop-color"
     values="rgb(0, 20, 0);rgb(20, 50, 20);rgb(50, 100, 50);rgb(20, 50, 20);rgb(0, 20, 0)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>
</linearGradient>


It's mostly shades of brown. I could go into it, but honestly I'd just be repeating myself from the previous part of this tutorial, so I won't.
<linearGradient id="hillGradient" x1="0%" y1="0%" x2="0%" y2="80%">
  <stop offset="0%" stop-color="rgb(100, 50, 50)">
    <animate
     attributeName="stop-color"
     values="rgb(100, 50, 50);rgb(200, 100, 100);rgb(250, 200, 200);rgb(200, 100, 100);rgb(100, 50, 50)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgb(50, 0, 0)">
    <animate
     attributeName="stop-color"
     values="rgb(50, 0, 0);rgb(150, 100, 100);rgb(200, 100, 100);rgb(150, 100, 100);rgb(50, 0, 0)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

</linearGradient>


Behold! But the real magic isn't here yet.


Before that, let's add another layer to the background - a second series of path tags, these filled using hill2Gradient. For this, make a copy of hillGradient and then modify it to a significantly darker series of browns.
<linearGradient id="hillGradient" x1="0%" y1="0%" x2="0%" y2="80%">
  <stop offset="0%" stop-color="rgb(100, 50, 50)">
    <animate
     attributeName="stop-color"
     values="rgb(100, 50, 50);rgb(200, 100, 100);rgb(250, 200, 200);rgb(200, 100, 100);rgb(100, 50, 50)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgb(50, 0, 0)">
    <animate
     attributeName="stop-color"
     values="rgb(50, 0, 0);rgb(150, 100, 100);rgb(200, 100, 100);rgb(150, 100, 100);rgb(50, 0, 0)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>
</linearGradient>

<linearGradient id="hill2Gradient" x1="0%" y1="0%" x2="0%" y2="80%">
  <stop offset="0%" stop-color="rgba(150, 150, 150, 0.5)">
    <animate
     attributeName="stop-color"
     values="rgba(150, 150, 150, 0.5);rgba(180, 180, 180, 0.5);rgba(200, 200, 200, 0.5);rgba(180, 180, 180, 0.5);rgba(150, 150, 150, 0.5)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgba(50, 50, 50, 0.5)">
    <animate
     attributeName="stop-color"
     values="rgba(50, 50, 50, 0.5);rgba(80, 80, 80, 0.5);rgba(100, 100, 100, 0.5);rgba(80, 80, 80, 0.5);rgba(50, 50, 50, 0.5)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>
</linearGradient>


<linearGradient id="grassGradient" x1="0%" y1="0%" x2="0%" y2="50%">
  <stop offset="0%" stop-color="rgb(80, 100, 80)">
    <animate
     attributeName="stop-color"
     values="rgb(80, 100, 80);rgb(100, 150, 100);rgb(120, 200, 120);rgb(100, 150, 100);rgb(80, 100, 80)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgb(0, 20, 0)">
    <animate
     attributeName="stop-color"
     values="rgb(0, 20, 0);rgb(20, 50, 20);rgb(50, 100, 50);rgb(20, 50, 20);rgb(0, 20, 0)"
     dur="100s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>
</linearGradient>


Here's the second series of paths. Make sure it comes before the first series of paths, in the code.
</circle>

<g>    
  <path d="M100 350
    L140 200
    L180 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M200 350
    L240 300
    L260 330
    L290 220
    L330 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M350 350
    L380 260
    L410 290
    L440 260
    L500 300
    L550 200
    L600 350
    Z"

    fill="url(#hill2Gradient)"
  />
</g>


<g>    
  <path d="M0 350
    L40 300
    L60 330
    L90 250
    L120 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M200 350
    L240 280
    L280 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M280 350
    L300 300
    L320 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M350 350
    L430 310
    L460 330
    L490 300
    L520 330
    L550 310
    L600 350
    Z"

    fill="url(#hillGradient)"
  />
</g>


So you see a darker series of hills behind that first series! It doesn't look that impressive. Not yet, at least.


Now, we want to extend both series of hills, for reasons that will be clear later. We want to copy the exact same layout, but to the right of the existing layout. So what we do first, is make a copy in their respective g tags.
<g>    
  <path d="M100 350
    L140 200
    L180 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M200 350
    L240 300
    L260 330
    L290 220
    L330 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M350 350
    L380 260
    L410 290
    L440 260
    L500 300
    L550 200
    L600 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M100 350
    L140 200
    L180 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M200 350
    L240 300
    L260 330
    L290 220
    L330 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M350 350
    L380 260
    L410 290
    L440 260
    L500 300
    L550 200
    L600 350
    Z"

    fill="url(#hill2Gradient)"
  />

</g>

<g>    
  <path d="M0 350
    L40 300
    L60 330
    L90 250
    L120 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M200 350
    L240 280
    L280 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M280 350
    L300 300
    L320 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M350 350
    L430 310
    L460 330
    L490 300
    L520 330
    L550 310
    L600 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M0 350
    L40 300
    L60 330
    L90 250
    L120 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M200 350
    L240 280
    L280 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M280 350
    L300 300
    L320 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M350 350
    L430 310
    L460 330
    L490 300
    L520 330
    L550 310
    L600 350
    Z"

    fill="url(#hillGradient)"
  />

</g>


Then modify the copies so that the horizontal specs each have 600 added to them, because 600 is the width of the SVG, remember? What this means is that now there are more hills, identical to what's visible, but outside of the SVG.
<g>    
  <path d="M100 350
    L140 200
    L180 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M200 350
    L240 300
    L260 330
    L290 220
    L330 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M350 350
    L380 260
    L410 290
    L440 260
    L500 300
    L550 200
    L600 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M700 350
    L740 200
    L780 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M800 350
    L840 300
    L860 330
    L890 220
    L930 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M950 350
    L980 260
    L1010 290
    L1040 260
    L1100 300
    L1150 200
    L1200 350
    Z"

    fill="url(#hill2Gradient)"
  />
</g>

<g>    
  <path d="M0 350
    L40 300
    L60 330
    L90 250
    L120 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M200 350
    L240 280
    L280 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M280 350
    L300 300
    L320 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M350 350
    L430 310
    L460 330
    L490 300
    L520 330
    L550 310
    L600 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M600 350
    L640 300
    L660 330
    L690 250
    L720 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M800 350
    L840 280
    L880 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M880 350
    L900 300
    L920 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M950 350
    L1030 310
    L1060 330
    L1090 300
    L1120 330
    L1150 310
    L1200 350
    Z"

    fill="url(#hillGradient)"
  />
</g>


What we want to do now, is add animateTransform tags as we did for the grassy background. We do it for both g tags containing the "hills". The animations will move the "hills" a full 600 pixels left, and repeat forever. The difference is that the light brown hills will animate faster at a duration 3 seconds, while the hills further in the background at a dark brown, will animate slower at a duration of 5 seconds! The contrast is going to be awesome!
<g>    
  <path d="M100 350
    L140 200
    L180 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M200 350
    L240 300
    L260 330
    L290 220
    L330 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M350 350
    L380 260
    L410 290
    L440 260
    L500 300
    L550 200
    L600 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M700 350
    L740 200
    L780 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M800 350
    L840 300
    L860 330
    L890 220
    L930 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <path d="M950 350
    L980 260
    L1010 290
    L1040 260
    L1100 300
    L1150 200
    L1200 350
    Z"

    fill="url(#hill2Gradient)"
  />

  <animateTransform
   attributeName="transform"
   type="translate"
   from="0,0"
   to="-600,0"
   dur="5s"
   repeatCount="indefinite"
  />

</g>

<g>    
  <path d="M0 350
    L40 300
    L60 330
    L90 250
    L120 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M200 350
    L240 280
    L280 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M280 350
    L300 300
    L320 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M350 350
    L430 310
    L460 330
    L490 300
    L520 330
    L550 310
    L600 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M600 350
    L640 300
    L660 330
    L690 250
    L720 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M800 350
    L840 280
    L880 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M880 350
    L900 300
    L920 350
    Z"

    fill="url(#hillGradient)"
  />

  <path d="M950 350
    L1030 310
    L1060 330
    L1090 300
    L1120 330
    L1150 310
    L1200 350
    Z"

    fill="url(#hillGradient)"
  />

  <animateTransform
   attributeName="transform"
   type="translate"
   from="0,0"
   to="-600,0"
   dur="3s"
   repeatCount="indefinite"
  />

</g>


At this point, you might want to slow down all the animations in the linear gradients, from 10 seconds to 100.
<defs>
  <linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">
    <stop offset="0%" stop-color="rgb(0, 0, 50)">
      <animate
       attributeName="stop-color"
       values="rgb(0, 0, 50);rgb(50, 150, 255);rgb(150, 20, 0);rgb(0, 0, 50)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>

    <stop offset="100%" stop-color="rgb(50, 50, 250)">
      <animate
       attributeName="stop-color"
       values="rgb(50, 50, 250);rgb(250, 250, 255);rgb(250, 150, 0);rgb(50, 50, 250)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>
  </linearGradient>

  <linearGradient id="hillGradient" x1="0%" y1="0%" x2="0%" y2="80%">
    <stop offset="0%" stop-color="rgb(100, 50, 50)">
      <animate
       attributeName="stop-color"
       values="rgb(100, 50, 50);rgb(200, 100, 100);rgb(250, 200, 200);rgb(200, 100, 100);rgb(100, 50, 50)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>

    <stop offset="100%" stop-color="rgb(50, 0, 0)">
      <animate
       attributeName="stop-color"
       values="rgb(50, 0, 0);rgb(150, 100, 100);rgb(200, 100, 100);rgb(150, 100, 100);rgb(50, 0, 0)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>
  </linearGradient>

  <linearGradient id="hill2Gradient" x1="0%" y1="0%" x2="0%" y2="80%">
    <stop offset="0%" stop-color="rgba(150, 150, 150, 0.5)">
      <animate
       attributeName="stop-color"
       values="rgba(150, 150, 150, 0.5);rgba(180, 180, 180, 0.5);rgba(200, 200, 200, 0.5);rgba(180, 180, 180, 0.5);rgba(150, 150, 150, 0.5)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>

    <stop offset="100%" stop-color="rgba(50, 50, 50, 0.5)">
      <animate
       attributeName="stop-color"
       values="rgba(50, 50, 50, 0.5);rgba(80, 80, 80, 0.5);rgba(100, 100, 100, 0.5);rgba(80, 80, 80, 0.5);rgba(50, 50, 50, 0.5)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>
  </linearGradient>

  <linearGradient id="grassGradient" x1="0%" y1="0%" x2="0%" y2="50%">
    <stop offset="0%" stop-color="rgb(80, 100, 80)">
      <animate
       attributeName="stop-color"
       values="rgb(80, 100, 80);rgb(100, 150, 100);rgb(120, 200, 120);rgb(100, 150, 100);rgb(80, 100, 80)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>

    <stop offset="100%" stop-color="rgb(0, 20, 0)">
      <animate
       attributeName="stop-color"
       values="rgb(0, 20, 0);rgb(20, 50, 20);rgb(50, 100, 50);rgb(20, 50, 20);rgb(0, 20, 0)"
       dur="100s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>
  </linearGradient>
</defs>


And make the circle animations also ten times slower. We made them fast while creating the SVG so we didn't have to wait around forever for stuff to happen, but now that we're about to finish, a slow-moving sky background makes more sense.
<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">
  <animate
   id="moon0"
   attributeName="cx"
   from="620" to="400"
   dur="50s"
   begin="0s;moon3.end"
   repeatCount="1"
  />

  <animate
   id="moon1"
   attributeName="opacity"
   from="1" to="0"
   dur="50s"
   begin="0s;moon3.end"
   repeatCount="1"
  />

  <animate
   id="moon2"
   attributeName="cx"
   from="300" to="-10"
   dur="50s"
   begin="moon1.end"
   repeatCount="1"
  />

  <animate
   id="moon3"
   attributeName="opacity"
   from="0" to="1"
   dur="50s"
   begin="moon1.end"
   repeatCount="1"
  />
</circle>


If you run the animation now, it's a galloping horse where the ground moves fastest. The first series of hills moves slightly slower, and the back series of hills are even slower. The sky background with the sun/moon moves at about a tenth of the speed. This provides the illusion of a multi-layered background.

Beautiful, ain't it? Enjoy the animation magic, and your Lunar New Year!





Quit horsing around!
T___T

Monday, 16 February 2026

Web Tutorial: Year of the Horse SVG Animation (Part 2/3)

So far we've animated opacity. We will soon animate linear backgrounds. The running horse deserves a good backdrop and we are going to provide one.

Create a rectangle that fills up the entire SVG, just behind the horse, text and frame. Note that the fill attribute points to skyGradient, which we have not yet created.
<svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">
    <rect fill="url(#skyGradient)" x="0" y="0" width="600" height="400" />

    <text x="300" y="30" text-anchor="middle" font-size="30px" fill="rgb(250, 100, 0)" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2">潮州雷祝大家龙马精神!</text>


Now we're going to create the linearGradient tag skyGradient, inside a defs tag. This gradient begins changing colors at the 50% mark, vertically.
<svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">
  <defs>
    <linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">

    </linearGradient>
  </defs>


  <rect fill="url(#skyGradient)" x="0" y="0" width="600" height="400" />


It goes from a deep blue to a purplish blue.
<defs>
  <linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">
    <stop offset="0%" stop-color="rgb(0, 0, 50)">

    </stop>

    <stop offset="100%" stop-color="rgb(50, 50, 250)">

    </stop>

  </linearGradient>
</defs>

Here, you can see the gradient.



Now add some animation here. It animates the stop-color attribute, changing from deep blue to purplish blue, to brown, then back to deep blue, in the space of 10 seconds, and runs indefinitely. We set the begin attribute to 0 seconds, so it starts right away.
<linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">
  <stop offset="0%" stop-color="rgb(0, 0, 50)">
    <animate
     attributeName="stop-color"
     values="rgb(0, 0, 50);rgb(50, 150, 255);rgb(50, 150, 255);rgb(150, 20, 0);rgb(0, 0, 50)"
     dur="10s"
     begin="0s"
     repeatCount="indefinite"
    />

  </stop>

  <stop offset="100%" stop-color="rgb(50, 50, 250)">

  </stop>
</linearGradient>


And in the lower regions of the gradient, we also animate the stop-color attribute, going from blue to near-white, then orange, and back to blue.
<linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">
  <stop offset="0%" stop-color="rgb(0, 0, 50)">
    <animate
     attributeName="stop-color"
     values="rgb(0, 0, 50);rgb(50, 150, 255);rgb(150, 20, 0);rgb(0, 0, 50)"
     dur="10s"
     begin="0s"
     repeatCount="indefinite"
    />
  </stop>

  <stop offset="100%" stop-color="rgb(50, 50, 250)">
    <animate
     attributeName="stop-color"
     values="rgb(50, 50, 250);rgb(250, 250, 255);rgb(250, 150, 0);rgb(50, 50, 250)"
     dur="10s"
     begin="0s"
     repeatCount="indefinite"
    />

  </stop>
</linearGradient>


Here you can see the transitions!


It feels like the horse is galloping through dawn...


... to day...


... to dusk!


We're going to add a sun. Or moon. Whatever, a heavenly body up in the sky. This is meant to help propagate the illusion that the horse is running forward. This takes the form of a circle tag. I've made it a pale yellow, with a translucent pale yellow border.
<rect fill="url(#skyGradient)" x="0" y="0" width="600" height="400" />

<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">

</circle>


<text x="300" y="30" text-anchor="middle" font-size="30px" fill="rgb(250, 100, 0)" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2">潮州雷祝大家龙马精神!</text>


You won't see anything even if you refresh. That's because the center of the circle, defined by the attributes cx and cy, are outside of the SVG, which is only 600 pixels in width. We're going to animate the position of this circle. The attribute to animate is cx, because we'll be moving the circle horizontally. Set repeatCount to 1; we won't need anything higher than this. For now, the duration should be 5 seconds. Let's name this moon0.
<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">
  <animate
   id="moon0"
   attributeName="cx"
   from="620" to="400"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

</circle>


We now create a new animation, moon1. It will run parallel to moon0, and animate the opacity property from 1 to 0. This means that the "moon" will enter the screen from the right, and fade in.
<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">
  <animate
   id="moon0"
   attributeName="cx"
   from="620" to="400"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

  <animate
   id="moon1"
   attributeName="opacity"
   from="1" to="0"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

</circle>


For the next two animations, moon2 runs once moon1 ends, and moves the "moon" all the way to the left side of the screen, out of sight.
<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">
  <animate
   id="moon0"
   attributeName="cx"
   from="620" to="400"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

  <animate
   id="moon1"
   attributeName="opacity"
   from="1" to="0"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

  <animate
   id="moon2"
   attributeName="cx"
   from="300" to="-10"
   dur="5s"
   begin="moon1.end"
   repeatCount="1"
  />

</circle>


The final animation, moon3, also runs once moon1 ends, and animates the opacity attribute back to 1. In essence, the "moon" moves left, out of screen, and fades out.
<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">
  <animate
   id="moon0"
   attributeName="cx"
   from="620" to="400"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

  <animate
   id="moon1"
   attributeName="opacity"
   from="1" to="0"
   dur="5s"
   begin="0s"
   repeatCount="1"
  />

  <animate
   id="moon2"
   attributeName="cx"
   from="300" to="-10"
   dur="5s"
   begin="moon1.end"
   repeatCount="1"
  />

  <animate
   id="moon3"
   attributeName="opacity"
   from="0" to="1"
   dur="5s"
   begin="moon1.end"
   repeatCount="1"
  />

</circle>


We then make sure moon0 also runs when moon3 ends, making this entire sequence run indefinitely.
<animate
id="moon0"
attributeName="cx"
from="620" to="400"
dur="5s"
begin="0s;moon3.end"
repeatCount="1"
/>


You can see that circle fade in fron the right...


...and fade out on the right as the sky changes!


Now we have a galloping horse, and it looks like it's galloping through night and day! The horse looks like it's galloping through the air. We'll need to plant some ground for it.

Before that, let's define the background colors for the ground. We want it paler green at the top and deeper green at the bottom. And then we want to animate the colors in time to the "sky", so we set both animations at 10 seconds. Both animations cycle through different shades of green at four stages, but the constant is that the upper half of the grassGradient is always significantly lighter than the lower half.
<defs>
  <linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="50%">
    <stop offset="0%" stop-color="rgb(0, 0, 50)">
      <animate
       attributeName="stop-color"
       values="rgb(0, 0, 50);rgb(50, 150, 255);rgb(150, 20, 0);rgb(0, 0, 50)"
       dur="10s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>

    <stop offset="100%" stop-color="rgb(50, 50, 250)">
      <animate
       attributeName="stop-color"
       values="rgb(50, 50, 250);rgb(250, 250, 255);rgb(250, 150, 0);rgb(50, 50, 250)"
       dur="10s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>
  </linearGradient>

  <linearGradient id="grassGradient" x1="0%" y1="0%" x2="0%" y2="50%">
    <stop offset="0%" stop-color="rgb(80, 100, 80)">
      <animate
       attributeName="stop-color"
       values="rgb(80, 100, 80);rgb(100, 150, 100);rgb(120, 200, 120);rgb(100, 150, 100);rgb(80, 100, 80)"
       dur="10s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>

    <stop offset="100%" stop-color="rgb(0, 20, 0)">
      <animate
       attributeName="stop-color"
       values="rgb(0, 20, 0);rgb(20, 50, 20);rgb(50, 100, 50);rgb(20, 50, 20);rgb(0, 20, 0)"
       dur="10s"
       begin="0s"
       repeatCount="indefinite"
      />
    </stop>
  </linearGradient>

</defs>


And now let's use a path tag. It goes all the way from the far left bottom of the SVG, stretching to the right end of the SVG. There are curves along the way, resulting in slight bumps. We'll want to use the stroke-dasharray attribute to simulate a fuzzy edge, like grass.
<circle r="10" cx="650" cy="100" fill="rgb(250,250,200)" stroke="rgba(250,250,200,0.8)" stroke-width="5">
  <animate
   id="moon0"
   attributeName="cx"
   from="620" to="400"
   dur="5s"
   begin="0s;moon3.end"
   repeatCount="1"
  />

  <animate
   id="moon1"
   attributeName="opacity"
   from="1" to="0"
   dur="5s"
   begin="0s;moon3.end"
   repeatCount="1"
  />

  <animate
   id="moon2"
   attributeName="cx"
   from="300" to="-10"
   dur="5s"
   begin="moon1.end"
   repeatCount="1"
  />

  <animate
   id="moon3"
   attributeName="opacity"
   from="0" to="1"
   dur="5s"
   begin="moon1.end"
   repeatCount="1"
  />
</circle>

<path d="M0 350
  Q100 340 200 350
  Q300 345 350 350
  Q500 340 600 350
  Z"

  stroke-dasharray="2,3"
  stroke="url(#grassGradient)"
  stroke-width="5"
  fill="url(#grassGradient)"
/>


<text x="300" y="30" text-anchor="middle" font-size="30px" fill="rgb(250, 100, 0)" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2">潮州雷祝大家龙马精神!</text>


Now, we want the path to stretch all the way to twice the width of the SVG. So copy and paste whatever we've done so far to create more points...
<path d="M0 350
  Q100 340 200 350
  Q300 345 350 350
  Q500 340 600 350
  Q100 340 200 350
  Q300 345 350 350
  Q500 340 600 350

  Z"

  stroke-dasharray="2,3"
  stroke="url(#grassGradient)"
  stroke-width="5"
  fill="url(#grassGradient)"
/>


... note that for the numbers pertaining to the horizontal positioning, we'll want to add 600 (which is the width of the SVG) to them, while keeping the vertical positioning the same. Then use the L directive to finish out the path.
<path d="M0 350
  Q100 340 200 350
  Q300 345 350 350
  Q500 340 600 350
  Q700 340 800 350
  Q900 345 950 350
  Q1100 340 1200 350
  L1200 450
  L0 450

  Z"

  stroke-dasharray="2,3"
  stroke="url(#grassGradient)"
  stroke-width="5"
  fill="url(#grassGradient)"
/>


You see the grassy ground!


Now, this looks weird because in order to complete the illusion of movement and progress, the ground must move with the horse. Envelope the path in a g tag. This will be necessary if we ever want to add any separate objects with the ground, such as a rock, or a shrub. But let's keep it simple for now.
<g>    
  <path d="M0 350
    Q100 340 200 350
    Q300 345 350 350
    Q500 340 600 350
    Q700 340 800 350
    Q900 345 950 350
    Q1100 340 1200 350
    L1200 450
    L0 450
    Z"

    stroke-dasharray="2,3"
    stroke="url(#grassGradient)"
    stroke-width="5"
    fill="url(#grassGradient)"
  />
</g>


Here, the g tag simplifies the animation, because when we add that animation, it's on the g tag instead of the path. Instead of worrying about what attribute to animate, we use an animateTransform tag and translate it left 600 pixels, in the space of 2.5 seconds. And because 600 is the width of the SVG and we've already made the path twice that in width, when you set repeatCount to indefinite, it seems to go on forever!
<g>    
  <path d="M0 350
    Q100 340 200 350
    Q300 345 350 350
    Q500 340 600 350
    Q700 340 800 350
    Q900 345 950 350
    Q1100 340 1200 350
    L1200 450
    L0 450
    Z"

    stroke-dasharray="2,3"
    stroke="url(#grassGradient)"
    stroke-width="5"
    fill="url(#grassGradient)"
  />

  <animateTransform
   attributeName="transform"
   type="translate"
   from="0,0"
   to="-600,0"
   dur="2.5s"
   repeatCount="indefinite"
  />

</g>

Next

More background magic.

Friday, 13 February 2026

Web Tutorial: Year of the Horse SVG Animation (Part 1/3)

It's that time of the year again! Chinese New Year has arrived! 2026 is the Year of the Horse, and I want to work on a nice SVG animation. It's so nice that I'm going to have to break up this web tutorial into more manageable components.

First, we want a galloping horse. No two ways about it. It's the Year of the Horse, after all. From the internet, I obtained this file and scaled it down, then cut out the individual horses to use as frames for the animation. No, I didn't pay for it. I'm also not profiting from this web tutorial, so...

The original image.



horse00.png



horse01.png



horse02.png



horse03.png


This part of the web tutorial shows you how to import external images into an SVG. Let's get the HTML in there. It's a 600 by 400 pixel animation. I set the background to green for visibility. Note that I've included a meta tag for UTF-8. There will be Chinese characters in here.
<!DOCTYPE html>
<html>
  <head>
    <title>Year of the Horse</title>
     <meta charset="utf-8">
  </head>

  <body>
    <svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">

    </svg>
  </body>
</html>


We want to place horse00.png right here, around the bottom middle.
<!DOCTYPE html>
<html>
  <head>
    <title>Year of the Horse</title>
    <meta charset="utf-8">
  </head>

  <body>
    <svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">
     <image href="horse00.png" x="250" y="300" width="100" height="55">

     </image>

    </svg>
  </body>
</html>


See this?


Now place the rest of the PNG files, all overlapping one another.
<!DOCTYPE html>
<html>
  <head>
    <title>Year of the Horse</title>
    <meta charset="utf-8">
  </head>

  <body>
    <svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">
     <image href="horse00.png" x="250" y="300" width="100" height="55">

     </image>

     <image href="horse01.png" x="250" y="300" width="100" height="55">

     </image>

     <image href="horse02.png" x="250" y="300" width="100" height="55">

     </image>

     <image href="horse03.png" x="250" y="300" width="100" height="55">

     </image>

    </svg>
  </body>
</html>


It's going to look a right mess, until...


...you set opacity to 0! At that point, the horses will all disappear.
<!DOCTYPE html>
<html>
  <head>
    <title>Year of the Horse</title>
    <meta charset="utf-8">
  </head>

  <body>
    <svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">
     <image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">

     </image>

     <image href="horse01.png" x="250" y="300" width="100" height="55" opacity="0">
  
     </image>

     <image href="horse02.png" x="250" y="300" width="100" height="55" opacity="0">
  
     </image>

     <image href="horse03.png" x="250" y="300" width="100" height="55" opacity="0">

     </image>
    </svg>
  </body>
</html>


We'll then animate the first image with this. This animation has the id horse0, which we'll need for referencing later. We set attributeName to opacity because that's what we'll be animating. It goes from 0.8 to 1, as you can see from the from and to attributes, with a very short duration of 0.1 seconds. It begins as soon as the SVG loads, so begin is set to 0 seconds. And lastly, we set repeatCount to 1. In theory, it should only execute once.
<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse0"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="0s"
   repeatCount="1"
  />

</image>


For the next one, we have horse1. It is identical to horse0, except that it begins only when horse0 ends.
<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse0"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="0s"
   repeatCount="1"
  />
</image>

<image href="horse01.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse1"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse0.end"
   repeatCount="1"
  />

</image>


And so on, and so for.
<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse0"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="0s"
   repeatCount="1"
  />
</image>

<image href="horse01.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse1"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse0.end"
   repeatCount="1"
  />
</image>

<image href="horse02.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse2"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse1.end"
   repeatCount="1"
  />

</image>

<image href="horse03.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse3"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse2.end"
   repeatCount="1"
  />

</image>


Of course, you'll want the reverse animation to make the previous frame disappear. These ones won't require an id. Now when you refresh your page, you'll see the horse animate... but only once.
<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse0"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="0s"
   repeatCount="1"
  />

  <animate
   attributeName="opacity"
   from="0.5" to="0"
   dur="0.1s"
   begin="horse0.end"
   repeatCount="1"
  />

</image>

<image href="horse01.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse1"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse0.end"
   repeatCount="1"
  />

  <animate
   attributeName="opacity"
   from="0.5" to="0"
   dur="0.1s"
   begin="horse1.end"
   repeatCount="1"
  />

</image>

<image href="horse02.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse2"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse1.end"
   repeatCount="1"
  />

  <animate
   attributeName="opacity"
   from="0.5" to="0"
   dur="0.1s"
   begin="horse2.end"
   repeatCount="1"
  />

</image>

<image href="horse03.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse3"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="horse2.end"
   repeatCount="1"
  />

  <animate
   attributeName="opacity"
   from="0.5" to="0"
   dur="0.1s"
   begin="horse3.end"
   repeatCount="1"
  />

</image>


In here, you'll need an additional trigger. horse0 should run once when the SVG loads... and also when horse3 ends. This will in turn trigger the rest of the animations in an infinite loop!
<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">
  <animate
   id="horse0"
   attributeName="opacity"
   from="0.8" to="1"
   dur="0.1s"
   begin="0s;horse3.end"
   repeatCount="1"
  />

  <animate
   attributeName="opacity"
   from="0.5" to="0"
   dur="0.1s"
   begin="horse0.end"
   repeatCount="1"
  />

</image>


Because of the reverse animations we added, you can see a shadowy effect of the horse in the animation! Without it, the animation would look a lot jerkier.



Excellent! We have a galloping horse. What next?

We'll do some easy parts first. Let's have some text. It's a simple text tag where I have some Chinese New Year greeting in Chinese text, in orange fill and red outline. In English, it means "Teochew Thunder wishes all the spirit of dragons and horses!"
<svg width="600" height="400" style="background-color:rgb(100,200,0)" viewBox="0 0 600 400" xmlns="www.w3.org">
  <text x="300" y="30" text-anchor="middle" font-size="30px" fill="rgb(250, 100, 0)" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2">潮州雷祝大家龙马精神!</text>

  <image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">
    <animate
      id="horse0"
      attributeName="opacity"
      from="0.8" to="1"
      dur="0.1s"
      begin="0s;horse3.end"
      repeatCount="1"
    />

    <animate
      attributeName="opacity"
      from="0.5" to="0"
      dur="0.1s"
      begin="horse0.end"
      repeatCount="1"
    />
  </image>


Cool, right?


Then four red rectangles on each of the corners of the SVGs, leaving a 10 pixel border of space.
<text x="300" y="30" text-anchor="middle" font-size="30px" fill="rgb(250, 100, 0)" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2">潮州雷祝大家龙马精神!</text>

<rect fill="none" x="10" y="10" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<rect fill="none" x="570" y="10" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<rect fill="none" x="10" y="370" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<rect fill="none" x="570" y="370" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />


<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">


See this?


And then lines to connect the rectangles.
<text x="300" y="30" text-anchor="middle" font-size="30px" fill="rgb(250, 100, 0)" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2">潮州雷祝大家龙马精神!</text>

<line x1="20" y1="20" x2="100" y2="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<line x1="500" y1="20" x2="580" y2="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<line x1="20" y1="20" x2="20" y2="380" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<line x1="580" y1="20" x2="580" y2="380" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<line x1="20" y1="380" x2="580" y2="380" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />

<rect fill="none" x="10" y="10" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<rect fill="none" x="570" y="10" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<rect fill="none" x="10" y="370" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />
<rect fill="none" x="570" y="370" width="20" height="20" stroke="rgba(250, 0, 0, 0.8)" stroke-width="2" />

<image href="horse00.png" x="250" y="300" width="100" height="55" opacity="0">


It's actually pretty simple, but the effect is that of a horse galloping in a frame.


That's it for now!

This is actually a good stopping point if it's enough for you. You just need to change the ugly green background to something else. But we're going to seriously improve on this...

Next

Adding a changing sky background.

Monday, 9 February 2026

Web Tutorial: The Valentine's Day Ribbon

It's that time of the year, lovebirds! In a few days, Valentine's Day approaches! Granted, this year's Valentine's Day offering looks rather sparing in effort, but I assure you it was a pain in the ass to make.

I present... the animated Valentine's Day Ribbon SVG. I actually created it with the help of the SVG Maker we made last month, but we don't have to use that if we don't want to. This little SVG can be scaled up or down, and placed anywhere you want!

Here's the file to test the SVG on. We'll link to vday2026.svg, and view it at different scales. The page has a black background, but feel free to test with different background colors too.

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Vday 2026 test</title>
  </head>

  <body style="background-color:black">
    <img src="vday2026.svg" />
    <img src="vday2026.svg" width="50" />
    <img src="vday2026.svg" width="500" />
  </body>
</html>


And here we begin with the standard SVG markup. We'll create it as a 300 by 300 pixel image.

vday2026.svg
<svg version="1.1" width="300px" height="300px" xmlns="http://www.w3.org/2000/svg">

</svg>


We will start off by defining some colors. This is the background that we will call valGradient. It's a linear gradient that goes from red to pink.

vday2026.svg
<svg version="1.1" width="300px" height="300px" xmlns="http://www.w3.org/2000/svg">
    <defs>
       <linearGradient id="valGradient" x1="0%" y1="0%" x2="0%" y2="100%">
           <stop offset="0%" stop-color="rgb(200, 0, 0)">

           </stop>

           <stop offset="100%" stop-color="rgb(250, 200, 200)">

           </stop>

       </linearGradient>
    </defs>
</svg>


valGradient2 is the exact opposite, going from pink to red.

vday2026.svg
<svg version="1.1" width="300px" height="300px" xmlns="http://www.w3.org/2000/svg">
    <defs>
       <linearGradient id="valGradient" x1="0%" y1="0%" x2="0%" y2="100%">
           <stop offset="0%" stop-color="rgb(200, 0, 0)">

           </stop>

           <stop offset="100%" stop-color="rgb(250, 200, 200)">

           </stop>
       </linearGradient>

       <linearGradient id="valGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
           <stop offset="0%" stop-color="rgb(250, 200, 200)">

           </stop>

           <stop offset="100%" stop-color="rgb(200, 0, 0)">

           </stop>
       </linearGradient>

    </defs>
</svg>


Now that we have our colors, let's start drawing the ribbon. We set the outline to a thick orange line so that we can see what we're doing.

vday2026.svg
<svg version="1.1" width="300px" height="300px" xmlns="http://www.w3.org/2000/svg">
<defs>
   <linearGradient id="valGradient" x1="0%" y1="0%" x2="0%" y2="100%">
       <stop offset="0%" stop-color="rgb(200, 0, 0)">

       </stop>

       <stop offset="100%" stop-color="rgb(250, 200, 200)">

       </stop>
   </linearGradient>

   <linearGradient id="valGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
       <stop offset="0%" stop-color="rgb(250, 200, 200)">

       </stop>

       <stop offset="100%" stop-color="rgb(200, 0, 0)">

       </stop>
   </linearGradient>
</defs>

  <path
  fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />

</svg>


The path's d attribute starts with an M directive to move to point (150, 50). The Z directive is to return to point of origin, which is (150, 50).

vday2026.svg
<path d="M150 50 Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


This basically means to move to point (125, 30) but curve towards (140, 25).

vday2026.svg
<path d="M150 50
Q140 25 125 30
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


Let's apply a second curve so that the path should form an "n" shape, like a hill.

vday2026.svg
<path d="M150 50
Q140 25 125 30
Q100 43 100 50
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


That's what we should have so far. This is the top left part of the "heart". You should see that we have three of these shapes. The leftmost one is the default size, the middle one is scaled down and the rightmost one is enlarged.


Here's the rest of the points that make up the entire left side of the "heart".

vday2026.svg
<path d="M150 50
Q140 25 125 30
Q100 43 100 50
Q90 60 100 80
Q130 100 135 115
L140 110
Q130 90 110 80
Q95 60 110 50
Q130 20 150 60

Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


Coming along nicely. Bear in mind this is not meant to be perfect, but look kind of organic, if that makes sense.


Without belaboring the point, this is the entire first path.

vday2026.svg
<path d="M150 50
Q140 25 125 30
Q100 43 100 50
Q90 60 100 80
Q130 100 135 115
L140 110
Q130 90 110 80
Q95 60 110 50
Q130 20 150 60
Q190 20 200 60
Q200 90 160 100
Q110 130 140 175
L145 170
Q120 140 155 110
Q190 100 205 70
Q205 5 150 50

Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


I think it's coming along quite nicely, wouldn't you say?


Here is the next path. It's a lot shorter.

vday2026.svg
<path d="M150 50
Q140 25 125 30
Q100 43 100 50
Q90 60 100 80
Q130 100 135 115
L140 110
Q130 90 110 80
Q95 60 110 50
Q130 20 150 60
Q190 20 200 60
Q200 90 160 100
Q110 130 140 175
L145 170
Q120 140 155 110
Q190 100 205 70
Q205 5 150 50
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />

<path d="M150 120
L145 130
Q170 150 135 185
Q120 200 120 250
L125 230
L135 250
Q120 200 145 185
Q170 150 150 120
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


...yep.


This last one is the shortest of all.

vday2026.svg
<path d="M150 120
L145 130
Q170 150 135 185
Q120 200 120 250
L125 230
L135 250
Q120 200 145 185
Q170 150 150 120
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />

<path d="M155 180
L150 185
Q170 200 160 250
L165 240
L170 250
Q170 190 155 180
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="2" />


And that's all the paths. But this is visually a little weird. The shading has no continuity.


We rectify this by using the reverse gradient, valGradient2. for the last two paths.

vday2026.svg
<path d="M150 120
L145 130
Q170 150 135 185
Q120 200 120 250
L125 230
L135 250
Q120 200 145 185
Q170 150 150 120
Z"
fill="url(#valGradient2)" stroke="rgb(250,100,0)" stroke-width="2" />

<path d="M155 180
L150 185
Q170 200 160 250
L165 240
L170 250
Q170 190 155 180
Z"
fill="url(#valGradient2)" stroke="rgb(250,100,0)" stroke-width="2" />


Looks good now! Or, at least, less weird.


Now let's just remove the outlines.

vday2026.svg
<path d="M150 50
Q140 25 125 30
Q100 43 100 50
Q90 60 100 80
Q130 100 135 115
L140 110
Q130 90 110 80
Q95 60 110 50
Q130 20 150 60
Q190 20 200 60
Q200 90 160 100
Q110 130 140 175
L145 170
Q120 140 155 110
Q190 100 205 70
Q205 5 150 50
Z"
fill="url(#valGradient)" stroke="rgb(250,100,0)" stroke-width="0" />

<path d="M150 120
L145 130
Q170 150 135 185
Q120 200 120 250
L125 230
L135 250
Q120 200 145 185
Q170 150 150 120
Z"
fill="url(#valGradient2)" stroke="rgb(250,100,0)" stroke-width="0" />

<path d="M155 180
L150 185
Q170 200 160 250
L165 240
L170 250
Q170 190 155 180
Z"
fill="url(#valGradient2)" stroke="rgb(250,100,0)" stroke-width="0" />


So far so good...


Now what we need to do is animate the gradients. For valGradient, the first color, as defined by the attribute stop-color, is red. We use the animate tag to animate the stop-color attribute, making it go from red to pink, and red again. repeatCount is set to indefinite to make it go on forever. The dur attribute is set to 4 seconds, though you can certainly vary this if you want.

vday2026.svg
<linearGradient id="valGradient" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%" stop-color="rgb(200, 0, 0)">
        <animate attributeName="stop-color" values="rgb(200, 0, 0);rgb(250, 200, 200);rgb(200, 0, 0)" dur="4s" repeatCount="indefinite" />
    </stop>

    <stop offset="100%" stop-color="rgb(250, 200, 200)">

    </stop>
</linearGradient>


For the second color, we do the reverse. It starts off pink, so we make it go red, then pink again.

vday2026.svg
<linearGradient id="valGradient" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%" stop-color="rgb(200, 0, 0)">
       <animate attributeName="stop-color" values="rgb(200, 0, 0);rgb(250, 200, 200);rgb(200, 0, 0)" dur="4s" repeatCount="indefinite" />
    </stop>

    <stop offset="100%" stop-color="rgb(250, 200, 200)">
        <animate attributeName="stop-color" values="rgb(250, 200, 200);rgb(200, 0, 0);rgb(250, 200, 200)" dur="4s" repeatCount="indefinite" />
    </stop>
</linearGradient>


For valGradient2, we repeat, but in reverse!

vday2026.svg
<defs>
  <linearGradient id="valGradient" x1="0%" y1="0%" x2="0%" y2="100%">
       <stop offset="0%" stop-color="rgb(200, 0, 0)">
          <animate attributeName="stop-color" values="rgb(200, 0, 0);rgb(250, 200, 200);rgb(200, 0, 0)" dur="4s" repeatCount="indefinite" />
       </stop>

       <stop offset="100%" stop-color="rgb(250, 200, 200)">
          <animate attributeName="stop-color" values="rgb(250, 200, 200);rgb(200, 0, 0);rgb(250, 200, 200)" dur="4s" repeatCount="indefinite" />
       </stop>
  </linearGradient>

  <linearGradient id="valGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
       <stop offset="0%" stop-color="rgb(250, 200, 200)">
           <animate attributeName="stop-color" values="rgb(250, 200, 200);rgb(200, 0, 0);rgb(250, 200, 200)" dur="4s" repeatCount="indefinite" />
       </stop>

       <stop offset="100%" stop-color="rgb(200, 0, 0)">
           <animate attributeName="stop-color" values="rgb(200, 0, 0);rgb(250, 200, 200);rgb(200, 0, 0)" dur="4s" repeatCount="indefinite" />
       </stop>
  </linearGradient>
</defs>


The romantic conclusion

This is a very small web tutorial, mostly because I didn't see the point of charting out every single point in the path. Still, I hope this illustrates why I enjoy SVGs so much. Even with just a small subset of all the features available, there's plenty that can be done.

Yours pathologically,
T___T