课堂目标
- 理解矩阵变换的概念
- 可以用不同的变换方式变换物体
知识点
- 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>
效果如下:
这个矩形在浏览器的左上角。
我们把它往右下方移动一下,方便观察。
/* 矩形1 */
ctx.save()
ctx.translate(300, 200)
ctx.fillRect(0, 0, 200, 100)
ctx.restore()
效果如下:
有点呆板,让它旋转30°
/* 矩形1 */
ctx.save()
ctx.translate(300, 200)
ctx.rotate(Math.PI / 6)
ctx.fillRect(0, 0, 200, 100)
ctx.restore()
效果如下:
我再在其本地坐标系的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()
效果如下:
这些看起来都没啥,很简单。
接下来,我们说重点。
我把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()
效果如下:
我刚才没有画出蓝色的矩形,而是画出了一个蓝色的平行四边形。
这是为什么呢?
这就是我们本章要讨论的话题了。
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()
效果如下:
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()
效果如下:
此时的transform()方法就相当于画矩形2时的:
ctx.rotate(Math.PI / 6)
ctx.fillRect(0, 0, 200, 100)
总结
我们这节课主要说了canvas内置的矩阵变换方法,以及其矩阵变换的实现原理。
基于这个原理我们可以去架构二维图形的模型矩阵、相机的视图投影矩阵、裁剪矩阵等多种矩阵,从而实现对图形和视图的灵活变换。
我最近就开发了一个canvas 矩阵变换相关的进阶课程,大家若感兴趣,可点击此链接:duz.xet.tech/s/Ky1eF