みかづきブログ その3

3ヶ月つづけてみました。

Canvasで落書き 2.2.1

前回つくったもの はパスが増えてくるとFPSがガッツリさがってしまうという問題があったため、ロジックに修正を加えました。


Canvasで落書き 2.2.0 - みかづきブログ その3

マウスアップした際にCanvasの状態を1度ビットマップ化することによって、パスを現在引いているLineのものだけにしています。
本当ならLineManagerクラスをつくりかえたほうが良いのですが、他のコードにも影響が出てしまうため、メインロジックの改修にとどめました。

DEMO


main.js

(function(win, doc, ns) {

    "use strict";
    
    var SIZE = 10,
        _getAve = _buildGetAve(10);
    
    function _buildGetAve(opt_maxLength) {
        var index     = 0,
            array     = [],
            maxLength = opt_maxLength || 10;

        function _ave() {
            var length = array.length,
                sum    = 0,
                i;

            for (i = 0; i < length; ++i) {
                sum += array[i];
            }

            return sum / length;
        }

        function getAve(val) {
            array[index] = val;
            index = (index + 1) % maxLength;

            return _ave();
        }

        return getAve;
    }
    
    function _getSize(l) {
        var MAX_DISTANCE = 100,
            width;
        
        if (l > MAX_DISTANCE) {
            width =  1;
        } else {
            width =  (MAX_DISTANCE - l) * SIZE / MAX_DISTANCE;
        }
        
        _getAve(width);
        
        return _getAve(width);
    }
    
    var ticker      = new ns.Ticker(60),
        lineManager = ns.LineManager.getInstance(),
        canvas      = doc.getElementById("canvas"),
        ctx         = canvas.getContext("2d"),
        sub        = doc.createElement("canvas"), // スナップショット保存用のCanvas
        subCtx  = sub.getContext("2d"),
        fps         = doc.getElementById("fps"),
        START_EVENT = "mousedown",
        MOVE_EVENT  = "mousemove",
        END_EVENT   = "mouseup";

    setup();

    function setup() {
        addSketchEventForCanvas(canvas);

        ticker.addEventListener("tick", function(evt) {
            fps.innerHTML = evt.measuredFPS;
            update(evt.delta);
            draw();
        });

        canvas.width = sub.width = win.innerWidth;
        canvas.height = sub.height = win.innerHeight;
        
        ticker.start();
    }

    function update(delta) {
    }

    function draw() {
        canvas.width = win.innerWidth;
        canvas.height = win.innerHeight;

        ctx.drawImage(sub, 0, 0);

        ctx.save();
            ctx.fillStyle = ctx.strokeStyle = "rgba(0, 0, 255, 1)";
            lineManager.drawQuadraticCurve(ctx);
        ctx.restore();
    }

    function addSketchEventForCanvas(elm) {
        var throttle     = new ns.Throttle(10, handleThrottleMoveEvent),
            ctx          = elm.getContext("2d"),
            positionList = [],
            lastPoint = null,
            index = 0;
        
        elm.addEventListener(START_EVENT, handleStartEvent, false);
        doc.addEventListener(END_EVENT,   handleEndEvent,   false);

        function handleThrottleMoveEvent(evt) {
            var currentPoint = new ns.Point(evt.offsetX, evt.offsetY);

            currentPoint.setSize(_getSize(ns.Point.getDistance(lastPoint, currentPoint)));
            lastPoint = currentPoint;
            lineManager.addPoint(currentPoint);
        }
        
        function handleStartEvent(evt) {
            lastPoint = new ns.Point(evt.offsetX, evt.offsetY, SIZE / 2);
  
            lineManager.push(new ns.Line(lastPoint));
            elm.addEventListener(MOVE_EVENT, handleMoveEvent, false);
        }
        
        function handleMoveEvent(evt) {
            throttle.fireEvent(evt);
        }
        
        function handleEndEvent(evt) {
            // 保存用Canvasをリサイズ&クリア
            sub.width = win.innerWidth;
            sub.height = win.innerHeight;

            // ビットマップとしてコピー
            subCtx.drawImage(canvas, 0, 0);

            // lineListを空にする
            lineManager.lineList = [];
            elm.removeEventListener(MOVE_EVENT, handleMoveEvent, false);
            lastPoint = null;
        }
    }

})(this, document, App);

これでたくさんLineを引いたときのFPSがかなり改善されました。
CodePenにはLineManagerに改修を加えたものをアップしています。


JavaScript

// NameSpace
(function(win) {

    "use strict";

    win.App = win.App || {};

})(this);

