【JavaScript】canvasでビュー操作してみよう【HTML5/canvas】

#はじめに
<a href="https://qiita.com/PG0721/items/aefabeb59172818b9247">以前公開した記事</a>の続編的な記事です。
canvasで、マウスイベント、リサイズイベントをハンドリングして、平行移動や拡大縮小をしてみましょう。
今回はcontext.setTransformを使ってみます。

2020/03/09　追記:　<a href="https://qiita.com/PG0721/items/7df8f2f3337203c0ec1c">さらに続編的な記事</a>を書きました。

#仕様
動作確認ブラウザはPC版のChromeのみ
ライブラリはjQueryのみ使用
ドラッグで平行移動、ホイールで拡大縮小する(拡大縮小はカーソル位置中心)
ブラウザのリサイズに対応する
ブラウザいっぱいにcanvasを描画する
#仕様のイメージ
###ドラッグによる平行移動
![maki_translate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/9696a75a-0e61-8033-7440-61bcc406ba76.png)
###ホイールによる拡大縮小
![maki_scale.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/86eb752c-6dd2-2e59-2903-8a34f28bb800.png)
###ブラウザリサイズ
![maki_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/1b2438bb-37b3-0591-31eb-14030ac31159.png)

#用語
マウスドラッグによる平行移動とホイール操作による拡大縮小を<b>ビュー操作</b>と呼ぶことにしましょう。
また、canvasの座標系を<b>スクリーン座標系、</b>スクリーン座標系へ変換される前の座標系を<b>ワールド座標系</b>と呼ぶことにします。
ワールド座標系の可視範囲を<b>ビューボリューム</b>、スクリーン座標系の表示範囲を<b>ビューポート</b>と呼ぶことにします。
今回は、canvasをブラウザいっぱいに広げるので、ビューポートとはcanvasの矩形の事です。
1つ注意ですが、普通、ビューボリュームのアスペクト比はビューポートの<b>アスペクト比と一致</b>させますので、本記事もそれに従います。
ワールド座標系からスクリーン座標系へ変換する写像は行列で表現できますが、これを<b>射影行列</b>と呼ぶことにします。
以上は、一般的なコンピュータグラフィックスの用語とは異なるかもしれませんが、本記事はこれでいきます。
![maki_word.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/eed31193-7036-1585-851d-fc8f80662745.png)

#ビュー操作時の射影行列の求め方
上記をまとめると、ビュー操作が行われた時に、射影行列を更新すればよいことになります。
いきなり、射影行列を計算するのは大変ですので、
まずはビューボリュームの更新を考えます。
ビューボリュームは単なる矩形で、左上隅の座標系と幅と高さを持っています。
先ほど説明した通り、ビューボリュームは可視範囲です。ですので、例えば平行移動を実現するには、ビューボリュームの左上隅の座標を変更すれば良さそうです。
ここでは、ビューボリュームの更新が終わったとしましょう。求めたいのは射影行列ですので、今わかっているビューボリュームとビューポートから射影行列を計算します。
要は、ビューボリュームをビューポートに重ねるような変換(写像)が射影行列なのですから、ビューボリュームの左上隅を原点へ移動する行列と、ビューポートへ重なるように拡大する行列をかけたものが、射影行列です。

```js
// 射影行列をビューポートとビューボリュームから計算する
function updatePrjMatrix() {
    const trans = Matrix.translate(-_vv.x, -_vv.y), // ビューボリュームの左上隅を原点へ移動する
        invTrans = Matrix.translate(_vv.x, _vv.y),  // ビューボリュームの左上隅を原点へ移動する逆行列を求める
        scale = Matrix.scale(_vp.w / _vv.w, _vp.h / _vv.h), // ビューボリュームの拡大縮小し、ビューポートにフィットような行列を求める
        invScale = Matrix.scale(_vv.w / _vp.w, _vv.h / _vp.h);   // ビューボリュームの拡大縮小し、ビューポートにフィットような行列の逆行列を求める
    _m = Matrix.multiply(scale, trans); // 射影行列を更新する
    _inv = Matrix.multiply(invTrans, invScale); // 射影行列の逆行列を更新する        
}
```

