Canvasで漫画にあるような集中線を書いてみた

#はじめに
[集中線GIFメーカー](https://speedline.herokuapp.com/)という面白いサービスにインスパイアを受け、Canvasで真似してみました。本家様のコードは一切見ずに作成したため、書き方やアニメーションの動作はまた違ったものになっているかと思います。結構良い感じにできたので公開してみます。

何かの参考にでもなれば幸いです。

**[動作イメージ]**
![line_anim1.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/35bb5a64-04bd-4eed-797d-d1209519ec4b.gif)
![line_anim2.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/09a367bf-68ba-db92-0161-ffeace8b45c4.gif)



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



#特長
なるべく拡張性・汎用性が高くなるように作成しました。

- Canvasサイズに依存しない
- 集中線の数が変更できる
- 集中線の色と太さが変更できる
- 集中線の中心部の大きさと位置が変更できる（位置については少しスクリプトの修正が必要）



#ソースコード

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

```canvas.js
/**
 * 集中線メーカー
 * @param {obj} canvas object
 * @param {number} centralX: 集中線を配置するx座標
 * @param {number} centralY: 集中線を配置するy座標
 * @param {number} lineWidth: 線の太さ（ランダムの上限）
 * @param {number} lineNum: 線の数
 * @param {number} circleRadiusMax: 集中線の円形の半径上限
 * @param {number} circleRadiusMin: 集中線の円形の半径下限
 * @param {string} lineColor: 集中線の色
 */

var focusLine = function(cs, centralX, centralY, lineWidth, lineNum, circleRadiusMax, circleRadiusMin, lineColor) {

    var ctx = cs.getContext('2d');
    var lines = [];

    // canvasの中心から角までの斜辺距離を円の半径とする
    // 中心をズラす場合は、もっと大きな値を設定してやる（引数でできるようにすべきか）
    var csRadius = Math.sqrt(Math.pow(cs.width / 2, 2) + Math.pow(cs.height / 2, 2)) | 0;

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

    /**
     * 円周上の座標を返す
     * @param d 角度
     * @param r 半径
     * @param cx, cy 中心座標
     */
    var getCircumPos = {
        // x座標
        x: function(d, r, cx) {
            return Math.cos(Math.PI / 180 * d) * r + cx;
        },
        // y座標
        y: function(d, r, cy) {
            return Math.sin(Math.PI / 180 * d) * r + cy;
        }
    };

    /**
     * @constructor
     */
    var Liner = function() {
        this.initialize();
    };
    Liner.prototype = {
        /* initialize()内の設定をsetPos()内へ移すと、アニメーションの動きが変わる */
        initialize: function() {
            this.deg       = getRandomInt(360, 0);
        },
        setPos: function() {
            this.moveDeg   = this.deg + (getRandomInt(lineWidth, 1) / 10);
            this.endRadius = getRandomInt(circleRadiusMax, circleRadiusMin);

            // 開始座標
            this.startPos = {
                x: getCircumPos.x(this.deg, csRadius, centralX),
                y: getCircumPos.y(this.deg, csRadius, centralY)
            };

            // 移動座標
            this.movePos = {
                x: getCircumPos.x(this.moveDeg, csRadius, centralX),
                y: getCircumPos.y(this.moveDeg, csRadius, centralY)
            };

            // 終了座標
            this.endPos = {
                x: getCircumPos.x(this.moveDeg, this.endRadius, centralX),
                y: getCircumPos.y(this.moveDeg, this.endRadius, centralY)
            };
        },
        update: function() {
            this.setPos();
        },
        draw: function() {
            ctx.beginPath();
            ctx.lineWidth = 1;
            ctx.fillStyle = lineColor;
            ctx.moveTo(this.startPos.x, this.startPos.y);
            ctx.lineTo(this.movePos.x,  this.movePos.y);
            ctx.lineTo(this.endPos.x,   this.endPos.y);
            ctx.fill();
            ctx.closePath();
        },
        render: function() {
            this.update();
            this.draw();
        }
    };

    /**
     * 線インスタンスの作成
     * @return lines[instance, instance...];
     */
    function createLines(num) {
        var i = 0;
        for (; i < num; i++) {
            lines[lines.length] = new Liner();
        }
    }

    /**
     * 描画
     */
    function render() {
        var i = 0;
        var l = lines.length;
        ctx.clearRect(0, 0, cs.width, cs.height);
        for (; i < l; i++) {
            lines[i].render();
        }
        setTimeout(function() {
            render();
        }, 100);
    }

    createLines(lineNum);
    render();
};


/**
 * focusLine()に渡す引数の設定
 */
var cs = document.getElementById('myCanvas');
var conf = {
    cx: cs.width / 2,
    cy: cs.height / 2,
    lineWidth: 30,
    lineNum: 200,
    crMax: 200, // 集中線の円形の半径上限
    crMin: 150, // 集中線の円形の半径下限
    color: 'black'
};

focusLine(cs, conf.cx, conf.cy, conf.lineWidth, conf.lineNum, conf.crMax, conf.crMin, conf.color);
```





#作り方・考え方

1. canvas要素の外側に点Aをランダムに配置
2. 点Aから少し角度をズラして、近くに点Bを配置
3. canvasの中心部Cに向けて線を引き、細長い三角形を作成
4. 1-3を沢山繰り返すことで、四方八方から中央に向かって線が伸びていく


## canvas要素の外側に点Aを配置

線が見切れてしまうことを防ぎます。

> 集中線を中央以外に配置する場合は、**外側の円周 `csRadius` をもっと大きく**します。（書いている途中で気づいたので、スクリプトは調整していません。。。）


**[イメージ]**
![sample1.png](https://qiita-image-store.s3.amazonaws.com/0/58436/1d66a3b7-c49c-a058-7246-3e7799ea0fb9.png)



##点Aから少し角度をズラして、近くに点Bを配置

角度を変えて結ぶことで線の太さを表現します。角度の増減をランダムにすることで自然な集中線を表現します

**[イメージ]**
![sample2.png](https://qiita-image-store.s3.amazonaws.com/0/58436/438d9c06-e411-bbfd-1440-e6cfeccef35d.png)



##canvasの中心部Cに向けて線を引き、細長い三角形を作成
点Aと点B、終点Cを結びます。ただし完全な中心点だと中央の余白が作れないため、中に小さな円を想定し、その円周上を終点Cとします。実際には三角形ごとに中の小円半径をランダムに変え、ギザギザの余白ができるようにします。

**[イメージ]**
![sample3.png](https://qiita-image-store.s3.amazonaws.com/0/58436/9974a0e9-20b4-2482-f084-4b23e9a6643d.png)

![sample4.png](https://qiita-image-store.s3.amazonaws.com/0/58436/4bd4569f-71d0-058f-a4b3-cd68c15f770c.png)



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

**[おまけ]**
![other1.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/4f370fb9-625d-5e62-31ef-912ee5a8c38f.gif)
![other2.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/2df9b6f0-3109-4e47-1328-4f5cde1cead8.gif)