イナヅマTVログ

Canvas始めました – ボタンを作る

| 1件のコメント

Canvasでインタラクティブなことをするためにボタンを作るの巻。

できたことはできたけど、完成にはかなり遠いところにしかたどり着けませんでした。
ActionScriptのSimpleButtonのような汎用性を目指しましたが途中で挫折しました。

作るにあたって懸念していたmouseoverの処理にやはり悩まされました。

Button on Canvas

Canvas Click Demo Vol.2 ボタンを作った

画面左上の「赤い四角」がボタンです。
もっとかっこ良くしたかったのは言うまでもないのですが、これ以上コードが増えないようにということで許して下さい。
・白いところをクリックするとその地点に丸が作られます。
・ボタンをクリックするとCanvas上の丸が消えます。

HTML

<canvas id='canvas'></canvas>

JavaScript

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
 
var requestAnimationFrame = new function () {
    var s = 'equestAnimationFrame',
            w = window,
            r = w['r'+r] = w['r'+r] || w['webkitR'+r] || w['mozR'+r] || w['msR'+r] || w['oR'+r];
 
    return function (callback) {
        if (r) {
            return r(callback);
        };
        setTimeout(callback, 1000 / 60)
    };
};
/**
 *
 * @type {Object}
 */
var MouseAction = {
    CLICK: 'click',
    MOUSE_OVER: 'mouseover',
    MOUSE_OUT: 'mouseout',
    MOUSE_UP: 'mouseup',
    MOUSE_DOWN: 'mousedown',
    MOUSE_MOVE: 'mousemove',
    /**
     *
     * @param e:MouseEvent
     * @return {Object}
     */
    getOffset: function (e) {
        var x = y = 0;
        x = e.clientX - canvas.offsetLeft;
        y = e.clientY - canvas.offsetTop;
 
        return {x: x, y: y};
    }
};
/**
 *
 * @param canvas: CanvasElement
 * @param context: context 2d
 * @type {*}
 */
var Stage = (function (canvas, context) {
    var enable = false,
        mouse = {},
        event = '',
        callbacks = [],
        loops = [],
        event = '',
        onOver = function (e) {
            enable = true;
            canvas.addEventListener(MouseAction.MOUSE_MOVE, onMove);
            event = e;
        },
        onOut = function (e) {
            enable = false;
            canvas.removeEventListener(MouseAction.MOUSE_MOVE, onMove);
            event = e;
        },
        onMove = function (e) {
            enable = true;
            event = e;
            for (var i = 0, limit = callbacks.length; i < limit; i++) {
                callbacks[i](event);
            };
        },
        loop = function () {
            for (var i = 0, limit = loops.length; i < limit; i++) {
                loops[i](event);
            };
            requestAnimationFrame(loop);
        },
        stage = function () {
            canvas.addEventListener(MouseAction.MOUSE_OVER, onOver);
            canvas.addEventListener(MouseAction.MOUSE_OUT, onOut);
            canvas.addEventListener(MouseAction.MOUSE_MOVE, onMove);
            loop();
        },
        stageInstance = new stage();
    ;
    return {
        addListener: function (type, callback) {
            if (type != MouseAction.MOUSE_OVER && type != MouseAction.MOUSE_OUT) {
                canvas.addEventListener(type, callback, false);
            } else {
                callbacks.push(callback);
            };
        },
        removeListener: function (type, callback) {
            if (type != MouseAction.MOUSE_OVER && type != MouseAction.MOUSE_OUT) {
                canvas.removeEventListener(type, callback, false);
            };
        },
        addLoop: function (callback) {
            loops.push(callback);
        },
        clear: function () {
            context.clearRect(0,0,canvas.width,canvas.height);
        }
    };
}(canvas, context));
 
/**
 *
 * @param upState:Rect
 * @param overState:Rect
 * @param downState:rect
 * @Constructor
 */
var SimpleButton = function (upState, overState, downState) {
    if (!upState) {
        return;
    };
    if (!overState) {
        overState = upState;
    };
    if (!upState) {
        downState = overState || upState;
    };
    this.upState = upState;
    this.overState = overState;
    this.downState = downState;
 
    var bounce = {
        x: upState.x,
        y: upState.y,
        width: upState.width,
        height: upState.height
    };
    var clicks = [];
    var outsides = [];
    var current = upState;
    /**
     *
     * @return {Object}:Bounce
     */
    this.getBounce = function () {
        return bounce;
    };
    this.addListener = function (type, callback) {
        if (type != MouseAction.CLICK) {
            Stage.addListener(type, callback);
        } else {
            clicks.push(callback);
        }
    };
    /**
     *
     * @param state:Rect
     */
    var drawReserve = function (state) {
        current = state;
    };
    var body = document.body;
    /**
     *
     * @param e:MouseAction
     * @return {Boolean}
     */
    var hitTest = function (e) {
        var position = MouseAction.getOffset(e);
        return !(position.x < bounce.x || position.y < bounce.y || position.x > bounce.x + bounce.width || position.y > bounce.y + bounce.height);
    };
    var over = function (e) {
        if (!hitTest(e)) {
            cursor('auto');
            return;
        };
        cursor('pointer');
        drawReserve(overState);
    };
    var down = function (e) {
        if (!hitTest(e)) {
            return;
        };
        cursor('pointer');
        drawReserve(downState);
    };
    var up = function (e) {
        if (!hitTest(e)) {
            return;
        };
        for (var i = 0, limit = clicks.length; i < limit; i++) {
            clicks[i](event);
        };
        drawReserve(upState);
    };
    var out = function (e) {
        if (hitTest(e)) {
            return;
        };
        cursor('auto');
        drawReserve(upState);
    };
    var outside = function (e) {
        if (hitTest(e)) {
            return;
        };
        for (var i = 0, limit = outsides.length; i < limit; i++) {
            outsides[i](event);
        };
    };
    var cursor = function (cursorType) {
        body.style.cursor = cursorType;
    };
    this.addListener(MouseAction.MOUSE_OVER, over);
    this.addListener(MouseAction.MOUSE_OUT, out);
    this.addListener(MouseAction.MOUSE_DOWN, down);
    this.addListener(MouseAction.MOUSE_UP, up);
    this.addListener(MouseAction.MOUSE_UP, outside);
 
    drawReserve(upState);
 
    this.defaultView = function () {
        drawReserve(upState);
    };
    this.outsideClick = function (callback) {
        outsides.push(callback);
    };
    this.currentView = function () {
        Draw.rect(current);
    };
};
/**
 * Static
 */