// EventDispatcher
(function(win, doc, ns) {

    "use strict";

    function _copyArray(array) {
        var newArray = [],
            i;

        try {
            newArray = [].slice.call(array);
        } catch(e) {
            for (i = 0; i < array.length; i++) {
                newArray.push(array[i]);
            }
        }

        return newArray;
    }

    function EventDispatcher() {
        this._events = {};
    }

    EventDispatcher.prototype.hasEventListener = function(eventName) {
        return !!this._events[eventName];
    };

    EventDispatcher.prototype.addEventListener = function(eventName, callback) {
        if (this.hasEventListener(eventName)) {
            var events = this._events[eventName],
                length = events.length,
                i;
    
            for (i = 0; i < length; i++) {
                if (events[i] === callback) {
                    return;
                }
            }
            events.push(callback);
        } else {
            this._events[eventName] = [callback];
        }

        return this;
    };

    EventDispatcher.prototype.removeEventListener = function(eventName, callback) {
        if (!this.hasEventListener(eventName)) {
            return;
        } else {
            var events = this._events[eventName],
                i = events.length,
                index;

            while (i--) {
                if (events[i] === callback) {
                    index = i;
                }
            }

            events.splice(index, 1);
        }

        return this;
    };

    EventDispatcher.prototype.fireEvent = function(eventName, opt_this, opt_arg) {
        if (!this.hasEventListener(eventName)) {
            return;
        } else {
            var events     = this._events[eventName],
                copyEvents = _copyArray(events),
                arg        = _copyArray(arguments),
                length     = events.length,
                i;

            // eventNameとopt_thisを削除
            arg.splice(0, 2);

            for (i = 0; i < length; i++) {
                copyEvents[i].apply(opt_this || this, arg);
            }
        }
    };

    ns.EventDispatcher = EventDispatcher;

})(this, document, App);

// Throttle
(function(win, doc, ns) {

    "use strict";


    function Throttle(opt_interval, opt_callback) {
        this._timer         = null;
        this._lastEventTime = 0;
        this._interval      = opt_interval || 500;
        this._callback      = opt_callback || function() {};
    }

    Throttle.prototype.setInterval = function(ms) {
        this._interval = ms;
    };

    Throttle.prototype.addEvent = function(fn) {
        this._callback = fn;
    };

    Throttle.prototype.fireEvent = function(opt_arg) {
        var _this         = this,
            currentTime   = new Date() - 0,
            timerInterval = this._interval / 10;

        clearTimeout(_this.timer);

        if (currentTime - _this._lastEventTime > _this._interval) {
            _fire();
        } else {
            _this.timer = setTimeout(_fire, timerInterval);
        }

        function _fire() {
            _this._callback.call(_this, opt_arg || null);
            _this._lastEventTime = currentTime;
        }
    };

    ns.Throttle = Throttle;

})(this, document, App);

// Ticker
(function(win, doc, ns) {

    "use strict";

    var _getNow = _buildGetNow(),
        _getAve = _buildGetAve();

    function _buildGetNow() {
        if (!!performance) {
            return function () {
                return performance.now();
            };
        } else {
            return function () {
                return new Date() - 0;
            };
        }
    }

    function _buildGetAve(opt_maxLength) {
        var index     = 0,
            array     = [],
            maxLength = opt_maxLength || 10;

        function _ave() {
            var length = array.length,
                sum    = 0,
                i;

            for (i = 0; i < length; ++i) {
                sum += array[i];
            }

            return sum / length;
        }

        function getAve(val) {
            array[index] = val;
            index = (index + 1) % maxLength;

            return _ave();
        }

        return getAve;
    }

    function Ticker(opt_fps) {
        var _this           = this,
            fps             = opt_fps || 24,
            interval        = 1000 / fps,
            timer           = null;

        _init();

        function _init() {
            ns.EventDispatcher.call(_this);
        }

        _this.fps             = fps;
        _this.interval        = interval;
        _this.timer           = timer;
        _this.beforeTime      = null;
    }

    Ticker.prototype = new ns.EventDispatcher();
    Ticker.prototype.constructor = Ticker;

    Ticker.prototype._check = function() {
        var _this       = this,
            currentTime = _getNow(),
            evt         = {};

        evt.delta       = currentTime - _this.beforeTime;
        evt.runTime     = currentTime - _this.startTime;
        evt.measuredFPS = _getAve(1000 / evt.delta);
        
        _this.fireEvent("tick", _this, evt);
        _this.beforeTime = currentTime;

        _this.timer = setTimeout(function() {
            _this._check();
        }, _this.interval);
    };

    Ticker.prototype.start = function() {
        var _this = this;

        _this.startTime = _this.beforeTime = _getNow();

        _this.timer = setTimeout(function() {
            _this._check();
        }, _this.interval);
    };


    Ticker.prototype.stop = function() {
        var _this = this;

        clearTimeout(_this.timer);
         _this.beforeTime = null;
    };

    ns.Ticker = Ticker;

})(this, document, App);

