UNPKG

kami-context

Version:
323 lines (287 loc) 11.1 kB
/** * Creates a WebGL context which attempts to manage the * state of kami objects that are created with this as a * parameter. * * @module kami-context */ var Class = require('klasse'); var Signal = require('signals'); var getContext = require('webgl-context'); /** * A thin wrapper around WebGLRenderingContext which handles * context loss and restore with various rendering objects (textures, * shaders and buffers). This also handles general viewport management. * * If the `canvas` option isn't specified, a new canvas will be created. * * If `gl` is specified and is an instance of WebGLRenderingContext, the `canvas` * and `attributes` options will be ignored and we will use `gl` without fetching another `getContext`. * Providing a canvas that has `getContext('webgl')` already called will not cause * errors, but in certain debuggers (e.g. Chrome WebGL Inspector), only the latest * context will be traced. * * If `handleContextLoss` is true (default), this will attempt to re-create the context * manually (i.e. no "Rats! WebGL hit a snag!" message), by iterating through all 'managed objects' * and calling create() on them. * * @class WebGLContext * @constructor * @param {Object} options some options for creation * @param {Number} options.width the width of the GL canvas * @param {Number} options.height the height of the GL canvas * @param {HTMLCanvasElement} options.canvas the optional DOM canvas element * @param {Object} options.attributes an object containing context attribs which * will be used during GL initialization * @param {WebGLRenderingContext} options.gl the already-initialized GL context to use * @param {Boolean} options.handleContextLoss whether to try and manage context loss, default true */ var WebGLContext = new Class({ initialize: function WebGLContext(options) { if (!(this instanceof WebGLContext)) return new WebGLContext(options); options = options||{}; var width = options.width; var height = options.height; var view = options.canvas; var gl = options.gl; var contextAttributes = options.contextAttributes; this.handleContextLoss = typeof options.handleContextLoss === "boolean" ? options.handleContextLoss : true; this.usePixelRatio = options.usePixelRatio !== false; /** * The list of rendering objects (shaders, VBOs, textures, etc) which are * currently being managed. Any object with a "create" method can be added * to this list. Upon destroying the rendering object, it should be removed. * See addManagedObject and removeManagedObject. * * @property {Array} managedObjects */ this.managedObjects = []; /** * The actual GL context. You can use this for * raw GL calls or to access GLenum constants. This * will be updated on context restore. While the WebGLContext * is not `valid`, you should not try to access GL state. * * @property gl * @type {WebGLRenderingContext} */ this.gl = null; //WebGLInspector in chrome swaps WebGLRenderingContext to //CaptureContext, with a 'rawgl' property var rawgl = (gl && gl.rawgl) ? gl.rawgl : gl; //if the user specified a GL context.. if (rawgl && typeof window.WebGLRenderingContext !== "undefined" && rawgl instanceof window.WebGLRenderingContext) { view = gl.canvas; this.gl = gl; this.valid = true; contextAttributes = undefined; //just ignore new attribs... } /** * The canvas DOM element for this context. * @property {HTMLCanvasElement} canvas */ this.canvas = view || document.createElement("canvas"); /** * The width of this canvas. * * @property width * @type {Number} */ if (typeof width==="number") this.width = this.canvas.width = width; else //if no size is specified, use canvas size this.width = this.canvas.width; /** * The height of this canvas. * @property height * @type {Number} */ if (typeof height==="number") this.height = this.canvas.height = height; else //if no size is specified, use canvas size this.height = this.canvas.height; /** * The context attributes for initializing the GL state. This might include * anti-aliasing, alpha settings, verison, and so forth. * * @property {Object} contextAttributes */ this.contextAttributes = contextAttributes; /** * Whether this context is 'valid', i.e. renderable. A context that has been lost * (and not yet restored) or destroyed is invalid. * * @property {Boolean} valid */ this.valid = false; /** * A signal dispatched when GL context is lost. * * The first argument passed to the listener is the WebGLContext * managing the context loss. * * @event {Signal} lost */ this.lost = new Signal(); /** * A signal dispatched when GL context is restored, after all the managed * objects have been recreated. * * The first argument passed to the listener is the WebGLContext * which managed the restoration. * * This does not gaurentee that all objects will be renderable. * For example, a Texture with an ImageProvider may still be loading * asynchronously. * * @event {Signal} restored */ this.restored = new Signal(); //setup context lost and restore listeners this.canvas.addEventListener("webglcontextlost", function (ev) { if (this.handleContextLoss) ev.preventDefault(); this._contextLost(ev); }.bind(this)); this.canvas.addEventListener("webglcontextrestored", function (ev) { if (this.handleContextLoss) ev.preventDefault(); this._contextRestored(ev); }.bind(this)); if (!this.valid) //would only be valid if WebGLRenderingContext was passed this._initContext(); this.resize(this.width, this.height); }, _initContext: function() { var err = ""; this.valid = false; this.gl = getContext({ canvas: this.canvas, attributes: this.contextAttributes }); if (this.gl) { this.valid = true; } else { throw new Error("WebGL Context Not Supported -- try enabling it or using a different browser"); } }, /** * Updates the width and height of this WebGL context, resizes * the canvas view, and calls gl.viewport() with the new size. * * By default, this tries to acommodate for retina sizes by scaling the * canvas based on the device pixel ratio (i.e. twice the size of width/height), * and then down-scaling the canvas via CSS styling. You can disable this by passing * usePixelRatio as `false` in the constructor, or setting it to false before resizing. * * @method resize * @param {Number} width the new width * @param {Number} height the new height */ resize: function(width, height) { var dpr = this.usePixelRatio ? window.devicePixelRatio : 1; this.width = width; this.height = height; this.canvas.width = width * dpr; this.canvas.height = height * dpr; if (this.usePixelRatio) { this.canvas.style.width = width + 'px'; this.canvas.style.height = height + 'px'; } var gl = this.gl; gl.viewport(0, 0, this.width, this.height); }, /** * (internal use) * A managed object is anything with a "create" function, that will * restore GL state after context loss. * * @private * @method addManagedObject * @param {[type]} tex [description] */ addManagedObject: function(obj) { this.managedObjects.push(obj); }, /** * (internal use) * Removes a managed object from the cache. This is useful to destroy * a texture or shader, and have it no longer re-load on context restore. * * Returns the object that was removed, or null if it was not found in the cache. * * @private * @method removeManagedObject * @param {Object} obj the object to be managed * @return {Object} the removed object, or null */ removeManagedObject: function(obj) { var idx = this.managedObjects.indexOf(obj); if (idx > -1) { this.managedObjects.splice(idx, 1); return obj; } return null; }, /** * Calls destroy() on each managed object, then removes references to these objects * and the GL rendering context. This also removes references to the view and sets * the context's width and height to zero. * * Attempting to use this WebGLContext or the GL rendering context after destroying it * will lead to undefined behaviour. * * @method destroy */ destroy: function() { for (var i=0; i<this.managedObjects.length; i++) { var obj = this.managedObjects[i]; if (obj && typeof obj.destroy === "function") obj.destroy(); } this.managedObjects.length = 0; this.valid = false; this.gl = null; this.canvas = null; this.width = this.height = 0; }, _contextLost: function(ev) { //all textures/shaders/buffers/FBOs have been deleted... //we need to re-create them on restore this.valid = false; this.lost.dispatch(this); }, _contextRestored: function(ev) { //first, initialize the GL context again this._initContext(); //now we recreate our shaders and textures if (this.handleContextLoss) { for (var i=0; i<this.managedObjects.length; i++) { if (this.managedObjects[i] && typeof this.managedObjects[i].create === "function") this.managedObjects[i].create(); } } //update GL viewport this.resize(this.width, this.height); this.restored.dispatch(this); }, /** * Backward-compatible view getter/setter. * Deprecated, may be removed in the future. * * @deprecated use canvas instead * @property {HTMLCanvas} view */ view: { get: function() { return this.canvas; }, set: function(canvas) { this.canvas = canvas; } } }); module.exports = WebGLContext;