Draw tomatoes with Svelte and SVG
The ease of being creative»
Appologize I couldn’t come up with a better name for this blog post. But what I always wanted to write about is exactly this. **cough**
Of course not, it was more of an accident when I played with my blog setup with SvelteKit, MDSvex and SVG. All started with some lines between points on a circle and I randomly picked red and green as colors to differentiate my drawn entities. I was working on an idea how I to visualize the Collatz row.
Creativity is a phenomenon whereby something somehow new and somehow valuable is formed. (Wikipedia)
It often turns out that limitations lead to even more creativity. So the boundaries of drawing only a circle and three basic curves can already result in so many different shapes. To give you a feeling you can interact with the result.
Presets
{
"radius": 200,
"count": 3,
"distance": 1,
"amount": 1,
"angle": 10,
"delta": 0.5,
"tomato": true
}
Dynamic values»
The circle is divided in segments. The start and end point of each segment are at times of the circles radius and connected with arches. You can change the elevation of the arches, currently at . A value of leads to a line, everything else to a quadratic bezier curve.
The angle where the first segment starts is and changes about degress per frame, a value of will stop the Animation.
Last but not least you can switch off mode. The radius of the tomato is .
Tutorial»
So maybe Tomatoes are not your favourite, but if you’re interested how to come up with such an interactive, animated piece of vector graphic (SVG) using Svelte, you should read on. This is not going to be a tutorial in full detail, but I’ll try to explain some key pieces to help you to get started.
SVG»
SVG means Scalable Vector Graphics and as a first class citizen in HTML is the (defacto) standard for the Web. All major browsers at all major platforms and devices can render and display SVG images or even inline SVG elements these dayss.
The XML Format is a mighty tool. Everything enclosed by an <svg></svg>
tag is your canvas. And the most flexible unit to actually draw is the <path d="" />
Element. When you create some illustration in your preferred application, it has most likely an option to export as SVG. I did and the result is in a quite readable format. For example the following Tomato SVG Image reads like this:
<svg width="500" height="500">
<path d="M474.609,246.875 C480.859,367.188 382.031,468.75 250.781,468.75 C119.531,468.75 22.656,351.172 35.938,231.641 C54.688,63.672 159.475,58.222 250.781,62.5 C323.73,65.918 460.547,14.063 474.609,246.875 z" fill="#FF0000"/>
<path d="M255.987,26.968 C262.1,27.172 266.168,26.376 271.442,29.52 C272.007,29.857 271.942,30.738 272.192,31.348 C270.218,48.21 264.407,63.992 281.372,60.791 C333.13,51.025 383.965,47.009 410.522,81.543 C376.147,62.012 316.284,94.238 316.284,94.238 C316.284,94.238 381.128,75.488 422.534,141.113 C408.639,135.095 393.856,129.663 378.973,132.118 C362.183,134.888 337.696,128.859 320.869,126.421 C341.686,161.936 318.79,184.887 330.317,226.622 C309.252,219.106 314.37,211.251 299.478,183.447 C281.25,149.414 277.344,164.062 246.753,114.55 C245.574,112.642 243.853,118.07 241.96,119.273 C220.322,133.037 217.278,128.165 193.373,135.808 C161.177,146.102 123.225,158.862 103.397,186.03 L103.394,186.035 C106.678,140.899 135.254,116.211 156.738,104.492 C180.131,91.733 193.235,90.892 202.547,89.361 C218.3,91.364 211.818,90.48 222.005,91.908 C186.128,81.869 147.519,74.86 112.183,87.402 C135.121,55.589 179.077,57.129 195.241,55.63 C212.222,54.055 237.985,49.279 248.413,63.477 C257.779,46.577 243.589,49.237 248.022,30.566 C250.636,26.924 248.589,28.997 255.987,26.968" fill="#65CC33"/>
</svg>
This image consists of two shapes, respective path tags, only. Each of these Elements consists of a d
Attribute which describes the path. The description language is simple and consists of commands (M
, C
, etc) and a bunch of coordinates.
For our Tomato we are going to use much more regular pathes which can be calculated mathematically. A circle shape for the fruit and lines or curves for the leafs. So let’s have a look at some functions we will use.
Math»
Working in 2D space we use functions that return an object with x, y coordinates as properties, Points, so to say. To draw the lines/curves, we need to do some calculations. First we find their start end points, using circlePoint
. This method needs to know the center of a circle, its radius and an angle betwee 0 and 360 degrees to calculate the x, y coordinates of a point at this circle. You can apply an additional factor distance
to the radius to get a point in an orbit larger or smaller than the circle itself.
const point = (x, y) => ({x, y});
const circlePoint = (center, radius, angle, distance = 1.0) => point(
center.x + radius * Math.cos(-angle * Math.PI/180) * distance,
center.y + radius * Math.sin(-angle * Math.PI/180) * distance
);
// examples
circlePoint(point(100,100), 80, 0); // {x: 180, y: 100}
circlePoint(point(100,100), 80, 90); // {x: 100, y: 20}
circlePoint(point(100,100), 80, 0, 2); // {x: 260, y: 100}
Because we are drawing quadratic bezier curves between all of these points at the circle, we need to come up with a control point somewhere in the middle of these points. For that we can use a function that is simple but powerful and often used in vector calculations. Its is called lerp
which stand for linear interpolation.
We have to interpolate the half between the x, y coordinates of 2D points. That’s why we are using plerp
.
Because we want a curve, not a line, we need also to interpolate the control point along a line between this midpoint and the center of the circle.
const lerp = (s, e, t) => (1 - t) * s + t * e;
const plerp = (s, e, t) => point(
lerp(s.x, e.x, t),
lerp(s.y, e.y, t),
);
// example
plerp(
point(0, 100),
point(100, 0),
0.5
); // {x: 50, y: 50}
pathes»
Next we need to know how to construct a path description, e.g. to draw a quadratic bezier curve from s(tart) to e(nd) using one (c)ontrol point, we add up a string for a moveTo
and quadTo
command.
We can use simple arrow functions which consume point objects {x: …, y: …}
.
const moveTo = (p) =>
`M ${p.x} ${p.y}`;
const lineTo = (p) =>
`L ${p.x} ${p.y}`;
const quadTo = (c, p) =>
`Q ${c.x} ${c.y} ${p.x} ${p.y}`;
const linePath = (a, b) =>
moveTo(a) + ' ' + lineTo(b);
const quadCurvePath = (s, c, e) =>
moveTo(s) + ' ' + quadTo(c, e);
So now we can draw a single curve, find two points at the circle, construct the control point, and get the description of the curve.
const linePathByAngle = (alpha, beta, center, radius, distance = 1.0, amount = false, quad = true) => {
const a = circlePoint(center, radius, alpha, distance);
const b = circlePoint(center, radius, beta, distance);
const c = amount !== false ? plerp(plerp(a, b, 0.5), center, amount) : center;
return quad
? quadCurvePath(a, c, b)
: linePath(a, b);
};
And then do this multiple times, depending on how much segments you want to split your circle in, e.g. spltting three times.
const linePathBySegment = (seg, numberOfSegments, center, radius, distance = 1.0, amount = false, startAngle = 0) =>
linePathByAngle(
startAngle + seg * 360/numberOfSegments,
startAngle + (seg + 1) * 360/numberOfSegments,
center,
radius,
distance,
amount
);
Reactivity with Svelte»
I skip how to get the path description for the circle, you can look that up later in the result. Lets concentrate on wiring things together. Of course there are mutliple, different ways to animate to scene using SVG only or CSS animations, but we want to have the user interact with it.
So things should react to user changes. Svelte components offer this kind of reactivity (just like React and other frameworks as well). Another term for this is ‘Data-Bindung’, so whenever your data, your component properties change, the visual representation of your component changes as well. I won’t explain the difference between Svelte and React here, both take care of this for us.
const nums = (len) => [...Array(len).keys()];
const center = point(250, 250);
export let radius = 200;
export let count = 3;
export let distance = 1.0;
export let amount = 1.0;
export let angle = 10;
export let delta = 0.5;
export let tomato = true;
$: pathes = nums(count);
So we basically define mutable variables with let
. When we use these in our markup, even as function arguments, Svelte is taking care to (re-)evaluate things and (re-)render our scene or, when possible, update existing DOM elements, only.
A special syntax is the $:
syntax, a ‘named expression’, so valid JS Syntax, but used by the Svelte compiler to mark a statement as reactive.
The SVG markup looks like this:
<svg width="500" height="500">
<path class="circle" d={circlePath(center, radius)}></path>
{#each pathes as path}
<path d={linePathBySegment(path, pathes.length, center, radius, distance, amount, angle)}></path>
{/each}
</svg>
You can see, that the value of the d
-Attribute is the result of our earlier defined functions. And the arguments to these function calls are our components properties. So whenever you change these, the scene changes accordingly.
For demonstration we use a input element (type: range) to render as a slider and use Svelte to bind its value to the count
variable, which ist also responsible how many points at the circle are connected.
count: (3)
<input type="range" min="2" max="10" bind:value={tutCount}>
The last step is Animation. For this I use a markup free component which wraps up a requestAnimationFrane loop. You only have to insert import the component, place it in your markup and write a method which is then repeatedly called executed. In our case it add up delta
to angle
. I put a slider here to let you change delta, so you can see what it does:
delta: (1)
<script>
import Animation from './animation.svelte';
const animate = () => {
const newAngle = angle + delta;
angle = newAngle > 360 ? angle -360 : newAngle < 0 ? newAngle + 360 : newAngle;
};
</script>
<Animation run={animate} />
Feel free to fork»
So if you read this article in full and are now interested in doing something similar on your own, you could take advantage of this code and make a fork from this Svelte Playground.