【HTML5/Canvas】setTransform()とtranform()について調べてみた


はじめに

canvasに行列を適用する関数に、setTransform()、transform()がある。
setTransform()は文字通り行列をセットする。
transform()は現在の行列に、指定した行列をかける。

context.setTransform(a, b, c, d, e, f) について

a, b, c, d, e, fが行列のどの要素に該当するか
ベクトルが列ベクトルの場合と行ベクトルの場合で示す。

列ベクトルで考える場合

\begin{pmatrix}
  x'\\
  y'\\
  1
\end{pmatrix}
=
\begin{pmatrix}
  a & c & e\\
  b & d & f\\
  0 & 0 & 1
\end{pmatrix}
\begin{pmatrix}
  x\\
  y\\
  1
\end{pmatrix}

行ベクトルで考える場合

\begin{pmatrix}
  x' & y' & 1
\end{pmatrix}
=
\begin{pmatrix}
  x & y & 1
\end{pmatrix}
\begin{pmatrix}
  a & b & 0\\
  c & d & 0\\
  e & f & 1
\end{pmatrix}

context.transform(a, b, c, d, e, f)について

a, b, c, d, e, fが行列のどの要素に該当するかはsetTransform()と同様である。
transformは通常、setTransform()の後に呼ばれる関数で、現在の行列に指定した行列をかける。
現在の行列のどちら側(右か左か)に行列をかけるかが重要である。
setTransform()後、transform()を2回呼ぶ場合の例
setTransform()で指定する行列を${\bf A}$, tranform()で掛ける行列を${\bf B}$,${\bf C}$、
ベクトルを${\bf x}$(列ベクトル)とすると、

[列ベクトルで考える場合]

変換後のベクトルは ${\bf A}{\bf B}{\bf C}{\bf x}$である
※${\bf B}$,${\bf C}$の順に${\bf A}$の右側にかかる

[行ベクトルで考える場合]

転置を取ればよいので
変換後のベクトルは ${\bf x}^T {\bf C}^T {\bf B}^T {\bf A}^T$となる
※${\bf B}^T$,${\bf C}^T$の順に${\bf A}^T$の左側にかかる

ちょっとしたテクニック

save(), restore()メソッドで行列をポップする
save()メソッドは現在の行列を保持する(※行列以外にも描画に関する情報も保持します)
restore()で、現在の行列をsave()呼び出し時の行列に戻す。

プログラム

4点(100, 100),(200, 100),(200, 200), (100, 200)を
(150, 150)を中心に45度回転させて、描画します。
描画時に、setTransform,transformを使った場合と
setTransform,transformを使わない場合の2パターン描画し、結果が一致することを確認します。
コメントアウトしている箇所を動かしてみたりしてください。

