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

No comments:

Post a Comment