// Point
(function(win, doc, ns) {

    "use strict";

    function Point(x, y, opt_size) {
        var _this = this;

        _init();

        function _init() {
            ns.EventDispatcher.call(_this);
        }

        _this.x    = x;
        _this.y    = y;
        _this.size = opt_size || 1;
    }

    Point.prototype = new ns.EventDispatcher();
    Point.prototype.constructor = Point;

    Point.prototype.setSize = function(size) {
        var _this = this;
        
        _this.size = size;
    };
    
    Point.prototype.draw = function(ctx, opt_size) {
        var _this = this,
            size  = opt_size || _this.size || 1;

        ctx.save();
            ctx.beginPath();
            ctx.arc(_this.x, _this.y, size, 0, Math.PI * 2, false);
            ctx.fill();
            ctx.moveTo(_this.x, _this.y);
        ctx.restore();

        return _this;
    };

    Point.getDistance = function(pointA, pointB) {
         return Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2));
    };
    
    ns.Point = Point;

})(this, document, App);

// Line
(function(win, doc, ns) {

    "use strict";

    var Point = ns.Point;

    function Line(opt_point) {
        var _this = this,
            pointList = [];

        _init();

        function _init() {
            ns.EventDispatcher.call(_this);
            _this.pointList = pointList;
            
            if (opt_point) {
                _this.push(opt_point);
            }
        }
    }

    Line.prototype = new ns.EventDispatcher();
    Line.prototype.constructor = Line;

    Line.prototype.push = function(pointModel) {
        var _this = this;

        _this.pointList.push(pointModel);
    };

    Line.prototype.drawLine = function(ctx) {
        var _this     = this,
            pointList = _this.pointList,
            length    = pointList.length,
            i;

        ctx.save();
            ctx.lineCap  = "round";
            ctx.lineJoin = "round";

        if (length > 1) {
            for (i = 1; i < length; ++i) {
                ctx.beginPath();
                    ctx.moveTo(pointList[i - 1].x, pointList[i - 1].y);
                    ctx.lineTo(pointList[i].x, pointList[i].y);
                    ctx.lineWidth = pointList[i].size;
                ctx.stroke();
            }
        } else {
            pointList[0].draw(ctx);
        }

        ctx.restore();        
    };

    Line.prototype.drawQuadraticCurve = function(ctx) {
        var _this              = this,
            pointList          = _this.pointList,
            length             = pointList.length,
            quadraticPointList = [],
            lastIndex          = 0,
            i;

        ctx.save();
            ctx.lineCap  = "round";
            ctx.lineJoin = "round";

            if (length > 1) {
                quadraticPointList[lastIndex] = pointList[0];

                ctx.beginPath();
                    ctx.moveTo(quadraticPointList[0].x, quadraticPointList[0].y);

                    for (i = 1; i < length; ++i) {
                        quadraticPointList[++lastIndex] = new Point(
                            (quadraticPointList[lastIndex - 1].x + pointList[i].x) / 2,
                            (quadraticPointList[lastIndex - 1].y + pointList[i].y) / 2
                        );
                        quadraticPointList[++lastIndex] = (pointList[i]);
                            ctx.quadraticCurveTo(
                                quadraticPointList[i * 2 - 2].x,
                                quadraticPointList[i * 2 - 2].y,
                                quadraticPointList[i * 2 - 1].x,
                                quadraticPointList[i * 2 - 1].y
                            );
                        ctx.lineWidth = pointList[i].size;
                        ctx.stroke();
                        ctx.beginPath();
                            ctx.moveTo(quadraticPointList[i * 2 - 1].x, quadraticPointList[i * 2 - 1].y);
                    }
                    ctx.lineTo(quadraticPointList[lastIndex].x, quadraticPointList[lastIndex].y);
                ctx.stroke();
            } else {
                pointList[lastIndex].draw(ctx);
            }
        ctx.restore();
    };

    ns.Line = Line;

})(this, document, App);