※私の作成したMatrixクラスは、ベクトルが列ベクトルであることを前提にして作ってあります。
また、Matrixクラスには今回のプログラムで使用していないメソッドも定義しております。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>canvas transform test</title>
<script src="./lib/temp/jquery-3.1.1.min.js" type="text/javascript"></script>
<style>
body {
    margin: 0px;
}
#canvas {
    position: absolute;
    left: 0px;
    top: 0px;    
}
</style>
<script>
// 行列クラス
class Matrix {
    // m0は行列、m1は行列又はベクトル
    // 行列は大きさ9の1次元配列であること。 ex. [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ]
    // ベクトルはxとyをプロパティに持つ連想配列であること。 ex. { x: 2, y: 4 }
    // 左からベクトルをかけることは想定していない
    static multiply(m0, m1) {
        if(m1.length && m1.length === 9) {// m1は行列
            return [
                m0[0] * m1[0] + m0[1] * m1[3] + m0[2] * m1[6],
                m0[0] * m1[1] + m0[1] * m1[4] + m0[2] * m1[7],
                m0[0] * m1[2] + m0[1] * m1[5] + m0[2] * m1[8],
                m0[3] * m1[0] + m0[4] * m1[3] + m0[5] * m1[6],
                m0[3] * m1[1] + m0[4] * m1[4] + m0[5] * m1[7],
                m0[3] * m1[2] + m0[4] * m1[5] + m0[5] * m1[8],
                m0[6] * m1[0] + m0[7] * m1[3] + m0[8] * m1[6],
                m0[6] * m1[1] + m0[7] * m1[4] + m0[8] * m1[7],
                m0[6] * m1[2] + m0[7] * m1[5] + m0[8] * m1[8],
            ];
        } else {// m1はベクトル
            return {
                x: m0[0] * m1.x + m0[1] * m1.y + m0[2],
                y: m0[3] * m1.x + m0[4] * m1.y + m0[5],                
            };
        }
    }
    // 単位行列
    static identify() {
        return [1, 0, 0, 0, 1, 0, 0, 0, 1];
    }
    // 平行移動行列
    static translate(x, y) {
        return [1, 0, x, 0, 1, y, 0, 0, 1];
    }
    // 拡大縮小行列
    static scale(x, y) {
        return [x, 0, 0, 0, y, 0, 0, 0, 1];
    }
    // 回転行列
    static rotate(theta) {
        const cos = Math.cos(theta),
            sin = Math.sin(theta);
        return [cos, -sin, 0, sin, cos, 0, 0, 0, 1];
    }
    // 逆行列を求める
    static inverse(m) {
        const det = Matrix.determinant(m),
            inv = [
                m[4] * m[8] - m[5] * m[7],    -(m[1] * m[8] - m[2] * m[7]),   m[1] * m[5] - m[2] * m[4],
                -(m[3] * m[8] - m[5] * m[6]), m[0] * m[8] - m[2] * m[6],      -(m[0] * m[5] - m[2] * m[3]),
                m[3] * m[7] - m[4] * m[6],    -(m[0] * m[7] - m[1] * m[6]),   m[0] * m[4] - m[1] * m[3]
            ];
        return inv.map(elm => elm / det);
    }
    // 行列式を求める
    static determinant(m) {
        return m[0] * m[4] * m[8] 
        + m[1] * m[5] * m[6] 
        + m[2] * m[3] * m[7]
        - m[2] * m[4] * m[6]
        - m[1] * m[3] * m[8]
        - m[0] * m[5] * m[7];
    }
}

// 初期化
$(() => {
    const points = [
        { x: 100, y: 100 },
        { x: 200, y: 100 },
        { x: 200, y: 200 },
        { x: 100, y: 200 },
    ];
    $('#canvas').prop({
        width: window.innerWidth,
        height: window.innerHeight
    });

    let m;
    const m0 = Matrix.translate(-150, -150);
    const m1 = Matrix.rotate(45 * (Math.PI / 180));
    const m2 = Matrix.translate(150, 150);    

    m = Matrix.multiply(m1, m0);
    m = Matrix.multiply(m2, m);

    const ctx = $('#canvas')[0].getContext('2d');

    // setTransformを使った描画
    ctx.save();  

    ctx.setTransform(m[0], m[3], m[1], m[4], m[2], m[5]);

    /*
    // 代わりにこう書いてもよし
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.transform(m2[0], m2[3], m2[1], m2[4], m2[2], m2[5]);
    ctx.transform(m1[0], m1[3], m1[1], m1[4], m1[2], m1[5]);
    ctx.transform(m0[0], m0[3], m0[1], m0[4], m0[2], m0[5]);
    */

    /*
    // 代わりにこう書いてもよし
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.translate(150, 150);
    ctx.rotate(45 * (Math.PI / 180));
    ctx.translate(-150, -150);
    */

    ctx.globalAlpha = 1;
    ctx.fillStyle = 'red';
    points.forEach(p => {
        ctx.beginPath();
        ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
    });
    ctx.restore();

    // setTransFormを使わない描画
    ctx.save();  
    // 直前でrestoreしているので、ctx.setTransform(1, 0, 0, 1, 0, 0);する必要なし
    ctx.globalAlpha = 0.5;
    ctx.fillStyle = 'blue';
    points.forEach(p => {
        ctx.beginPath();
        const mp = Matrix.multiply(m, p);   // 行列をかける
        ctx.arc(mp.x, mp.y, 10, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
    });
    ctx.restore();
});
</script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>