Canvasで斬！っと画面が斬れるようなエフェクトを書いてみた

# はじめに
Canvasを利用して、画面が**斬！**っと割れるようなエフェクトを表現してみました。つまらないものを斬ってしまった時には役に立つかもしれません。

**[動作イメージ]**

![斬2.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/bfa02d4e-095c-982e-0464-48f1f50e72d4.gif)

![斬.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/49392f48-5eed-f611-29c9-eb8fc08dc03a.gif)


> なお実際の動作は以下で確認することができます
> http://nekoneko-wanwan.github.io/demo/canvas/effect/zangeki/
> http://codepen.io/nekoneko-wanwan/pen/NGXMJb





# 特長・仕様
- Canvasサイズに依存しない
- 画像は1枚用意するだけでOK
- 斬られる角度は斜め45℃から90℃（垂直）まで対応
	- ただしコードを少し変更すれば、角度は柔軟に変えられる（多分）
	- サンプルコードでは45 - 90 の間でランダムに設定
- ずれていくアニメーションは細かく調整可能
	- コードだとベタで値を書いてるので、そこを調整してやれば...
- 分断されるアニメーションは、片方を固定させることも可能





# ソースコード

```index.html
<!-- Canvasサイズは画像サイズの比率に合わせている -->
<canvas id="myCanvas" width="500" height="333"></canvas>
```

```canvas.js
/* 変数定義 */
var cs       = document.getElementById('myCanvas');
var ctx      = cs.getContext('2d');
var csWidth  = cs.width;
var csHeight = cs.height;

/* 画像を生成し、読み込み後にエフェクト実行 */
var baseImg = new Image();
baseImg.src = 'img.jpg';
baseImg.onload = function() {
    zangeki();
};


/**
 * 斬撃エフェクト
 * Canvasの画像の比率が違うと、画像が変形されるのに注意
 */
var zangeki = function() {
    /* 変数定義 ----------------------------- */
    var hypotenuse; // Canvasの斜辺。動かす上限とする
    var basePos;    // 斬られる基準の位置（右上からのX座標）
    var move;       // 動く量
    var add;        // 加速度
    var alpha;      // 透明度
    var rad;        // ラジアン
    var deg;        // ラジアンを角度に変換


    /* 関数定義 ----------------------------- */
    /* 初期化。再利用しないなら、変数定義のところへまとめてしまってよい */
    var initialize = function() {
        hypotenuse = Math.sqrt(Math.pow(csWidth, 2) + Math.pow(csHeight, 2));
        basePos    = Math.round(Math.random() * csWidth / 2);
        move       = 0;
        add        = 0.1;
        alpha      = 1;

        /* 斬られる位置を元にした角度を求める */
        /* この角度を元に座標を変更していくことで移動とする */
        rad = Math.atan2(csHeight, csWidth - (basePos * 2));
        deg = rad * 180 / Math.PI;
    };

    /* 画像の描画 */
    var draw = {
        leftArea: function(isMove) {
            /* 移動量（斜辺）の算出 */
            var x = -move * Math.cos(rad);
            var y = move * Math.sin(rad);

            ctx.save();
            /* クリッピングする範囲を設定 */
            ctx.beginPath();
            ctx.moveTo(0, 0);
            ctx.lineTo(csWidth - basePos, 0);
            ctx.lineTo(basePos, csHeight);
            ctx.lineTo(0, csHeight);
            ctx.closePath();
            ctx.clip();
            /* 徐々に移動させる */
            if (isMove) {
                ctx.setTransform(1, 0, 0, 1, x, y);
            }
            ctx.drawImage(baseImg, 0, 0, csWidth, csHeight);
            ctx.restore();
        },
        rightArea: function(isMove) {
            /* 移動量（斜辺）の算出 */
            var x = move * Math.cos(rad);
            var y = -move * Math.sin(rad);

            ctx.save();
            /* クリッピングする範囲を設定 */
            ctx.beginPath();
            ctx.moveTo(csWidth - basePos, 0);
            ctx.lineTo(csWidth, 0);
            ctx.lineTo(csWidth, csHeight);
            ctx.lineTo(basePos, csHeight);
            ctx.closePath();
            ctx.clip();
            /* 徐々に移動させる */
            if (isMove) {
                ctx.setTransform(1, 0, 0, 1, x, y);
            }
            ctx.drawImage(baseImg, 0, 0, csWidth, csHeight);
            ctx.restore();
        }
    };

    /* レンダリング */
    var render = function() {
        ctx.clearRect(0, 0, csWidth, csHeight);
        ctx.globalAlpha = alpha;
        draw.leftArea(true);
        draw.rightArea(true);  // falseで固定（移動アニメーションをしない）

        /* 動いた量に応じて加速度を変更 */
        if (move  > 0 ) { add = 10;}
        if (move  > 50) { add = 0.2;}
        if (move  > 60) { add = 20;}

        /* 一定距離進んだら透明度を落としていく */
        if (move  > 300) {
            alpha -= 0.1;
            if (alpha < 0) {
                alpha = 0;
            }
        }

        move += add;
        var timer = requestAnimationFrame(render);

        /* 斜辺を超えたら一定時間停止し、再度エフェクトを開始する */
        if (move > hypotenuse + 10) { // +10はバッファ
            cancelAnimationFrame(timer);
            setTimeout(function() {
                initialize();
                render();
            }, 1000);
        }
    };

    initialize();
    render();
};
```



