使用 DOMMatrix 绘制一个星星 Drawing a star with DOMMatrix
最近我录制了一集关于 DOMPoint
和 DOMMatrix
的 HTTP 203 视频。如果你更喜欢观看视频版本,可以点击这里[1],但是请回到这里获取一些关于我犯了一个愚蠢错误的额外细节,而我几乎没有被发现。
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
允许你对 DOMPoints
应用变换。我发现这些 API 在绘制形状和处理变换结果时非常有用,而不会在 DOM 中引起完整的布局。
DOMMatrix
lets you apply transformations toDOMPoints
. 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
是的!很令人兴奋,对吧?好的,也许 DOMPoint
单独来看并不那么有趣,所以让我们介绍一下 DOMMatrix
:
Yeah! Exciting right? Ok, maybe
DOMPoint
isn't interesting on its own, so let's bring inDOMMatrix
:
const matrix = new DOMMatrix('translate(10px, 15px)').scale(2); console.log(matrix.a); // 2;
DOMMatrix
允许你创建一个矩阵,可以选择从 CSS 变换中创建,并对其执行额外的变换操作。每个变换都会创建一个新的 DOMMatrix
,但也有其他可以直接修改矩阵的方法[2],比如 scaleSelf()
。
DOMMatrix
lets you create a matrix, optionally from a CSS transform, and perform additional transforms on it. Each transform creates a newDOMMatrix
, but there are additional methods that mutate the matrix, such asscaleSelf()
.
当你结合使用 DOMMatrix
和 DOMPoint
时,事情变得有趣起来:
Things start to get fun when you combine
DOMMatrix
and `DOMPoint:
const newPoint = matrix.transformPoint(point);
我曾经在 Squoosh[3] 上使用这个方法来绘制 blob 形状,最近我还用它来绘制一个星星。
I used this to draw the blobs on Squoosh, but even more recently I used it to draw a star.
我需要一个中心点位于确切中心的星星。我可以下载一个星星图案并检查中心点,但为什么不自己绘制呢?星星只是一个有尖角的圆形,对吧?不幸的是,我不能记住这种类型的数学公式,但这并不重要,因为我可以让 DOMMatrix
来帮我完成。
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 }), );
我正在使用 Array.from
创建和初始化一个数组。我希望有一种更友好的方式来做这个[4]。
I'm using
Array.from
to create and initialise an array. I wish there was a friendlier way to do this.
一个典型的星星有 10 个点 - 5 个外部点和 5 个内部点,但我觉得允许其他类型的星星也是不错的。
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.
矩阵只有设置尺寸和位置的变换,所以它只会返回一堆以 x, y
为坐标的点。
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
.
无论如何,这不会阻止我。我将在下面的 <svg>
元素中绘制它:
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 }), );
我将每个点按照 360 度的一部分进行旋转。因此,第一个点旋转了 0/10 的 360 度,第二个点旋转了 1/10 的 360 度,然后是 2/10,以此类推。
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!
当我为《HTTP 203》节目准备幻灯片时,我意识到 points
参数不太对。它允许你像这样使用:
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:
这...不是一个星星。要创建一个有效的星星,点的数量必须是偶数。此外,到目前为止,我们创建的十个点的形状通常称为“五角星”,所以我改变了 API 以符合这种风格:
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 }), );
我快速测试了一下代码,看起来没问题。但是...它还不太对。你能发现我引入的 bug 吗?我直到 Dillon Pentz 在 Twitter 上指出才注意到[5]:
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[5]:
等等...如果数组循环中的索引满足
0 <= i < points * 2
,那么通过 points 进行除法如何得到正确的星星呢?这样不是会围绕圆圈旋转两次吗? Wait… If the index in the array from loop is0 <= i < points * 2
, how does it produce the correct star when dividing by points? Doesn't that rotate around the circle twice?
他是对的!我在数组长度中正确地乘以了 points
,但在 rotate
中忘记了这样做。我没有注意到它,因为对于奇数点的星星,它创建了一个几乎相同的形状。
And, he's right! I correctly multiply
points
for the array length, but I forgot to do it for therotate
. I didn't notice it, because for stars with odd-numbered points, it creates a shape that's almost identical.
上面的结果是我想要的。拖动滑块来查看代码实际上是如何工作的。
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.