<commit_before>---
layout: post_latex
title: Canvas的transform函数与2D仿射变换矩阵分解
tags: ['matrix','linear algebra','math']
published: true
---

最近偶然接触了一下canvas的2D仿射变换。和3D一样，canvas有scale、translate、rotate操作，本质上这3个函数也是矩阵乘法。

canvas应该内置了一套矩阵运算系统，并且canvas内含有一个仿射变换矩阵（大概认为是3x3=9个浮点数变量即可，2D是3x3矩阵，3D是4x4矩阵）。每次调用scale、translate、rotate就是对这个矩阵做矩阵乘法。

另外还有3个函数可以控制canvas的仿射变换：

- resetTransform 重置为单位矩阵
- transform(a,b,c,d,e,f) 以a,b,c,d,e,f构造一个仿射变换矩阵并乘到当前canvas的仿射变换矩阵
- setTransform(a,b,c,d,e,f) 重置为单位矩阵并应用transform(a,b,c,d,e,f)

我遇到的需求是，**如果canvas没有提供transform函数，怎么用scale、translate、rotate三个函数的组合，来模拟transform函数？**

<!--more-->

## 自定义的transform函数实现

先抛出答案：

```javascript

var transform = function(a, b, c, d, e, f) {
    var a = [a, b, c, d, e, f];
    var angle = Math.atan2(a[1], a[0]),
        denom = Math.pow(a[0], 2) + Math.pow(a[1], 2),
        scaleX = Math.sqrt(denom),
        scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX,
        skewX = Math.atan2(a[0] * a[2] + a[1] * a[3], denom);
    
    skewX = skewX / (Math.PI / 180); // Note: canvas has no skew only function!
    var skewY = 0;
    var translateX = a[4];
    var translateY = a[5];
    this.translate(translateX, translateY);
    this.rotate(angle);
    this.scale(scaleX, scaleY);
}

```

