0

Drawing a star with DOMMatrix

 1 year ago
source link: https://jakearchibald.com/2022/drawing-a-star/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Drawing a star with DOMMatrix

Posted 30 September 2022

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 DOMPoints. 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 <svg> 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:

10987654321

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:

10987654321

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:

987654321

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 <= i < points * 2, how does it produce the correct star when dividing by points? Doesn't that rotate around the circle twice?

And, they're right! I correctly multiply points for the array length, but I forgot to do it for the rotate. I didn't notice it, because for stars with odd-numbered points, it creates a shape that's almost identical.

12345678910

The above is the result I intended. Drag the slider to see what the code actually does.

Here's a correct implementation:

const createStar = ({ points = 5, x = 0, y = 0, size = 1 }) => {
  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.

View this page on GitHub


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK