canvas-app
Version:
sets up a retina-scaled canvas with render loop
232 lines (181 loc) • 6.54 kB
JavaScript
var isGL = require('is-webgl-context');
var getGL = require('webgl-context');
var debounce = require('debounce');
var addEvent = require('add-event-listener');
function isCanvasContext(obj) {
var ctx2d = typeof CanvasRenderingContext2D !== 'undefined' && obj instanceof CanvasRenderingContext2D;
return obj && (ctx2d || isGL(obj));
}
function CanvasApp(render, options) {
if (!(this instanceof CanvasApp))
return new CanvasApp(render, options);
//allow options to be passed as first argument
if (typeof render === 'object' && render) {
options = render;
render = null;
}
render = typeof render === 'function' ? render : options.onRender;
options = options||{};
options.retina = typeof options.retina === "boolean" ? options.retina : true;
var hasWidth = typeof options.width === "number",
hasHeight = typeof options.height === "number";
//if either width or height is specified, don't auto-resize to the window...
if (hasWidth || hasHeight)
options.ignoreResize = true;
options.width = hasWidth ? options.width : window.innerWidth;
options.height = hasHeight ? options.height : window.innerHeight;
var DPR = options.retina ? (window.devicePixelRatio||1) : 1;
//setup the canvas
var canvas,
context,
attribs = options.contextAttributes||{};
this.isWebGL = false;
//if user provided a context object
if (isCanvasContext(options.context)) {
context = options.context;
canvas = context.canvas;
}
//otherwise allow for a string to set one up
if (!canvas)
canvas = options.canvas || document.createElement("canvas");
canvas.width = options.width * DPR;
canvas.height = options.height * DPR;
if (!context) {
if (options.context === "webgl" || options.context === "experimental-webgl") {
context = getGL({ canvas: canvas, attributes: attribs });
if (!context) {
throw "WebGL Context Not Supported -- try enabling it or using a different browser";
}
} else {
context = canvas.getContext(options.context||"2d", attribs);
}
}
this.isWebGL = isGL(context);
if (options.retina) {
canvas.style.width = options.width + 'px';
canvas.style.height = options.height + 'px';
}
this.running = false;
this.width = options.width;
this.height = options.height;
this.canvas = canvas;
this.context = context;
this.onResize = options.onResize;
this._DPR = DPR;
this._retina = options.retina;
this._once = options.once;
this._ignoreResize = options.ignoreResize;
this._lastFrame = null;
this._then = Date.now();
this.maxDeltaTime = typeof options.maxDeltaTime === 'number' ? options.maxDeltaTime : 1000/24;
//FPS counter
this.fps = 60;
this._frames = 0;
this._prevTime = this._then;
if (!this._ignoreResize) {
options.resizeDebounce = typeof options.resizeDebounce === 'number'
? options.resizeDebounce : 50;
addEvent(window, "resize", debounce(function() {
this.resize(window.innerWidth, window.innerHeight);
}.bind(this), options.resizeDebounce, false));
addEvent(window, "orientationchange", function() {
this.resize(window.innerWidth, window.innerHeight);
}.bind(this));
}
if (typeof render === "function") {
this.onRender = render.bind(this);
} else {
//dummy render function
this.onRender = function (context, width, height, dt) { };
}
this.renderOnce = function() {
var now = Date.now();
var dt = Math.min(this.maxDeltaTime, (now-this._then));
this._frames++;
if (now > this._prevTime + 1000) {
this.fps = Math.round((this._frames * 1000) / (now - this._prevTime));
this._prevTime = now;
this._frames = 0;
}
if (!this.isWebGL) {
this.context.save();
this.context.scale(this._DPR, this._DPR);
} else {
this.context.viewport(0, 0, this.width * this._DPR, this.height * this._DPR);
}
this.onRender(this.context, this.width, this.height, dt);
if (!this.isWebGL)
this.context.restore();
this._then = now;
};
this._renderHandler = function() {
if (!this.running)
return;
if (!this._once) {
this._lastFrame = requestAnimationFrame(this._renderHandler);
}
this.renderOnce();
}.bind(this);
if (typeof options.onReady === "function") {
options.onReady.call(this, context, this.width, this.height);
}
}
Object.defineProperty(CanvasApp.prototype, 'retinaEnabled', {
set: function(v) {
this._retina = v;
this._DPR = this._retina ? (window.devicePixelRatio||1) : 1;
this.resize(this.width, this.height);
},
get: function() {
return this._retina;
}
});
Object.defineProperty(CanvasApp.prototype, 'deviceWidth', {
get: function() {
return this.width * this._DPR;
}
});
Object.defineProperty(CanvasApp.prototype, 'deviceHeight', {
get: function() {
return this.height * this._DPR;
}
});
CanvasApp.prototype.resetFPS = function() {
this._frames = 0;
this._prevTime = Date.now();
this._then = this._prevTime;
this.fps = 60;
};
CanvasApp.prototype.start = function() {
if (this.running)
return;
if (this._lastFrame)
cancelAnimationFrame(this._lastFrame);
//reset FPS counter
this.resetFPS();
this.running = true;
this._lastFrame = requestAnimationFrame(this._renderHandler);
};
CanvasApp.prototype.stop = function() {
if (this._lastFrame) {
cancelAnimationFrame(this._lastFrame);
this._lastFrame = null;
}
this.running = false;
};
CanvasApp.prototype.resize = function(width, height) {
var canvas = this.canvas;
this.width = width;
this.height = height;
canvas.width = this.width * this._DPR;
canvas.height = this.height * this._DPR;
if (this._retina) {
canvas.style.width = this.width + 'px';
canvas.style.height = this.height + 'px';
}
if (this._once)
requestAnimationFrame(this._renderHandler);
if (typeof this.onResize === "function")
this.onResize(this.width, this.height);
};
module.exports = CanvasApp;