#描画のタイミング
今回ビュー操作のタイミングでcanvasを再描画していません。requestAnimationFrameのコールバック内で再描画しています。こうすると、描画でかくつきにくくなります。
もし、mousemoveのタイミングで再描画するとなると、mousemoveイベントは1秒間に数百回起こるかもしれないので、再描画処理でかかる時間をかなり短くしなくてはならなくなります。
また、再描画はUIスレッド行う必要があり、UIスレッドに負荷をかけるのは良くありません。
通常のモニタですと、requestAnimationFrameのコールバックは1秒間に60回呼ばれますから、再描画処理は1/60秒を目指せばよいことになります。
今回の例はごく簡単な描画しかしていませんが、もし、canvasのプログラムを書いているのなら、以上の理由によりイベントハンドラでcanvasの再描画を行うべきではありません。

```js
function anim() {
    if (_redrawFlag) {// 再描画する
        updateView();
        _redrawFlag = false;
    } 
    requestAnimationFrame(anim);
}
```

```js
// ビュー(canvas)更新
// ※本関数はanim関数で呼ばれる
function updateView() {
    const ctx = $('#canvas')[0].getContext('2d');

    ctx.save();

    // canvasをクリアする
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    // 射影行列をセットする(これによりワールド座標系からスクリーン座標系への射影が行われる)
    ctx.setTransform(_m[0], _m[3], _m[1], _m[4], _m[2], _m[5]);

    // 矩形、丸、三角形を適当な位置へ描画

    // 青色の矩形を描画
    ctx.fillStyle = 'blue';
    ctx.fillRect(400, 200, 100, 100);        

    // 赤色の丸を描画
    ctx.fillStyle = 'red';
    ctx.beginPath();
    ctx.arc(300, 600, 50, 0, 360 * Math.PI / 180, false);
    ctx.fill();

    // 緑色の三角形を描画
    ctx.fillStyle = 'green';
    ctx.beginPath();
    ctx.moveTo(800, 500);
    ctx.lineTo(750, 600);
    ctx.lineTo(850, 600);
    ctx.fill();

    ctx.restore();
}
```

#平行移動の実現
mousedown時に、カーソルの座標を取得します。
この座標系はスクリーン座標系ですので、ワールド座標系へ変換して、1つ前の座標として保持します。
mousemove時に、カーソルの座標を取得しワールド座標系へ変換します。
1つ前の座標と現在の座標の差をビューボリュームの左上隅座標に加えます。
ビューボリュームが更新できましたので、射影行列を計算します。
次のmousemoveイベントに備えて、現在の座標を1つ前の座標とします。
これで、平行移動が実現できました。

```js
// イベントハンドラ 
$('#canvas').on('mousedown', e => {
    var cursorPos;  // スクリーン座標系のカーソルの座標

    // ブラウザのデフォルト動作を抑止(これをしないと、canvas上でのdragによる操作がうまく動作しない)
    e.preventDefault();

    if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

    if (_translating) {
        return;
    }
    _translating = true;

    // スクリーン座標系のカーソルの座標を取得
    cursorPos = { x: e.pageX, y: e.pageY };
    
    // スクリーン座標系をワールド座標系に変換
    _prePos = screenToWorld(cursorPos);        
}); 

$('#canvas').on('mousemove', e => {
    let cursorPos,  // スクリーン座標系のカーソルの座標
        curPos;     // ワールド座標系のカーソルの座標

    if(!_translating) {
        return;
    }

    // スクリーン座標系のカーソルの座標を取得
    cursorPos = { x: e.pageX, y: e.pageY };
    
    // スクリーン座標系をワールド座標系に変換
    curPos = screenToWorld(cursorPos);
    
    // 平行移動する
    translate({ x: _prePos.x - curPos.x, y: _prePos.y - curPos.y });
    
    // カーソルの座標をワールド座標系へ変換
    _prePos = screenToWorld(cursorPos);

    // 再描画フラグを立てる
    _redrawFlag = true;
});  

$('#canvas').on('mouseup', e => {
    _translating = false;
});
```

```js
// 平行移動
function translate(vec) {
    // ビューボリュームを更新する
    _vv.x += vec.x;
    _vv.y += vec.y;
    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}
```

#拡大縮小の実現
mousewheelイベントで、拡大か縮小か判定します。
ここでは、ホイールを奥側へ操作して、拡大であるとします。事前に適当に拡大率を決めておきます。
拡大処理をする前に、現在のカーソルの座標を取得し、ワールド座標系へ変換しておきます。これが拡大中心になります。
さて、ビューボリュームの更新ですが、
拡大中心を原点へ移動する行列、
拡大率に応じた拡大行列、
拡大中心を原点へ移動する行列の逆行列
を掛け合わせたものが射影行列となります。
ビューボリュームの計算ができましたので、射影行列を計算します。

