Site icon API Security Blog

Drawing a star with DOMMatrix

I recently recorded an episode of HTTP 203 on `DOMPoint` and `DOMMatrix`. If you’d rather watch the video version, [here it is](), but come back here for some bonus details on a silly mistake I made, which I almost got away with.

`DOMMatrix` lets you apply transformations to `DOMPoint`s. I find these APIs handy for drawing shapes, and working with the result of transforms without causing full layouts in the DOM.

## DOMPoint

Here’s `DOMPoint`:

const point = new DOMPoint(10, 15);
console.log(point.x); // 10
console.log(point.y); // 15

Yeah! Exciting right? Ok, maybe `DOMPoint` isn’t interesting on its own, so let’s bring in `DOMMatrix`:

## DOMMatrix

const matrix = new DOMMatrix(‘translate(10px, 15px)’).scale(2);
console.log(matrix.a); // 2;

`DOMMatrix` lets you create a matrix, optionally from a CSS transform, and perform additional transforms on it. Each transform creates a new `DOMMatrix`, but there are [additional methods that mutate the matrix](), such as `scaleSelf()`.

Things start to get fun when you combine `DOMMatrix` and `DOMPoint`:

const newPoint = matrix.transformPoint(point);

I used this to draw the [blobs on Squoosh](), but even more recently I used it to draw a star.

## Drawing a star

I needed a star where the center was in the exact center. I could download one and check the center point, but why not draw it myself? A star’s just a spiky circle right? Unfortunately I can’t remember the maths for this type of thing, but that doesn’t matter, because I can get `DOMMatrix` to do it for me.

const createStar = ({ points = 10, x = 0, y = 0, size = 1 }) =>
Array.from({ length: points }, (_, i) =>
new DOMMatrix()
.translate(x, y)
.scale(size)
.transformPoint({ x: 0, y: 0 }),
);

I’m using `Array.from` to create and initialise an array. [I wish there was a friendlier way to do this]().

A typical star has 10 points – 5 outer points and 5 inner points, but I figured it’d be nice to allow other kinds of stars.

The matrix only has transforms set to apply the size and position, so it’s just going to return a bunch of points at `x, y`.

Anyway, I’m not going to let that stop me. I’m going to draw it in an “ element below:

const starPoints = createStar({ x: 50, y: 50, size: 23 });
const starPath = document.querySelector(‘.star-path’);
starPath.setAttribute(
‘d’,
// SVG path syntax
`M ${starPoints.map((point) => `${point.x} ${point.y}`).join(‘, ‘)} z`,
);

And here’s the result:

So, err, that’s 10 points on top of each other. Not exactly a star. Ok, next step:

const createStar = ({ points = 10, x = 0, y = 0, size = 1 }) =>
Array.from({ length: points }, (_, i) =>
new DOMMatrix()
.translate(x, y)
.scale(size)
// Here’s the new bit!
.translate(0, -1)
.transformPoint({ x: 0, y: 0 }),
);

And the result:

Use the slider to transition between the previous and new state. I’m sure you’ll agree it was worth making this interactive.

Ok, so all the points are still on stop of each other. Let’s fix that:

const createStar = ({ points = 10, x = 0, y = 0, size = 1 }) =>
Array.from({ length: points }, (_, i) =>
new DOMMatrix()
.translate(x, y)
.scale(size)
// Here’s the new bit!
.rotate((i / points) * 360)
.translate(0, -1)
.transformPoint({ x: 0, y: 0 }),
);

I’m rotating each point by a fraction of 360 degrees. So the first point is rotated 0/10ths of 360 degrees, the second is rotated 1/10th, then 2/10ths and so on.

Here’s the result:

Now we have a shape! It’s not a star, but we’re getting somewhere.

To finish it off, move some of the points outward:

const createStar = ({ points = 10, x = 0, y = 0, size = 1 }) =>
Array.from({ length: points }, (_, i) =>
new DOMMatrix()
.translate(x, y)
.scale(size)
.rotate((i / points) * 360)
// Here’s the new bit!
.translate(0, i % 2 ? -1 : -2)
.transformPoint({ x: 0, y: 0 }),
);

Here’s the result:

And that’s a star!

## But then I messed it up

As I was getting the slides together for the HTTP 203 episode, I realised that the `points` argument wasn’t quite right. It lets you do something like this:

const starPoints = createStar({ points: 9, x: 50, y: 50, size: 23 });

Which looks like this:

Which… isn’t a star. The number of points has to be even to create a valid star. Besides, the ten-pointed shape we’ve been creating so far is typically called a “five-pointed star”, so I changed the API to work in that style:

const createStar = ({ points = 5, x = 0, y = 0, size = 1 }) =>
Array.from({ length: points * 2 }, (_, i) =>
new DOMMatrix()
.translate(x, y)
.scale(size)
.rotate((i / points) * 360)
.translate(0, i % 2 ? -1 : -2)
.transformPoint({ x: 0, y: 0 }),
);

I quickly tested the code, and it looked fine. But… it’s not quite right. Can you see the bug I’ve introduced? I didn’t notice it until [Dillon Pentz pointed it out on Twitter]():

> Wait… If the index in the array from loop is `0 {
const length = points * 2;

return Array.from({ length }, (_, i) =>
new DOMMatrix()
.translate(x, y)
.scale(size)
.rotate((i / length) * 360)
.translate(0, i % 2 ? -1 : -2)
.transformPoint({ x: 0, y: 0 }),
);
};

I guess the moral of the story is: Don’t change slides at the last minute.Read More

Exit mobile version