JavaScript
HTML5
canvas

Canvasで円形(ほぼ)をマウスオーバーでぐにゃらせてみる

More than 3 years have passed since last update.

はじめに

以前投稿した下記記事の応用として、マウスオーバーに応じてCanvasで円形をぐにゃぐにゃと動かすコードを書いてみました。
※勉強のためライブラリ等を使わないで作ってみましたが、ぐにゃぐにゃ動かすのに使用したベジェ曲線: bezierCurveTo()はヒジョーに動作が想像しづらかったので、ある程度複雑な動き・シェイプを用いるには素直にライブラリを探したほうが良いですね...

Canvasアニメーションの要点
http://qiita.com/nekoneko-wanwan/items/33afa5d20264c83b2bd1

Canvasイベント操作まとめ
http://qiita.com/nekoneko-wanwan/items/9af7fb34d0fb7f9fc870

イメージ

deform.gif

実際の動きは↓ページで確認ができます
http://nekoneko-wanwan.github.io/demo/canvas/effect/deform_circle/

書いたコードの概要

イベント

  • canvasにmouseover / outイベントを付与
  • mouseoverでアニメーションフラグを立て、描画関数を実行
  • mouseoutでアニメーションフラグを折り、描画関数を実行

描画

  • 実行するたびに更新した値を返すクロージャ関数(elastic)を用意
  • elastic()では、渡された最大値/最小値の範囲でカウントアップ/ダウンを繰り返す
  • 描画関数(draw)を用意する
  • draw()では、円を描くのにベジェ曲線bezierCurveTo()を使用する。
  • bezierCurveTo()の引数には、elastic関数で返される値を入れる
  • draw()では、アニメーションフラグの有無で再描画を判断する(requestAnimationFrame or cancelAnimationFrame)
  • 読み込み時に一度draw()を実行

コード

sample.js
var cs = document.getElementById('myCanvas');
var ctx = cs.getContext('2d');
var isAnimation = false;

/* 
 * 実行する度にnumを更新した値を返すクロージャ
 * var ex = elastic(max, min).count(num);
 *
 * 値が最大値を超えるとカウントダウンを行なう
 * 値が最小値を下回るとカウントアップを行う
 * @param {num} max 最大値
 * @param {num} min 最小値
 * @return {num} num 渡した値に amount分を増減して返す(値は小数点第一以下を切り捨てる)
 */
function elastic(max, min) {
    var _max      = max || 100;
    var _min      = min || _max - 10;
    var isReverse = false;
    var amount    = 0.5;

    return {
        count: function(num) {
            return function() {
                if (num > _max) {
                    isReverse = true;
                } else if (num <= _min) {
                    isReverse = false;
                }
                if (isReverse) {
                    num -= amount;
                } else {
                    num += amount;
                }
                return num.toFixed(1);
            };
        }
    };
}

/* bezierCurveTo()用のデータを定義 */
var DATA = {
    // ここのx, yの値は、d.x, d.yと揃える
    x: elastic(410, 390).count(400),
    y: elastic(210, 200).count(200),
    a: {
        cp1x: elastic(410, 390).count(400),
        cp1y: elastic(100,  90).count( 90),
        cp2x: elastic(310, 300).count(300),
        cp2y: elastic(10,    0).count( 10),
        x:    elastic(210, 200).count(200),
        y:    elastic(10,    0).count( 10)
    },
    b: {
        cp1x: elastic(100, 100).count(100),
        cp1y: elastic(10,   10).count( 10),
        cp2x: elastic(10,   10).count( 10),
        cp2y: elastic(100, 100).count(100),
        x:    elastic(10,    0).count( 10),
        y:    elastic(210, 200).count(200)
    },
    c: {
        cp1x: elastic(10,    0).count( 10),
        cp1y: elastic(310, 300).count(300),
        cp2x: elastic(100,  90).count( 90),
        cp2y: elastic(410, 380).count(400),
        x:    elastic(210, 200).count(200),
        y:    elastic(410, 380).count(400)
    },
    d: {
        cp1x: elastic(310, 300).count(300),
        cp1y: elastic(400, 400).count(400),
        cp2x: elastic(400, 400).count(400),
        cp2y: elastic(300, 300).count(300),
        x:    elastic(410, 390).count(400),
        y:    elastic(210, 200).count(200)
    }
};

/* 描画関数 */
function draw() {
    ctx.clearRect(0, 0, cs.width, cs.height);
    ctx.beginPath();
    ctx.moveTo(DATA.x(), DATA.y());

   /* 
    * bezierCurveTo()
    * bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
    * @param {num} cp1: 制御点(コントロールポイント)の座標
    * @param {num} cp2: 制御点2の座標
    * @param {num} x  : 新しくパスに追加される点のx座標
    * @param {num} y  : 新しくパスに追加される点のy座標
    *
    * ※座標はCanvas要素の左上端からの距離となる
    */
    ctx.bezierCurveTo(DATA.a.cp1x(), DATA.a.cp1y(), DATA.a.cp2x(), DATA.a.cp2y(), DATA.a.x(), DATA.a.y());
    ctx.bezierCurveTo(DATA.b.cp1x(), DATA.b.cp1y(), DATA.b.cp2x(), DATA.b.cp2y(), DATA.b.x(), DATA.b.y());
    ctx.bezierCurveTo(DATA.c.cp1x(), DATA.c.cp1y(), DATA.c.cp2x(), DATA.c.cp2y(), DATA.c.x(), DATA.c.y());
    ctx.bezierCurveTo(DATA.d.cp1x(), DATA.d.cp1y(), DATA.d.cp2x(), DATA.d.cp2y(), DATA.d.x(), DATA.d.y());
    ctx.stroke();
    ctx.closePath();

    if(isAnimation) {
        requestAnimationFrame(draw);
    } else {
        cancelAnimationFrame(draw);
    }
}

function onMouseOver() {
    isAnimation = true;
    draw();
}

function onMouseOut() {
    isAnimation = false;
    draw();
}

cs.addEventListener('mouseover', onMouseOver, false);
cs.addEventListener('mouseout', onMouseOut, false);

draw();

おわりに

初期描画の円形が若干歪んでしまっていたり、シェイプの汎用性が無かったりとまだまだ改善点はありますが、何かの参考になれば幸いです。
※もちろんここはこうした方が良いよというのがあれば、ぜひぜひご教授くださいー。