// LineManager
(function(win, doc, ns) {

    "use strict";

    var instance;

    function getInstance() {
        if (!instance) {
            instance = new LineManager();
        }

        return instance;
    }

    function LineManager() {
        var _this = this,
            lineList = [];

        _init();

        function _init() {
            ns.EventDispatcher.call(_this);
        }

        _this.lineList = lineList;
    }

    LineManager.prototype = new ns.EventDispatcher();
    LineManager.prototype.constructor = LineManager;

    LineManager.prototype.push = function(lineModel) {
        var _this = this;

        _this.lineList.push(lineModel);
    };

    LineManager.prototype.addPoint = function(pointModel, opt_index) {
        var _this = this,
            index = opt_index || _this.lineList.length - 1;

        _this.lineList[index].push(pointModel);
    };

    LineManager.prototype.drawQuadraticCurve = function(ctx) {
        var _this     = this,
            lineList = _this.lineList,
            length    = lineList.length;

        // 最後に引いたLineのみ描画するように変更
        if (length) {
            lineList[length - 1].drawQuadraticCurve(ctx);
        }
    };

    ns.LineManager = {
        getInstance: getInstance
    };

})(this, document, App);

// main
(function(win, doc, ns) {

    "use strict";
    
    var SIZE = 10,
        _getAve = _buildGetAve(10);
    
    function _buildGetAve(opt_maxLength) {
        var index     = 0,
            array     = [],
            maxLength = opt_maxLength || 10;

        function _ave() {
            var length = array.length,
                sum    = 0,
                i;

            for (i = 0; i < length; ++i) {
                sum += array[i];
            }

            return sum / length;
        }

        function getAve(val) {
            array[index] = val;
            index = (index + 1) % maxLength;

            return _ave();
        }

        return getAve;
    }
    
    function _getSize(l) {
        var MAX_DISTANCE = 100,
            width;
        
        if (l > MAX_DISTANCE) {
            width =  1;
        } else {
            width =  (MAX_DISTANCE - l) * SIZE / MAX_DISTANCE;
        }
        
        _getAve(width);
        
        return _getAve(width);
    }
    
    var ticker      = new ns.Ticker(60),
        lineManager = ns.LineManager.getInstance(),
        canvas      = doc.getElementById("canvas"),
        ctx         = canvas.getContext("2d"),
        sub        = doc.createElement("canvas"),
        subCtx  = sub.getContext("2d"),
        fps         = doc.getElementById("fps"),
        START_EVENT = "mousedown",
        MOVE_EVENT  = "mousemove",
        END_EVENT   = "mouseup";

    setup();

    function setup() {
        addSketchEventForCanvas(canvas);

        ticker.addEventListener("tick", function(evt) {
            update(evt.delta);
            draw();
        });

        canvas.width = sub.width = win.innerWidth;
        canvas.height = sub.height = win.innerHeight;
        
        ticker.start();
    }

    function update(delta) {
    }

    function draw() {
        canvas.width = win.innerWidth;
        canvas.height = win.innerHeight;

        ctx.drawImage(sub, 0, 0);

        ctx.save();
            ctx.fillStyle = ctx.strokeStyle = "rgba(50, 50, 50, 1)";
            lineManager.drawQuadraticCurve(ctx);
        ctx.restore();
    }

    function addSketchEventForCanvas(elm) {
        var throttle     = new ns.Throttle(10, handleThrottleMoveEvent),
            ctx          = elm.getContext("2d"),
            positionList = [],
            lastPoint = null,
            index = 0;
        
        elm.addEventListener(START_EVENT, handleStartEvent, false);
        doc.addEventListener(END_EVENT,   handleEndEvent,   false);

        function handleThrottleMoveEvent(evt) {
            var currentPoint = new ns.Point(evt.offsetX, evt.offsetY);

            currentPoint.setSize(_getSize(ns.Point.getDistance(lastPoint, currentPoint)));
            lastPoint = currentPoint;
            lineManager.addPoint(currentPoint);
        }
        
        function handleStartEvent(evt) {
            lastPoint = new ns.Point(evt.offsetX, evt.offsetY, SIZE / 2);
  
            lineManager.push(new ns.Line(lastPoint));
            elm.addEventListener(MOVE_EVENT, handleMoveEvent, false);
        }
        
        function handleMoveEvent(evt) {
            throttle.fireEvent(evt);
        }
        
        function handleEndEvent(evt) {
            sub.width = win.innerWidth;
            sub.height = win.innerHeight;
            subCtx.drawImage(canvas, 0, 0);
            elm.removeEventListener(MOVE_EVENT, handleMoveEvent, false);
            lastPoint = null;
        }
    }

})(this, document, App);