back
September 27, 2024

Svelte animations

I build a website recently for work, and as part of that process I got connected to fantastic designer who came up with an awesome vibrant and fun look for the site.

One aspect of her vision was to include lots of fun iconographics and animations, and it turned out she’s skilled at making the required 2D renderings in AfterEffects.

The animations were really cool, but the exported gifs weren’t always as smooth as I would have liked. We achieved a higher quality of visual by switching from gifs to lotties, but I had an feeling I could do the visuals with svg animated by javascript and it would render silky smooth.

rect>

The idea is basically to define a svelte $state() variable containing the timing (how far into a loop we are), and then pass that to child components to compute what they should look like based on their own delay, duration, and shape.

Here’s everything I ended up needed for the animation timing (cribbed from here).

<script>
  import { onMount } from 'svelte';

  let zero
  let time = $state();
  const loop_time = 5_500;

  function firstFrame(timestamp) {
    zero = timestamp;
    animate(timestamp);
  }

  function animate(timestamp) {
    time = timestamp - zero;
    if (time < loop_time) {
      requestAnimationFrame((t) => animate(t));
    } else {
      time = 0;
      setTimeout(() => requestAnimationFrame(firstFrame), 100);
    }
  }

  onMount(() => {
    requestAnimationFrame(firstFrame);
  });
</script>

What I like about the example I’m recreating here is it’s tight timing and sense of rhythm. To achieve that, we just have a Svelte component that receives the time prop from the parent above, and then computes an SVG <path/> that corresponds to the desired effect and timing.

Note the svelte/transition package has draw in it, which achieves much of the same effect by stroke-dash animation--- but I wanted my lines to retain their stroke dashes, and also to build towards more complicated shapes that can’t be animated with this trick.

Once you have the timing, Svelte makes the rest really easy.

The main thing is to compute what fraction of the way through the animation we are based on the timing, delay, and duration.

let fraction = $derived((time - delay) / duration)

We don’t want the fraction to be less than 0 or greater than 1, so we clamp the value to that interval. It would be nice if clamp were part of Math, but since it’s not, we’ll borrow a suggested implementation:

function clamp(num, min, max) {
    return num <= min 
      ? min 
      : num >= max 
        ? max 
        : num
  }

and then we just construct an svg element that uses the fraction to interpolate where it should be

<script>
  const {
    x1,
    y1,
    xto,
    yto,
    delay,
    duration,
    time,
    stroke,
    strokeWidth,
    strokeDasharray
  } = $props();

  let fraction = $derived(clamp((time - delay) / duration, 0, 1))
  let x2 = $derived(x1 + (xto - x1) * fraction)
  let y2 = $derived(y1 + (yto - y1) * fraction)
</script>

<line
  {x1} {y1} {x2} {y2}
  {stroke}
  stroke-dasharray={strokeDasharray}
  stroke-linecap="square"
  stroke-width={strokeWidth}
>
</line>

The colorful lines above are just

<svg viewbox="0 0 600 100" class="w-full mx-auto">
  <Line 
    x1={0} xto={600} y1={40} yto={10} stroke="DodgerBlue" 
    strokeWidth={10} strokeDashArray="0 25 50 0 25 50 25" delay=3000 duration=500 {time}
  />
  <Line 
    x1={0} xto={600} y1={50} yto={50} stroke="Tomato" 
    strokeWidth={20} strokeDashArray="0 25 0 25 0 25" delay=0 duration=3000 {time}
  />
  <Line 
    x1={0} xto={600} y1={20} yto={30} stroke="MediumSeaGreen" 
    strokeWidth={20} strokeDashArray="0 0 50 0 25 50 25" delay=0 duration=4000 {time}
  />
</svg>

The circles are the same deal, mostly. We use trigonometry to get some points on a circle, and then figure out what points are visible based on the timing and draw a path element.

I’m not sure what’s next. It seems like it would be pretty easy to add easing, but I’d also like to explore procedural animation, or maybe incorporating physics. Stay tuned.

🔲