A star is born

On a cold, slightly depressing evening I opened up a Svelte REPL and created a small SVG marvel. Here’s the syntax in its entirety.

<svg viewBox="-23.204545974731445 -24 45.409088134765625 43.56756591796875">
	<path
		fill="#ffdb47"
		d="M 0 15 Q -21 28 -15 4 Q -34 -11 -9 -13 Q -1 -35 8 -13 Q 33 -11 14 4 Q 20 28 0 15"
	/>
</svg>

And here’s how I came up with the numbers, decimals included. Be sure, there’s intent behind the values, and little left to random chance. And fret not, your patience will be rewarded with the colorful visual by the end of the post.

SVG canvas

Indubitably, I did not come up with the viewBox attribute from the get go. The journey begins instead with a squared canvas, centering the upcoming work of art in a comfortably large space.

<svg viewBox="-50 -50 100 100">
	<!--  -->
</svg>

Circle points

To draw a round shape with SVG you have a few options. Among which:

  • the <circle> element

    <circle r="35" />
  • a <path> element and the arc command

    <path d="M 0 -35 a 35 35 0 0 0 0 70 35 35 0 0 0 0 -70" />

Either approach leads you to a perfectly crisp circle.

The second option, however, opens a world of possibilities. The arc syntax conveniently creates the swooping curves, but with multiple points you can draw the shape connecting the dots, literally.

In a <script> initialize an array for the magic numbers.

const n = 10;
const points = Array(n);

In the map function tap into two values: the index and the total number of points.

const points = Array(n)
	.fill()
	.map((_, i, { length }) => {
		//
	});

The goal is to find an angle in the [0, 360] range and move between polar and cartesian coordinates. You walk around the center to compute the coordinates with Math.cos and Math.sin.

Find the angle through the incrementing index and the length of the array. You need the value in radians, meaning you either explore the [0, Math.PI * 2] interval or indulge in a quick conversion.

const angle = (360 / length) * i;
const radians = (angle / 180) * Math.PI;

Calculate the coordinates with an arbitrary offset. 35 to match the radius of the previous circle.

const x = Math.cos(radians) * 35;
const y = Math.sin(radians) * 35;

Return the values with the array, so that points resolves to be an array of [x, y] coordinates.

return [x, y];

In the <svg> element draw a small dot for every single point.

{#each points as [x, y]}
	<circle r="2" cx={x} cy={y} />
{/each}

You can crank up the number of points to find a more believable outline, but let’s explore a different route linking the points together.

A small adjustment first. Round down the numbers found with the infamous trigonometric functions. We don’t need to be that precise.

const x = Math.floor(Math.cos(radians) * 35);
const y = Math.floor(Math.sin(radians) * 35);

Line points

Once again you have several options. The most immediate relates to the <polyline> element and its compulsory points attribute.

<polyline points={points.join(' ')} />

I join the coordinates to have a string of values, x0 y0 x1 y1 ... x9 y9, but it seems the markup is robust enough to handle the nested arrays as-is.

<polyline points={points.join(' ')} />
<!-- <polyline {points} /> -->

As an alternative, prefer the flexible <path> element and the often cryptic d attribute. Here you need to instruct the browser with a series of commands, starting with the M character for the origin.

<path d="M " />

Continue with the sequence of points.

<path d="M {points.join(' ')}" />
<!-- <path d="M {points}" /> -->

Both lines lead to a filled, rugged visual, tracing the outline of the first circle. That being said, you now have access to the individual points.

Offset values

Instead of one arbitrary offset, rely on two values, alternating between 15 and the now-familiar 35 distance.

const offset = i % 2 === 0 ? 15 : 35;
const x = Math.floor(Math.cos(radians) * offset);
const y = Math.floor(Math.sin(radians) * offset);

At once, the title of this article starts to make sense.

The shape is tilted on its side, and not by accident. Navigating the [0, 360] range you start from the right, from the positive x axis, and walk around the circle in a clockwise manner. To have the leftmost edge at the very top offset the angle to move a quarter forward.

-const angle = (360 / length) * i;
+const angle = (360 / length) * i + 90;

Effectively, the angle falls in the [90, 450] range, producing the more orthodox shape.

Bezier curves

Keep drawing the shape with a <path> and you can replace the sharp segments with smooth curves.

In this regard, there are two pertinent commands: Q for a quadratic bezier curve and C for a cubic bezier curve. Both work through the concept of control points, but the latter has actually two of them. To make life easier, we are going to stick with the safer, simpler instruction.

When you specify two points after the M command SVG assumes the L character. The instruction creates a line from the previous coordinate to the new pair.

<svg viewBox="0 0 50 50">
	<g fill="none" stroke="currentColor" stroke-width="0.5">
		<!-- <path d="M 5 25 45 25" /> -->
		<path d="M 5 25 L 45 25" />
	</g>
</svg>

With the Q character you rely on four numbers instead, two for the position of the control point, two for the final position. The path connects the start and end values influenced by the intermediate step.

<!-- Q cx cy x y -->
<path d="M 5 25 Q 25 5 45 25" />

Drag the handles to update the coordinates of the control point.

d="M 5 25 Q 25 5 45 25"

Back to the almost-illuminated object. Let’s build the more complex d attribute in the <script>, one step at a time.

Start with the first point.

let d = `M ${points[0][0]} ${points[0][1]}`;

Continue with the remaining items in the array.

for (let i = 1; i < points.length; i++) {
	const [x, y] = points[i];
}

Past the origin you want to alternate between control points and end coordinates. First, describe the control point.

if (i % 2 !== 0) {
	d += ` Q ${x} ${y}`;
}

In the iteration which follows, complete the instruction with the end point.

if (i % 2 !== 0) {
	d += ` Q ${x} ${y}`;
} else {
	d += ` ${x} ${y}`;
}

Ten points, you skip the first, nine iterations. This means you end with the Q character referring to the last position. To wrap things up, follow the loop with the original pair.

d += ` ${points[0][0]} ${points[0][1]}`;

Inject the string in the <path> element.

<path {d} />

And enjoy a satisfying success.

Canvas size

The arbitrary viewBox finally proves to be excessive. Luckily, the drawing is complete, and the accurate measurements are within reach.

You need access to the actual <svg> element. With Svelte, achieve this in two steps:

  1. associate a variable through the bind:this directive

    <script>
    	let svg;
    </script>
    
    <svg bind:this={svg} viewBox="-50 -50 100 100">
    	<!--  -->
    </svg>
  2. wait for the element to exist pending the onMount lifecycle function

    <script>
    	import { onMount } from 'svelte';
    
    	let svg;
    	onMount(() => {
    		console.log(svg); // <svg viewB....
    	});
    </script>

With the newfound node, the four precious values are one getBBox method away.

const { x, y, width, height } = svg.getBBox();
const viewBox = `${x} ${y} ${width} ${height}`;

Now, for the final set of options. You could explore the clipboard API and fabricate a button to extract the string with a single press. Or, realize it’s way too late, you’re just too tired, and a manual copy-paste will just do.

I won’t judge. The final sight is too delightful to wait further.