Canvasで漫画にあるような吹き出しを書いてみた

#はじめに

Canvasを使用して、漫画で使われるような **吹き出し**表現を再現してみました。形自体にも汎用性があると思うので、吹き出し以外にも使えそうです。これで漫画表現シリーズは三作目となりました。（今までに書いたものは最下部を参照ください）今回書いた吹き出しは以下の3種類になります。

> **吹き出しのシッポ**（何ていうのか分からない。。発言者を示す細い三角のやつです）は想定していません。

**[動作イメージ]**
**パーッとした吹き出し**
![fukidashi_light.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/baf918f3-b9fe-feb0-9268-be4a3ffcbb44.gif)


**パンクな吹き出し**
![fukidashi_punk.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/227fa1ce-2a2c-0cd5-e952-0930dcb8364f.gif)


**モコモコな吹き出し**
![fukidashi_mokomoko.gif](https://qiita-image-store.s3.amazonaws.com/0/58436/6f948b56-af79-45f0-5243-b2411c3b8484.gif)



> なお実際の動作は以下で確認できます
> http://nekoneko-wanwan.github.io/demo/canvas/effect/fukidashi/light/
> http://nekoneko-wanwan.github.io/demo/canvas/effect/fukidashi/punk/
> http://nekoneko-wanwan.github.io/demo/canvas/effect/fukidashi/mokomoko/



#共通仕様
なるべく汎用的に使えるよう、色やサイズなどは柔軟に変えられることを目指しました。

- Canvasサイズに依存しない
- オブジェクトの位置や大きさ
- 線の長さや色
- 塗りつぶす色

などなど




#パーッとした吹き出し

**[生成イメージ]**
![光の？吹き出し.png](https://qiita-image-store.s3.amazonaws.com/0/58436/90a031a6-14a3-8342-934b-febc01d90dae.png)

##特長
- 正円から楕円まで対応できます
- 中心の背景を塗りつぶすことも抜くこともできます（抜く場合は`rgba()`を利用）


##ソースコード
```index.html
<canvas id="myCanvas" width="600" height="500"></canvas>
```
```canvas.js
/**
 * ランダムな整数を返す
 * @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;
    }
};

/**
 * 光の吹き出しを作成する
 * @param {obj}    ctx    : canvas context
 * @param {number} radiusX: 横半径
 * @param {number} radiusY: 縦半径
 * @param {number} num    : 角数
 * @param {number} cx     : 円の中心座標X
 * @param {number} cy     : 円の中心座標Y
 * @param {number} innerRadiusX : 中の円横半径
 * @param {number} innerRadiusY : 中の円縦半径
 * @param {number} addOuterLine : 外側の線のはみ出す上限
 * @param {number} addInnerLine : 内側の線のはみ出す上限
 * @param {color}  strokeStyle  : 線の色
 * @param {color}  fillStyle    : 塗りつぶしの色
 */

var createLightFukidashi = function(args) {
    var ctx = args.ctx;
    var deg = 0;
    var outerRandom;
    var innerRandom;

    // 塗りつぶしの円を描く
    // 楕円の計算が面倒なので普通に円を書いてscaleで曲げる
    // xを基準とする
    var ratio = args.innerRadiusY / args.innerRadiusX;

    ctx.scale(1, ratio);
    ctx.beginPath();
    ctx.arc(args.cx, args.cy / ratio, args.innerRadiusX, 0, Math.PI * 180, true);
    ctx.fillStyle = args.fillStyle;
    ctx.fill();
    ctx.setTransform(1, 0, 0, 1, 0, 0);

    // メインの線を描く
    ctx.beginPath();

    for (var i = 0; i < args.num; i++) {
        deg += 360 / args.num;
        outerRandom = getRandomInt(args.addOuterLine, 0);
        innerRandom = getRandomInt(args.addInnerLine, 0);

        ctx.moveTo(
            getCircumPos.x(deg, args.radiusX + outerRandom, args.cx),
            getCircumPos.y(deg, args.radiusY + outerRandom, args.cy)
        );
        ctx.lineTo(
            getCircumPos.x(deg, args.innerRadiusX - innerRandom, args.cx),
            getCircumPos.y(deg, args.innerRadiusY - innerRandom, args.cy)
        );
    }
    ctx.strokeStyle = args.strokeStyle;
    ctx.stroke();
};

var cs = document.getElementById('myCanvas');
var ctx = cs.getContext('2d');

// 引数の詳細はcreateLightFukidashi()のコメントアウトを参照
var config = {
    ctx: ctx,
    radiusX: 130,
    radiusY: 210,
    num: 300,
    cx: 160,
    cy: 250,
    innerRadiusX: 110,
    innerRadiusY: 190,
    addOuterLine: 30,
    addInnerLine: 0,
    strokeStyle: 'black',
    fillStyle: 'rgba(255,255,255,1)'
};
createLightFukidashi(config);
```


##作り方・考え方
- ランダムな整数を返す関数と円周上の座標と返す関数を用意します

> これは以降共通で使用します

- 吹き出し中心の塗りつぶし用の円を描画します
- 楕円に対応できるように、`scale()`で比率を元に変形させます
- 座標軸がズレるため、線を書いていく前に`setTransform()`で初期化します
- 円周上の座標を2つの半径で取得し、2点（それぞれx,y）を`moveTo`, `lineTo()`で結んでいきます




#パンクな吹き出し

**[生成イメージ]**
![パンクな吹き出し.png](https://qiita-image-store.s3.amazonaws.com/0/58436/d1e072f0-849f-a004-a7f1-aa2d16d046a5.png)


##特長
- 縦横の比率が変更できます
- 角の数や尖り具合が変更できます

##ソースコード
```index.html
<canvas id="myCanvas" width="600" height="500"></canvas>
```
```canvas.js
// ランダムな整数を返す（...省略）
// 円周上の座標を返す（...省略）

/**
 * 激しい吹き出しを作成する
 * @param {obj}    ctx    : canvas context
 * @param {number} radiusX: 縦半径
 * @param {number} radiusY: 横半径
 * @param {number} num    : 角数
 * @param {number} cx     : 円の中心座標X
 * @param {number} cy     : 円の中心座標Y
 * @param {number} punkLineMax : パンクの最大の高さ
 * @param {number} punkLineMin : パンクの最小の高さ
 * @param {color}  fillStyle   : 塗りつぶしの色
 * @param {color}  strokeStyle : 線の色
 * @param {number} lineWidth   : 線の太さ
 */
var createPunkFukidashi = function(args) {
    var ctx = args.ctx;
    var deg = 0;
    var addDeg = 360 / args.num;
    var random;
    var beginX, beginY, endX, endY;
    var cp1x, cp2x, cp1y, cp2y;

    // 共通設定
    ctx.beginPath();
    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';
    ctx.fillStyle = args.fillStyle;
    ctx.strokeStyle = args.strokeStyle;
    ctx.lineWidth = args.lineWidth;

    for (var i = 0; i < args.num; i++) {
        deg += addDeg;
        random = getRandomInt(args.punkLineMax, args.punkLineMin);

        // 始点・終点
        beginX = getCircumPos.x(deg, args.radiusX, args.cx);
        beginY = getCircumPos.y(deg, args.radiusY, args.cy);
        endX   = getCircumPos.x(deg + addDeg, args.radiusX, args.cx);
        endY   = getCircumPos.y(deg + addDeg, args.radiusY, args.cy);

        // 制御値
        cp1x = getCircumPos.x(deg, args.radiusX - random, args.cx);
        cp1y = getCircumPos.y(deg, args.radiusY - random, args.cy);
        cp2x = getCircumPos.x(deg + addDeg, args.radiusX - random, args.cx);
        cp2y = getCircumPos.y(deg + addDeg, args.radiusY - random, args.cy);

        // 開始点と最終点のズレを調整する
        if (i === 0) {
            ctx.arcTo(beginX, beginY, endX, endY, args.punkLineMax);
        }

        ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY);
    }

    ctx.fill();
    ctx.stroke();
};

var cs = document.getElementById('myCanvas');
var ctx = cs.getContext('2d');
var csWidth = cs.width;
var csHeight = cs.height;

// 引数の詳細はcreatePunkFukidashi()のコメントアウトを参照
var config = {
    ctx: ctx,
    radiusX: 120,
    radiusY: 80,
    num: 18,
    cx: 120,
    cy: 100,
    punkLineMax: 30,
    punkLineMin: 20,
    fillStyle: 'rgba(255,255,255,0.9)',
    strokeStyle: 'black',
    lineWidth: 3
};

createPunkFukidashi(config);
```



##作り方・考え方
- 円周上の座標を1つ取得します
- それの座標（x,y）の内側に、制御点として新たに座標を取得します
- 次の角度に対する座標と、その制御点を取得します
- それらを`bezierCurveTo()`で結んでいきます
- 開始点と最終点がずれるため、1回目は`arcTo()`で曲線を書いています


#モコモコな吹き出し

**[生成イメージ]**
![モコモコな吹き出し.png](https://qiita-image-store.s3.amazonaws.com/0/58436/dd064e09-6fc7-7c63-9527-ab0adf536d9d.png)

##特長
- パンクな吹き出しと同じです

##作り方・考え方
- 実はパンクな吹き出しの制御値を**内側から外側**へ変えただけで実現させています

```.js
// 制御値を変えただけ
cp1x = getCircumPos.x(deg, args.radiusX + random, args.cx);
cp1y = getCircumPos.y(deg, args.radiusY + random, args.cy);
cp2x = getCircumPos.x(deg + addDeg, args.radiusX + random, args.cx);
cp2y = getCircumPos.y(deg + addDeg, args.radiusY + random, args.cy);
```




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


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