diff --git a/src/main.js b/src/main.js index a5dc78b..8b9cc7b 100644 --- a/src/main.js +++ b/src/main.js @@ -4,18 +4,24 @@ var gl; var GL = { // ### Initialization // - // `GL.create()` creates a new WebGL context and augments it with more - // methods. The alpha channel is disabled by default because it usually causes - // unintended transparencies in the canvas. + // `GL.create()` creates a new WebGL context and augments it with + // more methods. Uses the HTML canvas given in 'options' or creates + // a new one if necessary. The alpha channel is disabled by default + // because it usually causes unintended transparencies in the + // canvas. create: function(options) { options = options || {}; - var canvas = document.createElement('canvas'); - canvas.width = 800; - canvas.height = 600; + var canvas = options.canvas; + if (!canvas) { + canvas = document.createElement('canvas'); + canvas.width = options.width || 800; + canvas.height = options.height || 600; + } if (!('alpha' in options)) options.alpha = false; try { gl = canvas.getContext('webgl', options); } catch (e) {} try { gl = gl || canvas.getContext('experimental-webgl', options); } catch (e) {} if (!gl) throw 'WebGL not supported'; + gl.viewport(0, 0, canvas.width, canvas.height); addMatrixStack(); addImmediateMode(); addEventListeners(); @@ -210,6 +216,7 @@ function addImmediateMode() { // augmented event object. The event object also has the properties `x`, `y`, // `deltaX`, `deltaY`, and `dragging`. function addEventListeners() { + var context = gl, oldX = 0, oldY = 0, buttons = {}, hasOld = false; var has = Object.prototype.hasOwnProperty; function isDragging() { @@ -218,12 +225,14 @@ function addEventListeners() { } return false; } - function augment(original) { - // Make a copy of original, a native `MouseEvent`, so we can overwrite - // WebKit's non-standard read-only `x` and `y` properties (which are just - // duplicates of `pageX` and `pageY`). We can't just use - // `Object.create(original)` because some `MouseEvent` functions must be - // called in the context of the original event object. + + // Make a copy of original, a native `MouseEvent`, so we can overwrite + // WebKit's non-standard read-only `x` and `y` properties (which are just + // duplicates of `pageX` and `pageY`). We can't just use + // `Object.create(original)` because some `MouseEvent` functions must be + // called in the context of the original event object. + // + function duplicateEvent(original) { var e = {}; for (var name in original) { if (typeof original[name] == 'function') { @@ -237,12 +246,22 @@ function addEventListeners() { } } e.original = original; - e.x = e.pageX; - e.y = e.pageY; + e.preventDefault = function() { + e.original.preventDefault(); + }; + e.stopPropagation = function() { + e.original.stopPropagation(); + }; + return e; + } + + function addRelativeCoords(e) { for (var obj = gl.canvas; obj; obj = obj.offsetParent) { e.x -= obj.offsetLeft; e.y -= obj.offsetTop; } + e.x = Math.round(e.x * gl.canvas.width / gl.canvas.offsetWidth); + e.y = Math.round(e.y * gl.canvas.height / gl.canvas.offsetHeight); if (hasOld) { e.deltaX = e.x - oldX; e.deltaY = e.y - oldY; @@ -253,15 +272,29 @@ function addEventListeners() { } oldX = e.x; oldY = e.y; + } + + function augmentMouseEvent(original) { + var e = duplicateEvent(original); + e.x = e.pageX; + e.y = e.pageY; + addRelativeCoords(e); e.dragging = isDragging(); - e.preventDefault = function() { - e.original.preventDefault(); - }; - e.stopPropagation = function() { - e.original.stopPropagation(); - }; return e; } + + function augmentTouchEvent(original) { + var e = duplicateEvent(original); + if (e.targetTouches.length > 0) { + var touch = e.targetTouches[0]; + e.x = touch.pageX; + e.y = touch.pageY; + addRelativeCoords(e); + e.dragging = true; + } + return e; + } + function mousedown(e) { gl = context; if (!isDragging()) { @@ -272,16 +305,18 @@ function addEventListeners() { off(gl.canvas, 'mouseup', mouseup); } buttons[e.which] = true; - e = augment(e); + e = augmentMouseEvent(e); if (gl.onmousedown) gl.onmousedown(e); e.preventDefault(); } + function mousemove(e) { gl = context; - e = augment(e); + e = augmentMouseEvent(e); if (gl.onmousemove) gl.onmousemove(e); e.preventDefault(); } + function mouseup(e) { gl = context; buttons[e.which] = false; @@ -292,22 +327,75 @@ function addEventListeners() { on(gl.canvas, 'mousemove', mousemove); on(gl.canvas, 'mouseup', mouseup); } - e = augment(e); + e = augmentMouseEvent(e); if (gl.onmouseup) gl.onmouseup(e); e.preventDefault(); } + + function mousewheel(e) { + gl = context; + e = augmentMouseEvent(e); + if (gl.onmousewheel) gl.onmousewheel(e); + e.preventDefault(); + } + + function touchstart(e) { + resetAll(); + // Expand the event handlers to the document to handle dragging off canvas. + on(document, 'touchmove', touchmove); + on(document, 'touchend', touchend); + off(gl.canvas, 'touchmove', touchmove); + off(gl.canvas, 'touchend', touchend); + gl = context; + e = augmentTouchEvent(e); + if (gl.ontouchstart) gl.ontouchstart(e); + e.preventDefault(); + } + + function touchmove(e) { + gl = context; + if (e.targetTouches.length === 0) { + touchend(e); + } + e = augmentTouchEvent(e); + if (gl.ontouchmove) gl.ontouchmove(e); + e.preventDefault(); + } + + function touchend(e) { + // Shrink the event handlers back to the canvas when dragging ends. + off(document, 'touchmove', touchmove); + off(document, 'touchend', touchend); + on(gl.canvas, 'touchmove', touchmove); + on(gl.canvas, 'touchend', touchend); + gl = context; + e = augmentTouchEvent(e); + if (gl.ontouchend) gl.ontouchend(e); + e.preventDefault(); + } + function reset() { hasOld = false; } + function resetAll() { buttons = {}; hasOld = false; } + + // We can keep mouse and touch events enabled at the same time, + // because Google Chrome will apparently never fire both of them. + on(gl.canvas, 'mousedown', mousedown); on(gl.canvas, 'mousemove', mousemove); on(gl.canvas, 'mouseup', mouseup); + on(gl.canvas, 'mousewheel', mousewheel); + on(gl.canvas, 'DOMMouseScroll', mousewheel); on(gl.canvas, 'mouseover', reset); on(gl.canvas, 'mouseout', reset); + on(gl.canvas, 'touchstart', touchstart); + on(gl.canvas, 'touchmove', touchmove); + on(gl.canvas, 'touchend', touchend); on(document, 'contextmenu', resetAll); } @@ -439,6 +527,7 @@ function addOtherMethods() { options.near || 0.1, options.far || 1000); gl.matrixMode(gl.MODELVIEW); } + if (gl.onresize) gl.onresize(); if (gl.ondraw) gl.ondraw(); } on(window, 'resize', resize); diff --git a/src/shader.js b/src/shader.js index 4ed6314..4c84a88 100644 --- a/src/shader.js +++ b/src/shader.js @@ -263,3 +263,34 @@ Shader.prototype = { return this; } }; + +// ### GL.Shader.fromURL(vsURL, fsURL) +// +// Compiles a shader program using the provided vertex and fragment +// shaders. The shaders are loaded synchronously from the given URLs. +// +Shader.fromURL = function(vsURL, fsURL) { + + var XMLHttpRequestGet = function (uri) { + var mHttpReq = new XMLHttpRequest(); + mHttpReq.open("GET", uri + "?" + Math.random(), false); + mHttpReq.send(null); + if (mHttpReq.status !== 200) { + throw 'could not load ' + uri; + } + return mHttpReq.responseText; + }; + + var vsSource = XMLHttpRequestGet(vsURL); + var fsSource = XMLHttpRequestGet(fsURL); + + return new Shader(vsSource, fsSource); +}; + +Shader.from = function(vsURLorID, fsURLorID) { + try { + return new Shader(vsURLorID, fsURLorID); + } catch (e) { + return Shader.fromURL(vsURLorID, fsURLorID); + } +}; diff --git a/src/texture.js b/src/texture.js index 8eee952..97d7507 100644 --- a/src/texture.js +++ b/src/texture.js @@ -30,7 +30,7 @@ function Texture(width, height, options) { this.format = options.format || gl.RGBA; this.type = options.type || gl.UNSIGNED_BYTE; gl.bindTexture(gl.TEXTURE_2D, this.id); - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, options.filter || options.magFilter || gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, options.filter || options.minFilter || gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, options.wrap || options.wrapS || gl.CLAMP_TO_EDGE); @@ -59,33 +59,40 @@ Texture.prototype = { gl.bindTexture(gl.TEXTURE_2D, null); }, - // ### .drawTo(callback) + // ### .drawTo(callback[, options]) // - // Render all draw calls in `callback` to this texture. This method sets up - // a framebuffer with this texture as the color attachment and a renderbuffer - // as the depth attachment. It also temporarily changes the viewport to the - // size of the texture. - // - // Example usage: + // Render all draw calls in `callback` to this texture. This method + // sets up a framebuffer with this texture as the color attachment + // and a renderbuffer as the depth attachment. The viewport is + // temporarily changed to the size of the texture. + // + // The depth buffer can be omitted via `options` as shown in the + // example below: // // texture.drawTo(function() { // gl.clearColor(1, 0, 0, 1); // gl.clear(gl.COLOR_BUFFER_BIT); - // }); - drawTo: function(callback) { + // }, { depth: false }); + drawTo: function(callback, options) { + + options = options || {}; var v = gl.getParameter(gl.VIEWPORT); + gl.viewport(0, 0, this.width, this.height); + framebuffer = framebuffer || gl.createFramebuffer(); - renderbuffer = renderbuffer || gl.createRenderbuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); - if (this.width != renderbuffer.width || this.height != renderbuffer.height) { - renderbuffer.width = this.width; - renderbuffer.height = this.height; - gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height); - } gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0); - gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); - gl.viewport(0, 0, this.width, this.height); + + if (!(options.depth === false)) { + renderbuffer = renderbuffer || gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); + if (this.width != renderbuffer.width || this.height != renderbuffer.height) { + renderbuffer.width = this.width; + renderbuffer.height = this.height; + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height); + } + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); + } callback();