```js
$('#canvas').on('mousewheel', e => {

    let cursorPos,  // スクリーン座標系のカーソルの座標
        curPos,     // ワールド座標系のカーソルの座標
        rate;

    if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

    // スクリーン座標系のカーソルの座標を取得
    cursorPos = { x: e.pageX, y: e.pageY };
    
    // スクリーン座標系をワールド座標系に変換
    curPos = screenToWorld(cursorPos);

    if (e.originalEvent.wheelDelta > 0) {// 奥へ動かした -> 拡大する -> ビューボリュームを縮小する
        rate = 1 / 1.2;
    } else {// 手前へ動かした -> 縮小する -> ビューボリュームを拡大する
        rate = 1.2;
    }

    // 拡大縮小する
    scale(curPos, rate);

    // 再描画フラグを立てる
    _redrawFlag = true;
});
```

```js
// 拡大縮小
function scale(center, rate) {
    let topLeft = { x: _vv.x, y: _vv.y },
        mat;

    // 中心座標を原点へ移動する
    mat = Matrix.translate(-center.x, -center.y);
    // 拡大縮小する
    mat = Matrix.multiply(Matrix.scale(rate, rate), mat);
    // 原点を中心座標へ戻す
    mat = Matrix.multiply(Matrix.translate(center.x, center.y), mat);
    
    topLeft = Matrix.multiply(mat, topLeft);
    
    // ビューボリューム更新
    _vv.x = topLeft.x;
    _vv.y = topLeft.y;
    _vv.w *= rate;
    _vv.h *= rate;
    
    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}
```

#リサイズ時の処理
リサイズ時の処理を4種類用意しました。
とりあえず、左上にあるコンボボックス(リサイズ時の処理)を変更して、動かしてみてください。
詳細についてはソースのコメントをお読みください。
ブラウザがリサイズされるので、それに合わせてcanvasのサイズを変更する必要があります。
あとは、ビューボリュームを更新して、射影行列を計算します。

```js
// リサイズ
// ビューボリュームの矩形の中心が変わらないように更新する
function resizeScaleCenter() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
    let vvsq = {};

    if(_vv.w > _vv.h) {// 横長
        vvsq.y = _vv.y;
        vvsq.size = _vv.h;
        vvsq.x = _vv.x + (_vv.w - vvsq.size) / 2; 
    } else {// 縦長
        vvsq.x = _vv.x;
        vvsq.size = _vv.w;
        vvsq.y = _vv.y + (_vv.h - vvsq.size) / 2;
    }        

    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新
    const aspect = _vp.w / _vp.h;
    if(aspect > 1) {// 横長
        _vv.y = vvsq.y;
        _vv.h = vvsq.size;
        _vv.x = vvsq.x - (vvsq.size * aspect) / 2 + vvsq.size / 2;
        _vv.w = vvsq.size * aspect;
    } else {// 縦長
        _vv.x = vvsq.x;
        _vv.w = vvsq.size;
        _vv.y = vvsq.y - (vvsq.size / aspect) / 2 + vvsq.size / 2;
        _vv.h = vvsq.size / aspect;
    }

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

// リサイズ
// ビューボリュームの矩形の左上隅が変わらないように更新する
function resizeScaleTopLeft() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
    let vvsq = {};

    if(_vv.w > _vv.h) {// 横長
        vvsq.size = _vv.h;
    } else {// 縦長
        vvsq.size = _vv.w;
    }        

    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新
    const aspect = _vp.w / _vp.h;
    if(aspect > 1) {// 横長
        _vv.h = vvsq.size;
        _vv.w = vvsq.size * aspect;
    } else {// 縦長
        _vv.w = vvsq.size;
        _vv.h = vvsq.size / aspect;
    }

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

// リサイズ
// 矩形の中央を中心に何も変化がないように見せる
function resizeNoScaleCenter() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく
    // 変更前のビューボリュームの中心点を求める
    const oldCenter = {
        x: _vv.x + _vv.w / 2,
        y: _vv.y + _vv.h / 2
    };
    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新(幅と高さのみ更新する)
    _vv.w = _vp.w * rate.x;
    _vv.h = _vp.h * rate.y;
    _vv.x = oldCenter.x - _vv.w / 2;
    _vv.y = oldCenter.y - _vv.h / 2;

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

// リサイズ
// 矩形の左上隅を中心に何も変化がないように見せる
function resizeNoScaleTopLeft() {
    // 変更前の拡大率を求める
    const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく

    // ビューポートの更新
    updateViewport();

    // ビューボリュームの更新(幅と高さのみ更新する)
    _vv.w = _vp.w * rate.x;
    _vv.h = _vp.h * rate.y;

    // 射影行列と射影行列の逆行列を更新する   
    updatePrjMatrix();
}

```

