Canvasで漫画にあるような "ざわ・・・" を書いてみた

#はじめに
前回[Canvasで漫画にあるような集中線を書いてみた](http://qiita.com/nekoneko-wanwan/items/0911a59bf835d5b9e35a)という記事を書きました。

漫画シリーズ第二弾として、私の好きな漫画でよく出てくるようなオノマトペをCanvasで再現してみました。「ざわざわ」させたい時には役に立つかもしれません。



**[動作イメージ]**
※Gifアニメが重くなってしまったので、静止画を使っています。本当は動きます。

![zawazawa1.png](https://qiita-image-store.s3.amazonaws.com/0/58436/966a8ccd-c3f4-6145-8139-314297671c1f.png)

![zawazawa2.png](https://qiita-image-store.s3.amazonaws.com/0/58436/f5e11f7e-9bf9-8544-e559-8a6dc53c1eb1.png)




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





#特長

- Canvasサイズに依存しない
- **`ざわ・・・`**の大きさと位置は一定範囲内でランダム
- **`ざわ・・・`**の小さいものはゆっくり、大きい物は早くアニメーションする（奥行きを表現）
- **`ざわ・・・`**の数が変更できる
- **`ざわ・・・`**には4つのパターンを用意
	- 黒文字
	- 白文字
	- 黒文字・白フチ
	- 白文字・黒フチ
- **`ざわ・・・`**の流れる方向が左右で変更できる **←[NEW!]**


> **`ざわ・・・`**の大きさはスクリプト内で設定しているため、変更する場合は少し手を加える必要があります





#ソースコード

```index.html
<canvas id="myCanvas" width="500" height="500"></canvas>
```

```canvas.js
/**
 * ざわ・・・メーカー
 * @param {obj} cs: canvas object
 * @param {number} zawaNum: 生成する「ざわ・・・」の数
 * @param {boolean} isBordering: 縁取りするかどうか
 *     黒文字の場合は白フチに、白文字の場合は黒フチに
 * @param {boolean} isColorBlack: 黒文字にするかどうか（falseで白文字）
 * @param {boolean} isRightFlow: 右から流れるようにするか
 */
var zawaMaker = function(cs, zawaNum, isBordering, isColorBlack, isRightFlow ) {
    var ctx      = cs.getContext('2d');
    var csWidth  = cs.width;
    var csHeight = cs.height;
    var zawas    = [];

    /**
     * ランダムな整数を返す
     * @param max 最大値
     * @param min 最小値
     * @return min ~ max
     */
    var getRandomInt = function(max, min) {
        return Math.floor(Math.random() * (max - min)) + min;
    };

    /**
     * @constructor
     */
    var ZawaZawa = function() {
        this.initialize();
    };

    ZawaZawa.prototype = {
        initialize: function() {
            this.scale     = getRandomInt(10, 5) / 10; // 1 ~ 0.5の倍率
            this.width     = this.scale * 160;  // 元サイズを大体150pxくらいと規定
            this.height    = this.scale * 60;   // 元サイズを大体60pxくらいと規定
            this.moveX     = getRandomInt(csWidth  - this.width, 0);
            this.moveY     = getRandomInt(csHeight - this.height, 0);
            this.addMoveX  = this.scale * 5;  // 奥行きを出すために、小さい要素は移動量を少なくする
            this.alpha     = 1;
        },
        setStatus: function() {
            /* 変換マトリクスを初期化 */
            ctx.setTransform(1, 0, 0, 1, 0, 0);

            ctx.scale(this.scale, this.scale);
            ctx.translate(this.moveX, this.moveY);
            ctx.globalAlpha = this.alpha;
        },
        update: function() {
            /* scaleで座標軸がズレているので、scaleした分を割った値をCanvasサイズとする */
            var _csWidth = csWidth / this.scale;

            /* 流れる向きの条件分岐 */
            if (isRightFlow) {
                if (this.moveX + this.width + 50 < 0 ) {
                    this.alpha = 0;
                    this.moveX = _csWidth + this.width;

                    /* scaleを変えるため再設定 */
                    this.scale    = getRandomInt(10, 5) / 10;
                    this.width    = this.scale * 160;
                    this.height   = this.scale * 60;
                    this.moveY    = getRandomInt(csHeight - this.height, 0);
                    this.addMoveX = this.scale * 5;
                } else {
                    this.moveX -= this.addMoveX;
                    this.alpha += 0.01;
                }
            } else {
                if (this.moveX >_csWidth) {
                    this.alpha = 0;
                    this.moveX = - this.width;

                    /* scaleを変えるため再設定 */
                    this.scale    = getRandomInt(10, 5) / 10;
                    this.width    = this.scale * 160;
                    this.height   = this.scale * 60;
                    this.moveY    = getRandomInt(csHeight - this.height, 0);
                    this.addMoveX = this.scale * 5;
                } else {
                    this.moveX += this.addMoveX;
                    this.alpha += 0.01;
                }
            }

            /* 透明度が1以上の場合は透明度の増加を止める */
            if (this.alpha > 1) {
                this.alpha = 1;
            }
        },
        draw: function() {
            /**
             * @param {number} width: 線の太さ
             * @param {string} color: 線の色
             */
            function createZawaPath(width, color) {
                ctx.lineWidth = width;
                ctx.strokeStyle = color;
                ctx.beginPath();
                ctx.lineCap = "round";
                ctx.lineJoin = "round";

                // ざ
                ctx.moveTo(10, 45);
                ctx.lineTo(50, 45);

                ctx.moveTo(25, 35);
                ctx.lineTo(50, 60);
                ctx.lineTo(20, 60);
                ctx.bezierCurveTo(5, 60, 5, 75, 20, 75);
                ctx.lineTo(50, 75);

                ctx.moveTo(38, 30);
                ctx.lineTo(38, 36);

                ctx.moveTo(46, 30);
                ctx.lineTo(46, 36);

                // わ
                ctx.moveTo(70, 35);
                ctx.lineTo(70, 75);

                ctx.moveTo(60, 45);
                ctx.lineTo(80, 45);
                ctx.lineTo(60, 75);
                ctx.bezierCurveTo(80, 25, 120, 70, 85, 75);

                // ・・・
                ctx.moveTo(110, 60);
                ctx.lineTo(115, 60);

                ctx.moveTo(125, 60);
                ctx.lineTo(130, 60);

                ctx.moveTo(140, 60);
                ctx.lineTo(145, 60);

                ctx.closePath();
                ctx.stroke();
            }

            /* 縁取と黒・白文字の条件分岐 */
            if (isBordering && isColorBlack) {
                createZawaPath(15, 'white');
                createZawaPath(6, 'black');
            }
            if (isBordering && !isColorBlack) {
                createZawaPath(15, 'black');
                createZawaPath(6, 'white');
            }
            if (!isBordering && isColorBlack) {
                createZawaPath(6, 'black');
            }
            if (!isBordering && !isColorBlack) {
                createZawaPath(6, 'white');
            }

        },
        render: function() {
            this.setStatus();
            this.draw();
            this.update();
        }
    };


    /**
     * ざわインスタンスの作成
     * @return zawas[instance, instance...];
     */
    function createZawas(num) {
        var i = 0;
        for (; i < num; i++) {
            zawas[zawas.length] = new ZawaZawa();
        }
    }

   /**
     * 描画
     */
    function render() {
        var i = 0;
        var l = zawas.length;
        ctx.setTransform(1,0,0,1,0,0);
        ctx.clearRect(0, 0, csWidth, csHeight);
        for (; i < l; i++) {
            zawas[i].render();
        }
        requestAnimationFrame(render);
    }

    /* 実行 */
    createZawas(zawaNum);
    render();
};

var cs = document.getElementById('myCanvas1');

/* args: canvasオブジェクト, ざわの数, 縁取りか, 黒文字か, 右から流すか */
zawaMaker(cs, 10, false, true, false);
```





#作り方・考え方

1. Canvas上で **`ざわ・・・`**の文字を表現する
2. **`ざわ・・・`**の大きさと位置をランダムに設定する
3. 一定時間ごとに左→右へループさせる


##Canvas上で **`ざわ・・・`**の文字を表現する
この程度であれば**`stroke()`**でも再現できるため、画像やSVGは使っていません。**`moveTo()`** **`lineTo()`**で線を細く引いていきます。

> 結果的に画像を使った方が動作が軽くなるかもしれませんが・・・


##**`ざわ・・・`**の大きさと位置をランダムに設定する
1.のように作成したため、座標位置や大きさは**`scale()`** **`translate()`**で表現しています。

> 上記メソッドを使うと**座標軸が変更**されるため、再描画の際には座標軸のリセットやCanvasの実サイズに注意します


##一定時間ごとに左or右へループさせる
どちらへ流れるかは引数で設定できるようにしました。座標軸のズレに気をつけていれば、特に難しいことは無いと思います。





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

> 大きさによってスピードを変えることで視差効果を付けたのは良いものの、縁取りした時の文字の重なり順が・・・。x(


**[おまけ]**
![super_zawazawa.png](https://qiita-image-store.s3.amazonaws.com/0/58436/ae0b4eff-d741-7935-2493-7b9cf699edb5.png)

[Canvasで漫画にあるような集中線を書いてみた](http://qiita.com/nekoneko-wanwan/items/0911a59bf835d5b9e35a)と組み合わせて、すごくうるさい画像を作成してみました（Gifアニメが大変重たいので静止画です）

> http://nekoneko-wanwan.github.io/demo/canvas/effect/zawazawa2/