Draw = {
    /**
     *
     * @param rect:Rect
     */
    rect: function (rect) {
        context.fillStyle = rect.color;
        context.fillRect(rect.x, rect.y, rect.width, rect.height)
    },
    /**
     *
     * @param circle:Circle
     */
    circle: function (circle) {
        context.fillStyle = circle.color;
        context.beginPath();
        context.arc(circle.x, circle.y, circle.radius, 0, Math.PI*2, true);
        context.closePath();
        context.fill();
    }
};
/**
 *
 * @param x:Number
 * @param y:Number
 * @param width:Number
 * @param height:Number
 * @param color:String
 * @Constructor
 */
var Rect = function (x, y, width, height, color) {
    this.color = color;
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
};
/**
 *
 * @param x
 * @param y
 * @param radius
 * @param color
 * @constructor
 */
var Circle = function (x, y, radius, color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
};
// ============================================
// 初期処理
(function (canvas, context) {
    var clearButton,
        circles = []
    ;
    // button
    (function () {
        var x = 10, y = 10, w = 120, h = 50;
        var up = new Rect(x, y, w, h, 'rgba(255, 0, 0, 1)');
        var over = new Rect(x, y, w, h, 'rgba(0, 0, 255, 1)');
        var down = new Rect(x, y, w, h, 'rgba(0, 0, 0, 1)');
        clearButton = new SimpleButton(up, over, down);
    }());
    /**
     *
     * @param rgb:uint
     * @return {Array}
     */
    var toRGB = function (rgb) {
        var r = g = b = 0;
 
        r = ((rgb >> 16) & 0xff);
        g = ((rgb >> 8) & 0xff);
        b = (rgb & 0xff);
 
        return [r, g, b];
    }
    /**
     *
     * @param e:MouseAction
     */
    var onStageClick = function (e) {
        e.preventDefault();
        e.stopPropagation();
 
        var position = MouseAction.getOffset(e);
        var color = toRGB( Math.floor( Math.random() * 0xffffff )).join(',');
        circles.push(new Circle(position.x, position.y, 15, 'rgba(' + color + ',.5)'));
 
        return false;
    };
 
    var onClearButtonClick = function (e) {
        e.preventDefault();
        e.stopPropagation();
 
        circles = [];
        Stage.clear();
        clearButton.currentView();
 
        return false;
    };
    // draw
    var update = function (e) {
        Stage.clear();
        for (var i = 0, limit = circles.length; i < limit; i++) {
            Draw.circle(circles[i]);
        };
        clearButton.currentView();
    };
 
    clearButton.addListener(MouseAction.CLICK, onClearButtonClick);
    clearButton.outsideClick(onStageClick);
    Stage.addLoop(update);
}(canvas, context));

Canvas始めました – マウスアクション(click)を使う最初の一歩で作ったCanvas上をクリックで丸を作るのに「クリアボタン」を付けるだけの簡単なお仕事のつもりだったのですが。
もう一回、よーく考えて作り直した方が良さそうです。
ボタン機能を実装したところは全て書き換えてしまいたいと思っています。
突っ込みどころの満載なコードですが公開します。

まだ試していませんがCreateJSあたりを使ったらもっと簡単に実装できるかもしれません。

CanvasのローレベルAPIを使って自力で実装していくのは裸足で富士山に登っているように思えててしまいます。
わけなく実装してしまう人が少なく無いことはわかっていますが、自分の力ではかなり大変そうです。

【教訓】
画面の再描画は一カ所にする。
・Processingのdrawみたいな感じにここでしか再描画をしないと決めておいた方がメンテナンス性も上がるようです。

再描画のライフサイクル・イベントは一つにする。
・上記にも共通するのですが、mousemoveとrequestAnimationFrameの両方で再描画するとコードの複雑さが増すだけのように感じました。
どちらかに決めて描画処理をした方が良いように思います。

無理せず良くできたライブラリを使う。
・使わなくても良いスキルをお持ちの方は別です。
・ローレベルAPIだけだと、さすがにこれはかなり手強いことがわかりました。これからはAdobe一押しのCreateJSを触ってみようかな。

【お断り】
今回は無理矢理ひとつのCanvasで何もかも実現しようとしましたが、良い選択肢とは言えません。
ボタンだけ別のCanvasに描画し重ねた方が処理は簡単になると思います。

あるいはボタンはCanvasを使わずにHTMLElementにし重ねた方が処理はもっと楽になると思いますし、デザインの自由度も上がることでしょう。

1件のコメント

  1. ピンバック: Canvas始めました - Canvas APIで遊んで(遊ばれて)わかったことをだらだらと « イナヅマTVログ

コメントを残す

必須欄は * がついています