```js
$(window).on('resize', e => {
    // リサイズイベント毎に処理しないように少し時間をおいて処理する
    if(_resizeTimeoutId !== -1) {
        clearTimeout(_resizeTimeoutId);
        _resizeTimeoutId = -1;
    }
    _resizeTimeoutId = setTimeout(() => {
        if(_resizeType === 'scale center') {
            resizeScaleCenter();
        } else if(_resizeType === 'scale top left') {
            resizeScaleTopLeft();
        } else if(_resizeType === 'no scale center') {
            resizeNoScaleCenter();
        } else if(_resizeType === 'no scale top left') {
            resizeNoScaleTopLeft();
        } 
        updateDom();
        _redrawFlag = true;
        _resizeTimeoutId = -1;
    }, 500);
});
```


#全ソース
.htmlで1ファイルです。
コメントはたくさん書いています。
著作権は放棄しますので、ご自由にお使いください。
jqueryだけ、ご準備ください。
次の記事はこの仕様に加えて、丸、三角、四角を操作できるようにしてみようと思います。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>canvas sample(PC Chrome)</title>
<script src="./js/jquery-3.4.1.min.js" type="text/javascript"></script>
<style>
body {
    overflow: hidden;
}
#canvas {
    position: absolute;
    left: 0;
    top: 0;    
}
#resize-select {
    position: absolute;
    left: 20px;
    top: 20px;
    font-size: 24px;    
}
</style>
<script>
// ES6 for Chrome
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 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];
    }
}
$(() => {

    let _translating,   // 平行移動中かどうかのフラグ
        _prePos,        // 平行移動時の1つ前の座標
        _redrawFlag,    // 再描画フラグ
        _m,             // 射影行列
        _inv,           // 射影行列の逆行列
        _vv,            // ビューボリューム(表示範囲)
        _vp,            // ビューポート(canvasの矩形)
        _resizeTimeoutId,   // windowリサイズ時に使用するタイマーのID
        _resizeType;        // windowリサイズ時のビューボリューム更新メソッドの種類

    initModel();        // モデルの初期化
    updateDom();        // ビュー(DOM)の初期化
    initController();   // コントローラの初期化
    anim();             // ビュー(canvas)の更新は、ここで行う

    // モデルの初期化
    function initModel() {
        _translating = false;   // 平行移動中かどうかのフラグ
        _redrawFlag = true;     // 再描画フラグ(初回描画の為trueにしておく)
        _resizeTimeoutId = -1;  // windowリサイズ時に使用するタイマーのID
        _resizeType = 'no scale top left'; // windowリサイズ時のビューボリューム更新メソッドの種類

        // ビューポートとビューボリュームを初期化する
        updateViewport();
        _vv = { x: 0, y: 0, w: _vp.w, h: _vp.h };
        // 射影行列と射影行列の逆行列を更新する 
        updatePrjMatrix();
    }

    // コントローラの初期化
    function initController() {

        // イベントハンドラ 
        $('#canvas').on('mousedown', e => {
            var cursorPos;  // スクリーン座標系のカーソルの座標

            // ブラウザのデフォルト動作を抑止(これをしないと、canvas上でのdragによる操作がうまく動作しない)
            e.preventDefault();

            if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

            if (_translating) {
                return;
            }
            _translating = true;

            // スクリーン座標系のカーソルの座標を取得
            cursorPos = { x: e.pageX, y: e.pageY };
            
            // スクリーン座標系をワールド座標系に変換
            _prePos = screenToWorld(cursorPos);        
        }); 

        $('#canvas').on('mousemove', e => {
            let cursorPos,  // スクリーン座標系のカーソルの座標
                curPos;     // ワールド座標系のカーソルの座標
        
            if(!_translating) {
                return;
            }

            // スクリーン座標系のカーソルの座標を取得
            cursorPos = { x: e.pageX, y: e.pageY };
            
            // スクリーン座標系をワールド座標系に変換
            curPos = screenToWorld(cursorPos);
            
            // 平行移動する
            translate({ x: _prePos.x - curPos.x, y: _prePos.y - curPos.y });
            
            // カーソルの座標をワールド座標系へ変換
            _prePos = screenToWorld(cursorPos);

            // 再描画フラグを立てる
            _redrawFlag = true;
        });  

        $('#canvas').on('mouseup', e => {
            _translating = false;
        });

        $('#canvas').on('mousewheel', e => {

            let cursorPos,  // スクリーン座標系のカーソルの座標
                curPos,     // ワールド座標系のカーソルの座標
                rate;

            if(_resizeTimeoutId !== -1) { return; } // リサイズ処理待ち

            // スクリーン座標系のカーソルの座標を取得
            cursorPos = { x: e.pageX, y: e.pageY };
            
            // スクリーン座標系をワールド座標系に変換
            curPos = screenToWorld(cursorPos);

            if (e.originalEvent.wheelDelta > 0) {// 奥へ動かした -> 拡大する -> ビューボリュームを縮小する
                rate = 1 / 1.2;
            } else {// 手前へ動かした -> 縮小する -> ビューボリュームを拡大する
                rate = 1.2;
            }

            // 拡大縮小する
            scale(curPos, rate);

            // 再描画フラグを立てる
            _redrawFlag = true;
        });

        $(window).on('resize', e => {
            // リサイズイベント毎に処理しないように少し時間をおいて処理する
            if(_resizeTimeoutId !== -1) {
                clearTimeout(_resizeTimeoutId);
                _resizeTimeoutId = -1;
            }
            _resizeTimeoutId = setTimeout(() => {
                if(_resizeType === 'scale center') {
                    resizeScaleCenter();
                } else if(_resizeType === 'scale top left') {
                    resizeScaleTopLeft();
                } else if(_resizeType === 'no scale center') {
                    resizeNoScaleCenter();
                } else if(_resizeType === 'no scale top left') {
                    resizeNoScaleTopLeft();
                } 
                updateDom();
                _redrawFlag = true;
                _resizeTimeoutId = -1;
            }, 500);
        });

        $('#resize-select').change(e => {
            _resizeType = $(e.target).val();
        });
    }    

    // 滑らかに描画できるようにrequestAnimationFrameのタイミングで必要に応じて再描画する
    function anim() {
        if (_redrawFlag) {// 再描画する
            updateView();
            _redrawFlag = false;
        } 
        requestAnimationFrame(anim);
    }

    // ビューポートの更新
    function updateViewport() {
        _vp = { x: 0, y: 0, w: window.innerWidth, h: window.innerHeight };
    }

    // 射影行列をビューポートとビューボリュームから計算する
    function updatePrjMatrix() {
        const trans = Matrix.translate(-_vv.x, -_vv.y), // ビューボリュームの左上隅を原点へ移動する
            invTrans = Matrix.translate(_vv.x, _vv.y),  // ビューボリュームの左上隅を原点へ移動する逆行列を求める
            scale = Matrix.scale(_vp.w / _vv.w, _vp.h / _vv.h), // ビューボリュームの拡大縮小し、ビューポートにフィットような行列を求める
            invScale = Matrix.scale(_vv.w / _vp.w, _vv.h / _vp.h);   // ビューボリュームの拡大縮小し、ビューポートにフィットような行列の逆行列を求める
        _m = Matrix.multiply(scale, trans); // 射影行列を更新する
        _inv = Matrix.multiply(invTrans, invScale); // 射影行列の逆行列を更新する        
    }

    // リサイズ
    // ビューボリュームの矩形の中心が変わらないように更新する
    function resizeScaleCenter() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
        let vvsq = {};

        if(_vv.w > _vv.h) {// 横長
            vvsq.y = _vv.y;
            vvsq.size = _vv.h;
            vvsq.x = _vv.x + (_vv.w - vvsq.size) / 2; 
        } else {// 縦長
            vvsq.x = _vv.x;
            vvsq.size = _vv.w;
            vvsq.y = _vv.y + (_vv.h - vvsq.size) / 2;
        }        

        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新
        const aspect = _vp.w / _vp.h;
        if(aspect > 1) {// 横長
            _vv.y = vvsq.y;
            _vv.h = vvsq.size;
            _vv.x = vvsq.x - (vvsq.size * aspect) / 2 + vvsq.size / 2;
            _vv.w = vvsq.size * aspect;
        } else {// 縦長
            _vv.x = vvsq.x;
            _vv.w = vvsq.size;
            _vv.y = vvsq.y - (vvsq.size / aspect) / 2 + vvsq.size / 2;
            _vv.h = vvsq.size / aspect;
        }

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // リサイズ
    // ビューボリュームの矩形の左上隅が変わらないように更新する
    function resizeScaleTopLeft() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };
        let vvsq = {};

        if(_vv.w > _vv.h) {// 横長
            vvsq.size = _vv.h;
        } else {// 縦長
            vvsq.size = _vv.w;
        }        

        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新
        const aspect = _vp.w / _vp.h;
        if(aspect > 1) {// 横長
            _vv.h = vvsq.size;
            _vv.w = vvsq.size * aspect;
        } else {// 縦長
            _vv.w = vvsq.size;
            _vv.h = vvsq.size / aspect;
        }

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // リサイズ
    // 矩形の中央を中心に何も変化がないように見せる
    function resizeNoScaleCenter() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく
        // 変更前のビューボリュームの中心点を求める
        const oldCenter = {
            x: _vv.x + _vv.w / 2,
            y: _vv.y + _vv.h / 2
        };
        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新(幅と高さのみ更新する)
        _vv.w = _vp.w * rate.x;
        _vv.h = _vp.h * rate.y;
        _vv.x = oldCenter.x - _vv.w / 2;
        _vv.y = oldCenter.y - _vv.h / 2;

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // リサイズ
    // 矩形の左上隅を中心に何も変化がないように見せる
    function resizeNoScaleTopLeft() {
        // 変更前の拡大率を求める
        const rate = { x: _vv.w / _vp.w, y: _vv.h / _vp.h };    // rate.xはrate.yと等しいんだけど、一応xもyも求めておく

        // ビューポートの更新
        updateViewport();

        // ビューボリュームの更新(幅と高さのみ更新する)
        _vv.w = _vp.w * rate.x;
        _vv.h = _vp.h * rate.y;

        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // 平行移動
    function translate(vec) {
        // ビューボリュームを更新する
        _vv.x += vec.x;
        _vv.y += vec.y;
        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // 拡大縮小
    function scale(center, rate) {
        let topLeft = { x: _vv.x, y: _vv.y },
            mat;
    
        // 中心座標を原点へ移動する
        mat = Matrix.translate(-center.x, -center.y);
        // 拡大縮小する
        mat = Matrix.multiply(Matrix.scale(rate, rate), mat);
        // 原点を中心座標へ戻す
        mat = Matrix.multiply(Matrix.translate(center.x, center.y), mat);
        
        topLeft = Matrix.multiply(mat, topLeft);
        
        // ビューボリューム更新
        _vv.x = topLeft.x;
        _vv.y = topLeft.y;
        _vv.w *= rate;
        _vv.h *= rate;
        
        // 射影行列と射影行列の逆行列を更新する   
        updatePrjMatrix();
    }

    // スクリーン座標をワールド座標へ変換する
    function screenToWorld(screenPos) {
        return Matrix.multiply(_inv, screenPos);
    }

    // ビュー(DOM)の更新
    function updateDom() {

        // canvasをリサイズ
        $('#canvas').prop({
            width: _vp.w,
            height: _vp.h
        });  

        // リサイズタイプの設定(初期化時のみ)
        $('#resize-select').val(_resizeType);
    }

    // ビュー(canvas)更新
    // ※本関数はanim関数で呼ばれる
    function updateView() {
        const ctx = $('#canvas')[0].getContext('2d');

        ctx.save();

        // canvasをクリアする
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        // 射影行列をセットする(これによりワールド座標系からスクリーン座標系への射影が行われる)
        ctx.setTransform(_m[0], _m[3], _m[1], _m[4], _m[2], _m[5]);

        // 矩形、丸、三角形を適当な位置へ描画

        // 青色の矩形を描画
        ctx.fillStyle = 'blue';
        ctx.fillRect(400, 200, 100, 100);        

        // 赤色の丸を描画
        ctx.fillStyle = 'red';
        ctx.beginPath();
        ctx.arc(300, 600, 50, 0, 360 * Math.PI / 180, false);
        ctx.fill();

        // 緑色の三角形を描画
        ctx.fillStyle = 'green';
        ctx.beginPath();
        ctx.moveTo(800, 500);
        ctx.lineTo(750, 600);
        ctx.lineTo(850, 600);
        ctx.fill();

        ctx.restore();
    }
});
</script>
</head>
<body>
<canvas id="canvas"></canvas>
<select id="resize-select">
    <option value="scale center">1:&nbsp;scale 中央中心</option>
    <option value="scale top left">2:&nbsp;scale 左上隅中心</option>
    <option value="no scale center">3:&nbsp;no&nbsp;scale 中央中心</option>
    <option value="no scale top left">4:&nbsp;no&nbsp;scale 左上隅中心</option>
</select>
</body>
</html>


```