代码来源:

 [stackoverflow - Find the Rotation and Skew of a Matrix transformation
](https://stackoverflow.com/questions/5107134/find-the-rotation-and-skew-of-a-matrix-transformation/32125700#32125700)

[unmatrix - parse(str) ](https://github.com/matthewmueller/unmatrix/blob/master/index.js)

[DecomposeMatrix 此代码最原始出处（有注释）](https://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp)

[GRAPHICS GEMS II edited by <NAME>]()

我这个版本代码看起来不复杂，但还是慢慢地来分析下数学原理吧。

## 2D仿射变换矩阵的分解

transform的参数a, b, c, d, e, f组成了一个3x3仿射变换矩阵A：


{% assign matA = "a,c,e,b,d,f,0,0,1" | split: ',' %}


\\( A = {% include render_matrix_raw.html mat = matA row = 3 col = 3 %}  \\)

设有2维向量\\(\\mathbf p=(x, y, 1)\\)，让它左乘A，就会得到经过A变换后的向量\\(\\mathbf p'=(x', y', 1)\\)。


{% assign matP = "x,y,1" | split: ',' %}

\\( \\mathbf p = {% include render_matrix_raw.html mat = matP row = 3 col = 1 %}  \\)

{% assign matP2 = "x',y',1" | split: ',' %}

{% assign matP3 = "ax+cy+e,bx+dy+f,1" | split: ',' %}

\\( \\mathbf p' = A\\mathbf p  = {% include render_matrix_raw.html mat = matP2 row = 3 col = 1 %} = {% include render_matrix_raw.html mat = matA row = 3 col = 3 %} {% include render_matrix_raw.html mat = matP row = 3 col = 1 %} = {% include render_matrix_raw.html mat = matP3 row = 3 col = 1 %} \\)


现在问题是，怎么把A分解成T(translate)、R(rotate)、S(scale)三个矩阵。

### 提取T

首先先把translate矩阵提取出来：

{% assign matT = "1,0,e,0,1,f,0,0,1" | split: ',' %}


\\( T = {% include render_matrix_raw.html mat = matT row = 3 col = 3 %}  \\)

显然这是正确的，可以试一下：

{% assign matP4 = "x+e,y+f,1" | split: ',' %}

\\( \\mathbf p' = T\\mathbf p = {% include render_matrix_raw.html mat = matT row = 3 col = 3 %} {% include render_matrix_raw.html mat = matP row = 3 col = 1 %} = {% include render_matrix_raw.html mat = matP4 row = 3 col = 1 %} \\)

x偏移了e，y偏移了f。

## 原始注释解释

decomposition algorithm：

```c

/*
 * The relevant section of the transitions specification:
 * http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types-
 * defers all of the details to the 2-D and 3-D transforms specifications.
 * For the 2-D transforms specification (all that's relevant for us, right
 * now), the relevant section is:
 * http://dev.w3.org/csswg/css3-2d-transforms/#animation
 * This, in turn, refers to the unmatrix program in Graphics Gems,
 * available from http://tog.acm.org/resources/GraphicsGems/ , and in
+
− * particular as the file GraphicsGems/gemsii/unmatrix.c
 * in http://tog.acm.org/resources/GraphicsGems/AllGems.tar.gz
 *<commit_msg>no message<commit_after>---
layout: post_latex
title: Canvas的transform函数与2D仿射变换矩阵分解
tags: ['matrix','linear algebra','math']
published: true
---

最近偶然接触了一下canvas的2D仿射变换。和3D一样，canvas有scale、translate、rotate操作，本质上这3个函数也是矩阵乘法。

canvas应该内置了一套矩阵运算系统，并且canvas内含有一个仿射变换矩阵（大概认为是3x3=9个浮点数变量即可，2D是3x3矩阵，3D是4x4矩阵）。每次调用scale、translate、rotate就是对这个矩阵做矩阵乘法。

另外还有3个函数可以控制canvas的仿射变换：

- resetTransform 重置为单位矩阵
- transform(a,b,c,d,e,f) 以a,b,c,d,e,f构造一个仿射变换矩阵并乘到当前canvas的仿射变换矩阵
- setTransform(a,b,c,d,e,f) 重置为单位矩阵并应用transform(a,b,c,d,e,f)

我遇到的需求是，**如果canvas没有提供transform函数，怎么用scale、translate、rotate三个函数的组合，来模拟transform函数？**

<!--more-->

## 自定义的transform函数实现

先抛出答案：

```javascript

var transform = function(a, b, c, d, e, f) {
    var a = [a, b, c, d, e, f];
    var angle = Math.atan2(a[1], a[0]),
        denom = Math.pow(a[0], 2) + Math.pow(a[1], 2),
        scaleX = Math.sqrt(denom),
        scaleY = (a[0] * a[3] - a[2] * a[1]) / scaleX,
        skewX = Math.atan2(a[0] * a[2] + a[1] * a[3], denom);
    
    skewX = skewX / (Math.PI / 180); // Note: canvas has no skew only function!
    var skewY = 0;
    var translateX = a[4];
    var translateY = a[5];
    this.translate(translateX, translateY);
    this.rotate(angle);
    this.scale(scaleX, scaleY);
}

```

代码来源:

 [stackoverflow - Find the Rotation and Skew of a Matrix transformation
](https://stackoverflow.com/questions/5107134/find-the-rotation-and-skew-of-a-matrix-transformation/32125700#32125700)

[unmatrix - parse(str) ](https://github.com/matthewmueller/unmatrix/blob/master/index.js)

[DecomposeMatrix 此代码最原始出处（有注释）](https://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp)

[GRAPHICS GEMS II edited by <NAME>]()

我这个版本代码看起来不复杂，但还是慢慢地来分析下数学原理吧。

## 2D仿射变换矩阵的分解

transform的参数a, b, c, d, e, f组成了一个3x3仿射变换矩阵A：


{% assign matA = "a,c,e,b,d,f,0,0,1" | split: ',' %}


\\( A = {% include render_matrix_raw.html mat = matA row = 3 col = 3 %}  \\)

设有2维向量\\(\\mathbf p=(x, y, 1)\\)，让它左乘A，就会得到经过A变换后的向量\\(\\mathbf p'=(x', y', 1)\\)。


{% assign matP = "x,y,1" | split: ',' %}

\\( \\mathbf p = {% include render_matrix_raw.html mat = matP row = 3 col = 1 %}  \\)

{% assign matP2 = "x',y',1" | split: ',' %}

{% assign matP3 = "ax+cy+e,bx+dy+f,1" | split: ',' %}

\\( \\mathbf p' = A\\mathbf p  = {% include render_matrix_raw.html mat = matP2 row = 3 col = 1 %} = {% include render_matrix_raw.html mat = matA row = 3 col = 3 %} {% include render_matrix_raw.html mat = matP row = 3 col = 1 %} = {% include render_matrix_raw.html mat = matP3 row = 3 col = 1 %} \\)


现在问题是，怎么把A分解成T(translate)、R(rotate)、S(scale)三个矩阵。

### 提取T

首先先把translate矩阵提取出来：

{% assign matT = "1,0,e,0,1,f,0,0,1" | split: ',' %}


\\( T = {% include render_matrix_raw.html mat = matT row = 3 col = 3 %}  \\)

显然这是正确的，可以试一下：

{% assign matP4 = "x+e,y+f,1" | split: ',' %}

\\( \\mathbf p' = T\\mathbf p = {% include render_matrix_raw.html mat = matT row = 3 col = 3 %} {% include render_matrix_raw.html mat = matP row = 3 col = 1 %} = {% include render_matrix_raw.html mat = matP4 row = 3 col = 1 %} \\)

x偏移了e，y偏移了f。

再设等式：A = QT。Q是未知矩阵，且Q包含了scale、rotate变换。

Q可以用参数a, b, c, d, e, f表示：

{% assign matQ = "a,c,e - ae - cf,b,d,f - be - df,0,0,1" | split: ',' %}

\\( Q = {% include render_matrix_raw.html mat = matQ row = 3 col = 3 %}  \\)

验证下：


\\( QT = {% include render_matrix_raw.html mat = matQ row = 3 col = 3 %}{% include render_matrix_raw.html mat = matT row = 3 col = 3 %} \\)

{% assign matTmp = "a, c, ae + cf + e - ae - cf, b, d, be + df + f - be - df, 0, 0, 1" | split: ',' %}

\\( = {% include render_matrix_raw.html mat = matTmp row = 3 col = 3 %} \\)

\\( = {% include render_matrix_raw.html mat = matA row = 3 col = 3 %} = A \\)

### 分解Q

设 Q = SR，S是scale，R是rotate。2D的R、S矩阵分别为：


{% assign matR = "cosθ, -sinθ, 0, sinθ, cosθ, 0, 0, 0, 1" | split: ',' %}

\\( R = {% include render_matrix_raw.html mat = matR row = 3 col = 3 %}  \\)

{% assign matS = "x, 0, 0, 0, y, 0, 0, 0, 1" | split: ',' %}

\\( S = {% include render_matrix_raw.html mat = matS row = 3 col = 3 %}  \\)

其实，Q还可能包含了shear斜切变换。一般来说斜切是比较少见到的一种变换，且canvas并没有单独的shear函数。所以本文开头的目标，自定义custom，怎么弄都不能实现shear变换（只有scale、translate、rotate可用）。

现在要分解Q，可以把shear也一并考虑。shear矩阵形式如下；

{% assign matSH = "1, s, 0, 0, 1, 0, 0, 0, 1" | split: ',' %}

\\( Shear = {% include render_matrix_raw.html mat = matSH row = 3 col = 3 %}  \\)

如果没有shear变换，那么s=0。

综上，Q = Shear * Scale * Rotate：

\\( Q = {% include render_matrix_raw.html mat = matSH row = 3 col = 3 %} {% include render_matrix_raw.html mat = matS row = 3 col = 3 %} {% include render_matrix_raw.html mat = matR row = 3 col = 3 %}  \\)

{% assign matQ2 = "xcosθ, -xsinθ, 0, ysinθ, ycosθ, 0, 0, 0, 1" | split: ',' %}

{% assign matQ3 = "xcosθ + sysinθ, -xsinθ+sycosθ, 0, ysinθ, ycosθ, 0, 0, 0, 1" | split: ',' %}

\\( = {% include render_matrix_raw.html mat = matSH row = 3 col = 3 %}  {% include render_matrix_raw.html mat = matQ2 row = 3 col = 3 %}  \\)

\\( = {% include render_matrix_raw.html mat = matQ3 row = 3 col = 3 %}  \\)

再回顾上一小节的Q：

\\( Q = {% include render_matrix_raw.html mat = matQ row = 3 col = 3 %}  \\)

所以可以得到方程组：

- \\( a =  x  cosθ + s y sinθ \\)

- \\( b =  y  sinθ \\)

- \\( c = -x  sinθ + s y cosθ \\)

- \\( d = y cosθ \\)

- \\( 0 = e - ae - cf  \\)

- \\( 0 = f - be - df \\)

看起来有点乱，慢慢拆解下吧。观察发现第二四等式可以解出未知数y：

\\( b\^\{2\} = y\^\{2\} sin\^\{2\}θ \\)

\\( d\^\{2\} = y\^\{2\} cos\^\{2\}θ  \\)

\\( b\^\{2\} + d\^\{2\} = y\^\{2\} sin\^\{2\}θ + y\^\{2\} cos\^\{2\}θ \\)

\\( b\^\{2\} + d\^\{2\} = y\^\{2\} ( sin\^\{2\}θ + cos\^\{2\}θ ) \\)

\\( b\^\{2\} + d\^\{2\} = y\^\{2\} \\)

\\( y = \\sqrt \{ b\^\{2\} + d\^\{2\} \} \\)


## 原始注释解释

decomposition algorithm：

```c

/*
 * The relevant section of the transitions specification:
 * http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types-
 * defers all of the details to the 2-D and 3-D transforms specifications.
 * For the 2-D transforms specification (all that's relevant for us, right
 * now), the relevant section is:
 * http://dev.w3.org/csswg/css3-2d-transforms/#animation
 * This, in turn, refers to the unmatrix program in Graphics Gems,
 * available from http://tog.acm.org/resources/GraphicsGems/ , and in
+
− * particular as the file GraphicsGems/gemsii/unmatrix.c
 * in http://tog.acm.org/resources/GraphicsGems/AllGems.tar.gz
 *
{'fraction_non_alphanumeric': 0.19428174327409908, 'fraction_numerical': 0.033555048148515836, 'mean_word_length': 3.4106830122591942, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7573735', 'n_tokens_mistral': 5031, 'n_tokens_neox': 4939, 'n_words': 915}