前回つくったもの はパスが増えてくるとFPSがガッツリさがってしまうという問題があったため、ロジックに修正を加えました。
Canvasで落書き 2.2.0 - みかづきブログ その3
マウスアップした際にCanvasの状態を1度ビットマップ化することによって、パスを現在引いているLineのものだけにしています。
本当ならLineManagerクラスをつくりかえたほうが良いのですが、他のコードにも影響が出てしまうため、メインロジックの改修にとどめました。
DEMO
JavaScript
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);