Blog

SVG in Svelte's world

SVG in Svelte's worldIt's not the destination, it's the journey.
6 min read - 13.01.2020

We already know the strength of svelte, of fully reactive small bundles and fast code. And although it can do a lot, like SSR with Sapper or mobile with Svelte Native. I am most excited about possibilities to use it for SVG and canvas manipulation. For example, to animate an SVG stroke, we just need to call a draw function from Svelte animate module. Also, the idea of using it with Three.js or Babylon with project Svelte Gl is awesome and I am really hopeful for the project.

1import { draw } from 'svelte/transition';
2import { quintOut } from 'svelte/easing';
3
4<svg viewBox="0 0 5 5" xmlns="http://www.w3.org/2000/svg">
5 {#if condition}
6 <path transition:draw="{{duration: 5000, delay: 500, easing: quintOut}}"
7 d="M2 1 h1 v1 h1 v1 h-1 v1 h-1 v-1 h-1 v-1 h1 z"
8 fill="none"
9 stroke="cornflowerblue"
10 stroke-width="0.1px"
11 stroke-linejoin="round"
12 />
13 {/if}
14</svg>

Manipulating SVG can be a tedious process. That is why we use libraries like D3 to help us out, this is not a replacement for D3, quite the opposite, Svelte with D3 is a really powerful combination. I hope to write an article about it in the future. In this article, we will explore how we can use Svelte to draw SVG patters, animate and morph path. So let's begin.

Components

In the first example, we will be creating a Checkerboard SVG component. We will use loops to create pattern of black and white squares.

Checker Board

In this example, we create squares and with properties that we can define from the parent component. From squares property using reactive declaration, to be able to change squares number from parent dynamically, we generate array of in this example 8 items. This will represent a number of squares on both x and y coordinates.

1export let squares = 8;
2export let width = 400;
3
4$: fields = [...Array(squares)];
5
6function getEven(i, j) {
7 return ((i + j) % 2) === 0;
8}

We can see how fields array can be useful, as we are looping twice through an array to generate a 2d matrix grid of squares. On a viewbox property, we add 1 to squares number to create a border, We also use getEven function for alternating between white and black squares.

1<svg width="{width}px" height="{width}px" viewbox="0 0 {squares + 1} {squares + 1}">
2 <rect class="board" width="100%" height="100%" fill="orange"/>
3 {#each fields as item, i}
4 {#each fields as item, j}
5 <rect
6 class={getEven(i, j) ? 'white' : 'black'}
7 x={i + 0.5}
8 y={j + 0.5}
9 width="1"
10 height="1"
11 />
12 {/each}
13 {/each}
14</svg>

In the end, all we need to do is to instantiate the component.

1<script>
2 import Chessboard from './Chessboard.svelte';
3</script>
4
5<Chessboard squares={8} />

Animating

Svelte compiles all files into one. So we can mix the Svelte components inside SVG tags. That gives us options to separate parts of SVG. In this example, we will create a car driving on the road.

There are 2 SVG components CarSymbols that have car SVG code and RoadPattern with 4 properties, that we can change. Id, width, and height are self-explanatory, while patternTransform is used to animate road. patternTransform is a transform property for pattern tag in SVG, accepts the same as transform property od SVG or style.

1export let patternTransform = '';
2export let id = 'road';
3export let width = '10%';
4export let height = '100%';

In the App file, we import both components and use request animation frame to loop. To create an animation we just pass changed transform value to the child component of the road pattern to update position of the pattern. Working example

1function drive() {
2 if(carDriving) {
3 miles = miles + 1;
4 setTimeout(() => {
5 window.requestAnimationFrame(drive);
6 }, 50);
7 }
8}

SVG Path

If we want to morph SVG paths, and we don't need a full animation library like AnimeJs or GSAP that have their own plugins for manipulation paths. We can use a morphing library like Flubber or Polymorph which are specialized in path manipulation. Here is an example of how to use Polymorph within Svelte.

We use the interpolate function from Polymorph, This function takes 2 paths, and returns a function that takes a number between 0 and 1 and returns a timeline. The number represents a position on the timeline, example 0.5 is 50% point. This is perfect to combine with tweened function from Svelte as it interpolates between 2 numbers on some duration. So we use it to animate number from 0 to 1, that we inject in Polymorph timeline.

1const progress = tweened(0, {
2 duration: 600,
3 easing: cubicOut
4});
5
6onMount(() => {
7 morph = interpolate(["#circle", "#square"], { precision: 1 });
8 morphMouth = interpolate(["#circle", "#smile"], { precision: 1 });
9});

Interpolate function needs to be set after Dom has rendered, so we add it to onMount function. Next, all we need to do is add it to the path. As tweened function returns a store, we basically subscribe to it, and tweened will rerender component when data changes. Now every time we change values to the in our example variable progress, it will set morphing animation.

1<svg viewbox="0 0 50 50" fill="#FBB43E">
2 <defs>
3 <path id="circle" d="M 0 25 a 25,25 0 1,0 50,0 a 25,25 0 1,0 -50,0" />
4 <path id="square" d="M 0 0, V 50, H 50 V 0 Z" />
5 <path id="smile" d="M20 38.1883C21.4487 39.6436 23.4541 40.5444 25.6698 40.5444C27.9796 40.5444 30.0607 39.5655 31.521 38C29.5403 38.3522 27.4012 38.5444 25.1698 38.5444C23.3754 38.5444 21.6407 38.4201 20 38.1883Z" />
6 </defs>
7 <path d="{(morph) && morph($progress)}" />
8 <g fill="#FDF3C2">
9 <path d="{(morphMouth) && morphMouth($progress)}" />
10 </g>
11</svg>

If you combine all the techniques above, you can easily create raining animation like in bottom example.

Although these were simple examples, this can be a powerful combination for creating feature-rich interactive UX. In future articles, I will try to go into other powerful combination for data visualization with Svelte and D3 library.

That is all for now and until next time, happy coding.