Day7 - 2D渲染环境基础篇 III[ 变形与阵列运算 ] - 成为Canvas Ninja ~ 理解2D渲染的精髓

之前我们有提到过,canvas其实本身可以看做一群像素形成的2维阵列,而Canvas的图像变形,其实就是对canvas自身做的一种阵列运算。

高中读理组的同学可能还记得数学课学过的『旋转矩阵』、『平移矩阵』...之类的东西~没有错,其实Canvas的变形API背後的原理就是矩阵运算~而第一次听到这些名词的人也不用太担心如何理解这些数理常识,因为在2D渲染环境的case中,其实光靠API就已经可以处理绝大部分的变形问题。

接下来我们就直接看看变形相关的API都有哪些~

Rotate/Translate/scale

之所以把这三个放在一起介绍,是因为这三个API常常会一同使用。

有些人可能会想像canvas的旋转应该就是旋转当前画好的图像,但是实际上不是的。
Canvas的旋转是固定以座标轴原点(0,0)为旋转中心转动整张canvas。

就像MDN上面的这张图片一样

img

到这边一定会有人问~那如果说我需要以某个座标作为旋转中心, 要怎麽办?
这时候就会用到ctx.translate。ctx.translate的用途是把座标轴中心移动到指定新座标,这样我们就可以透过移动座标轴原点来满足移动旋转中心的需求

img

而最後scale其实就跟rotate差不多,比较不同的是scale可以输入两个参数,一个参数是for X轴,另一个for Y轴(分别是两个方向的缩放变形),而当然因为缩放也同样会有所谓的"变形中心",变形中心也就跟rotate一样固定默认为座标轴原点,所以要移动变形中心也是必须要依赖ctx.translate

Transform/setTransform/getTransform

之所以把Transform系列的API 独立出来讲,是因为Transform的参数稍多,一条一条拉出来讲解比较清楚些。
而且因为这个API会牵涉到一点点矩阵运算的常识,所以我想也可以稍微提到点这部分背後的运算逻辑(其实还蛮有趣的)~

由於这边我们不会另外讲解一般矩阵运算的做法,高中没有学过或已经还给老师的人可以看这边

ctx.transform() 的用法

void ctx.transform(a, b, c, d, e, f); //一共会有六个参数

//a (m11): 水平scale参数, 最小值为1

//b (m12): Y轴倾斜参数(skewY) , 最小值为0

//c (m21): X轴倾斜参数(skewX) , 最小值为0

//d (m22): 垂直scale参数 , 最小值为1

//e (dx): X轴平移参数 , 最小值为0

//f (dy): Y轴平移参数 , 最小值为0

在这边我们可以看到这些m11、m12..,etc.这些奇怪的代号,这边m11,m12,m21,m22指的是当前变形矩阵(Current Transform Matrix, 简称CTM)的四个子项名称(例如m11意思就是CTM的第一行第一列的元素)。

所谓的当前变形矩阵(CTM)是一个3x3矩阵,用途是用来表示canvas元素当前的变形状态,而当我们每次去使用ctx.scale/ctx.translate/ctx.transform/ctx.rotate 最终都会导致CTM产生变化。

仔细观察上面a,b,c,d,e,f 这几个参数,会发现一个有趣的地方,那就是这些参数似乎没有跟Rotate相关的数值(例如角度),这是因为~ Canvas的旋转,其实是SkewX/SkewY 合并运用而产生的结果,所以这边我们可以看到SkewX/SkewY相关的参数,但却看不到Rotate的Degree值。

ctx.getTransform()/ctx.setTransform() 的用法

首先,这两个方法其实就是直接去 取得/设定 CTM。
getTransform 本身并没有参数,他会回传当前渲染环境的CTM阵列值。

let storedTransform = ctx.getTransform();

setTransform 本身的参数则和transform一模一样,但是他和transform的差别就在於setTransform是直接赋予CTM指定的新阵列值,而transform的阵列值则会透过矩阵乘积的方式累计到当前的CTM上(後面会提到)

void ctx.transform(a, b, c, d, e, f); //一共会有六个参数

当前变形矩阵(CTM)与变形前後向量座标的关系

这边我们实际用矩阵来表示CTM的组成:

a(m11) c(m21) e(dx)

b(m12) d(m22) f(dy)

0   0   1   

假设今天我们要透过改写CTM来引导Canvas的变形,则CTM与任意一个像素(Xi,Yi)的关系就如下图:

旧像素座标阵列 = CTM * 新像素座标阵列

img

这边我们可以发现一个有趣的点,那就是CTM竟然是放在等号後面的,这意味着若我们已知变形前座标和CTM的阵列值,我们其实就可以透过解二元一次联立方程序来取得变形後座标。

就像这样:

Xin*a+Yin*c+dx = Xip
Xin*b+Yin*d+dy = Yip

当前变形矩阵(CTM)与变形的计算方法

我们前面有提到过Canvas的变形运算其实就是简单的矩阵乘法,假设我们今天令一个canvas先後做了两次Transform

CTM的初始值:

1 0 0
0 1 0
0 0 1

第一次Transform使用的a,b,c,d,e,f 值:

1  -0.5  30
0.5  1  10
0    0   1

第二次Transform使用的a,b,c,d,e,f 值:

1  -0.5  30
0.5  1  10
0    0   1

则最终CTM将会变成一二两次Transform阵列的乘积:

0.75 -1  55

1   0.75  35

0    0    1

小结

虽然说大部分人可能会觉得变形只是一种稀松平常的电脑绘图操作,但是我觉得它背後的数学运算相当的有趣。

除此之外,虽然这次介绍的部分可能稍微比较需要花时间理解,而且在大部分的2D渲染案例,我们也比较少会需要理解到这麽深,但是未来若是要学习webgl环境的渲染编程,阵列运算会是相当重要的一环。

所以还是老话一句『学了不亏』~


<<:  DAY10: setTimeout和setImmediate的比较

>>:  Day07 - Login to Ptt

【Vue】帮元件加上样式啦|修改 bootstrap 变数供全域样式共用 失败

将样式区分为全域样式/区域样式 全域样式:大多页面都会共用到的样式,reset & vari...

第18车厢-动ㄘ动ㄘ!tab页签切换+轮播应用篇

本篇介绍透过bootstrap4直接使用tab切换功能,并且实作tab切换自动循环播放 我们在昨篇...

[Day15] 建立订单交易API_8

本节将进行完整的虚拟订单请求发送 def get_order(shop_no, need_pay, ...

Media queries

为什麽需要 Media Queries Media queries 可以让我们依据不同装置的特性来调...

Day9 - TextView(三)

如何让你的APP更独特 五颜六色的界面是一个很棒的范例 随便打开一个APP,他的字不可能都是黑色的,...