canvas矩阵变换原理

773 阅读3分钟

课堂目标

  • 理解矩阵变换的概念
  • 可以用不同的变换方式变换物体

知识点

  • ctx.translate()
  • ctx.scale()
  • ctx.rotate()
  • ctx.transform()
  • 矩阵乘法

1-用canvas做个试验

先画这个矩形热热身。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>变换</title>
        <style>
            body {
                margin: 0;
                overflow: hidden;
            }
        </style>
    </head>
    <body>
        <canvas id="canvas"></canvas>
        <script type="module">
            const canvas = document.getElementById('canvas')
            canvas.width = window.innerWidth
            canvas.height = window.innerHeight
            const ctx = canvas.getContext('2d')
              /* 矩形1 */
            ctx.save()
            ctx.fillRect(0, 0,200, 100)
            ctx.restore()
        </script>
    </body>
</html>

效果如下:

image-20230313095632358

这个矩形在浏览器的左上角。

我们把它往右下方移动一下,方便观察。

/* 矩形1 */
ctx.save()
ctx.translate(300, 200)
ctx.fillRect(0, 0, 200, 100)
ctx.restore()

效果如下:

image-20230313100812032

有点呆板,让它旋转30°

/* 矩形1 */
ctx.save()
ctx.translate(300, 200)
ctx.rotate(Math.PI / 6)
ctx.fillRect(0, 0, 200, 100)
ctx.restore()

效果如下:

image-20230313101020594

我再在其本地坐标系的y轴上再做一个缩放:

/* 矩形1 */
ctx.save()
ctx.translate(300, 200)
ctx.rotate(Math.PI / 6)
ctx.scale(1, 2)
ctx.fillRect(0, 0, 200, 100)
ctx.restore()

效果如下:

image-20230313101319036

这些看起来都没啥,很简单。

接下来,我们说重点。

我把rotate和scale的顺序颠倒,再画一个蓝色的矩形。

/* 矩形2 */
ctx.save()
ctx.fillStyle = '#00acec'
ctx.translate(300, 200)
ctx.scale(1, 2)
ctx.rotate(Math.PI / 6)
ctx.fillRect(0, 0, 200, 100)
ctx.restore()

效果如下:

image-20230313095413740

我刚才没有画出蓝色的矩形,而是画出了一个蓝色的平行四边形。

这是为什么呢?

这就是我们本章要讨论的话题了。

2-矩阵乘法

canvas里每一次translate(),rotate()或scale() ,都是独立的矩阵变换,当连续执行这些方法时,就可以视之为矩阵的相乘。

因为矩阵的乘法不符合乘法的交换律,所以当缩放矩阵非等比缩放的时候,(旋转矩阵 * 缩放矩阵)不等于(缩放矩阵 * 旋转矩阵)。

我们用矩阵写一下这个逻辑。

旋转矩阵

设:

  • 旋转矩阵为mr

  • 旋转角度为30°

    • s=sin30°
    • c=cos30°

则:

mr=[
c,-s,0,
s, c,0,
0, 0,1,
]

缩放矩阵

设:

  • 缩放矩阵为ms
  • x,y方向的缩放量为(1,2)

则:

ms=[
1,0,0,
0,2,0,
0,0,1,
]

旋转矩阵*缩放矩阵

将mr的第n行点积ms的第n列。

mr*ms=
[
(c,-s,0)·(1,0,0),(c,-s,0)·(0,2,0),(c,-s,0)·(0,0,1),
(s, c,0)·(1,0,0),(s, c,0)·(0,2,0),(s, c,0)·(0,0,1),
(0, 0,1)·(1,0,0),(0, 0,1)·(0,2,0),(0, 0,1)·(0,0,1),
]
=
[
c,-2*s,0,
s,2*c, 0,
0,0,   1,
]

缩放矩阵*旋转矩阵

ms*mr=[
c,  -s,  0,
2*s,2*c, 0,
0,  0,   1,
]

对比一下两个矩阵相乘的结果,会发现他们是不一样的。

对于这两个结果,我们可以画一下看看。

3-矩阵绘图

我们先看一下旋转矩阵*缩放矩阵的效果。

1.计算一个30°的正弦值和余弦值。

const ang = Math.PI / 6
const [s, c] = [Math.sin(ang), Math.cos(ang)]

2.按照之前旋转矩阵*缩放矩阵的结果声明一个矩阵。

let m = [c, -2 * s, 0, s, 2 * c, 0, 0, 0, 1]

3.使用上面的矩阵绘制一个绿色的矩形。

ctx.save()
ctx.fillStyle = '#acec00'
ctx.translate(300, 200)
ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5])
ctx.fillRect(0, 0, 200, 100)
ctx.restore()

效果如下:

image-20230313113732824

transform()是canvas内置的相对矩阵变换方法,其参数与行主序三阶矩阵的对应关系是[0,3,1,4,2,5],与列主序三阶矩阵的对应关系是[0,1,3,4,6,7]。

此时的transform()方法就相当于画矩形1时的:

ctx.rotate(Math.PI / 6)
ctx.scale(1, 2)

接下来我们可以用同样原理测试一下缩放矩阵*旋转矩阵的效果。

m = [c, -s, 0, 2 * s, 2 * c, 0, 0, 0, 1]
ctx.save()
ctx.fillStyle = '#ac00ec'
ctx.translate(300, 200)
ctx.transform(m[0], m[3], m[1], m[4], m[2], m[5])
ctx.fillRect(0, 0, 200, 100)
ctx.restore()

效果如下:

image-20230313114339959

此时的transform()方法就相当于画矩形2时的:

ctx.rotate(Math.PI / 6)
ctx.fillRect(0, 0, 200, 100)

总结

我们这节课主要说了canvas内置的矩阵变换方法,以及其矩阵变换的实现原理。

基于这个原理我们可以去架构二维图形的模型矩阵、相机的视图投影矩阵、裁剪矩阵等多种矩阵,从而实现对图形和视图的灵活变换。

我最近就开发了一个canvas 矩阵变换相关的进阶课程,大家若感兴趣,可点击此链接:duz.xet.tech/s/Ky1eF