# 作り方・考え方
## エフェクトの実行まで
1. Canvasサイズに合わせて画像を用意します
	- サイズの比率が合わないと画像が変形されてしまいます（このコードの場合）
2. 画像の読み込みが完了した後に、エフェクト関数を実行します


## 画像の描画まで
1. Canvasを斜めに分断した時にできる、片側が垂直の台形を2つ想定します
	- このコードでは、右上端からのX座標の移動量を分断する基準とします
	- 斜めの角度によってはただの四角形、直角三角形になります
	- つまりCanvasサイズの半分の値で垂直となります
2. 台形の斜辺になっている箇所の内角角度を算出しておきます
3. 台形の4角を`lineTo()`でつないでパスを作成した後に`clip()`で、表示範囲を制限します
4. `setTransform()`でX, Y座標を変形し、アニメーションを実現します
	- 2. の角度を元に、斜辺の移動量を算出し適用します
5. 画像を配置します（`clip()`により台形型に表示）
6. ここまでの流れを`save()`, `restore()`しておきます
	- 別オブジェクトを生成した時に状態を適用させないため

> この手順を2回行うことで、それぞれ台形にクリッピングされた画像が2つ描画されます。
> 2つの画像がくっついた状態なので、画像や環境によっては動かさない状態でも切れ目が見えるかもしれません。


## アニメーションまで
1. 画像を描画し、動かす量をアップデートしていきます
2. 加速度の条件を細かく設定することで、イージングのようなものを実装します
3. 一定条件でアニメーションを停止させます（ここではCanvasの対角線よりも移動した時）





# 終わりに
作り方は自分で考えてみたので、書き方が微妙だったり非効率な可能性があります。もっと良い方法があればご教授くださいませ。

なおエフェクトのアイデアはこちらを参考にさせていただきました
> http://liginc.co.jp/web/html-css/143360

**今まで作成した謎なシリーズ**
> [Canvasで漫画にあるような吹き出しを書いてみた](http://qiita.com/nekoneko-wanwan/items/df5d8eaf33fa477fe7c9)
> [Canvasで漫画にあるような "ざわ・・・" を書いてみた](http://qiita.com/nekoneko-wanwan/items/4b196365183580831758)
> [Canvasで漫画にあるような集中線を書いてみた](http://qiita.com/nekoneko-wanwan/items/0911a59bf835d5b9e35a)