From 05ca858bf44429b115c569ce789cc0df9eb2546b Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Mon, 1 Oct 2012 13:43:33 +0300 Subject: [PATCH 01/12] GL.create now allows specifying the canvas width and height in options: GL.create({ width:800, height:600}); --- src/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index a5dc78b..ba38b1a 100644 --- a/src/main.js +++ b/src/main.js @@ -10,8 +10,8 @@ var GL = { create: function(options) { options = options || {}; var canvas = document.createElement('canvas'); - canvas.width = 800; - canvas.height = 600; + 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) {} From 0d818623c72daff66cad876c659e07235340a1f3 Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Mon, 1 Oct 2012 17:16:33 +0300 Subject: [PATCH 02/12] Added GL.Shader.fromURL(vsURL, fsURL), using synchronous XMLHttpRequest --- src/shader.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/shader.js b/src/shader.js index 4ed6314..8db7d1e 100644 --- a/src/shader.js +++ b/src/shader.js @@ -263,3 +263,26 @@ 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, 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); +}; From 4461cf63a33d551ad75922d7c311b6d874c5c536 Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Tue, 16 Oct 2012 15:15:06 +0300 Subject: [PATCH 03/12] GL.create() now allows passing in a pre-existing canvas in options --- src/main.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main.js b/src/main.js index ba38b1a..f2caa82 100644 --- a/src/main.js +++ b/src/main.js @@ -4,14 +4,14 @@ 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 = options.width || 800; - canvas.height = options.height || 600; + var canvas = options.canvas || document.createElement('canvas'); 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) {} From 8c9352bc9b99f812cb34b0be01e350c61378c6eb Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Tue, 16 Oct 2012 15:28:13 +0300 Subject: [PATCH 04/12] default canvas size 800x600 added back in --- src/main.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index f2caa82..f8836b7 100644 --- a/src/main.js +++ b/src/main.js @@ -11,7 +11,12 @@ var GL = { // canvas. create: function(options) { options = options || {}; - var canvas = options.canvas || document.createElement('canvas'); + 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) {} From 7ca1b34b57a4e7fa73e1e90ea61701921c51c7bd Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Wed, 31 Oct 2012 16:10:51 +0200 Subject: [PATCH 05/12] Added a gl.onresize() callback, called when resizing the window in fullscreen mode --- src/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.js b/src/main.js index f8836b7..a06bebb 100644 --- a/src/main.js +++ b/src/main.js @@ -444,6 +444,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); From d02b4199f462df2a8be3dca8d21278dfa211a23e Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Wed, 31 Oct 2012 16:40:50 +0200 Subject: [PATCH 06/12] the depth buffer attachment is now optional in texture.drawTo() --- src/texture.js | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) 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(); From 63f3cc007d26e43adb0b4a6110fd38a376a43c89 Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Fri, 16 Nov 2012 14:33:34 +0200 Subject: [PATCH 07/12] Added mousewheel event support, gl.onmousewheel --- src/main.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main.js b/src/main.js index a06bebb..c3007e7 100644 --- a/src/main.js +++ b/src/main.js @@ -301,6 +301,12 @@ function addEventListeners() { if (gl.onmouseup) gl.onmouseup(e); e.preventDefault(); } + function mousewheel(e) { + gl = context; + e = augment(e); + if (gl.onmousewheel) gl.onmousewheel(e); + e.preventDefault(); + } function reset() { hasOld = false; } @@ -311,6 +317,8 @@ function addEventListeners() { 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(document, 'contextmenu', resetAll); From 9da5fc4016e3d4c89a1266bdf69fcfdc70b54f93 Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Tue, 20 Nov 2012 17:02:22 +0200 Subject: [PATCH 08/12] added factory method Shader.from(vsURLorID, fsURLorID) that tries to load the shaders from the given URLs, or failing that, from DOM elements by the given IDs --- src/shader.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shader.js b/src/shader.js index 8db7d1e..80e5c78 100644 --- a/src/shader.js +++ b/src/shader.js @@ -286,3 +286,11 @@ Shader.fromURL = function(vsURL, fsURL) { return new Shader(vsSource, fsSource); }; + +Shader.from = function(vsURLorID, fsURLorID) { + try { + return Shader.fromURL(vsURLorID, fsURLorID); + } catch (e) { + return new Shader(vsURLorID, fsURLorID); + } +} From 3e89bb4a7928e1edeab5414eda12732457d2ad87 Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Fri, 7 Dec 2012 17:22:13 +0200 Subject: [PATCH 09/12] Shader.from() failed with an obscure error message if shader compilation failed, now fixed --- src/shader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shader.js b/src/shader.js index 80e5c78..802549e 100644 --- a/src/shader.js +++ b/src/shader.js @@ -289,8 +289,8 @@ Shader.fromURL = function(vsURL, fsURL) { Shader.from = function(vsURLorID, fsURLorID) { try { - return Shader.fromURL(vsURLorID, fsURLorID); - } catch (e) { return new Shader(vsURLorID, fsURLorID); + } catch (e) { + return Shader.fromURL(vsURLorID, fsURLorID); } -} +}; From af6900ed0191a884f064ec189d702bcf7ea695a0 Mon Sep 17 00:00:00 2001 From: Tomi Aarnio Date: Fri, 7 Dec 2012 17:22:42 +0200 Subject: [PATCH 10/12] touch event handling --- src/main.js | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/main.js b/src/main.js index c3007e7..03c13f0 100644 --- a/src/main.js +++ b/src/main.js @@ -215,6 +215,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() { @@ -223,6 +224,7 @@ 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 @@ -267,6 +269,53 @@ function addEventListeners() { }; return e; } + + function augmentTouchEvent(original) { + var e = {}; + for (var name in original) { + if (typeof original[name] == 'function') { + e[name] = (function(callback) { + return function() { + callback.apply(original, arguments); + }; + })(original[name]); + } else { + e[name] = original[name]; + } + } + e.original = original; + + if (e.targetTouches.length > 0) { + var touch = e.targetTouches[0]; + e.x = touch.pageX; + e.y = touch.pageY; + + for (var obj = gl.canvas; obj; obj = obj.offsetParent) { + e.x -= obj.offsetLeft; + e.y -= obj.offsetTop; + } + if (hasOld) { + e.deltaX = e.x - oldX; + e.deltaY = e.y - oldY; + } else { + e.deltaX = 0; + e.deltaY = 0; + hasOld = true; + } + oldX = e.x; + oldY = e.y; + e.dragging = true; + } + + e.preventDefault = function() { + e.original.preventDefault(); + }; + e.stopPropagation = function() { + e.original.stopPropagation(); + }; + return e; + } + function mousedown(e) { gl = context; if (!isDragging()) { @@ -281,12 +330,14 @@ function addEventListeners() { if (gl.onmousedown) gl.onmousedown(e); e.preventDefault(); } + function mousemove(e) { gl = context; e = augment(e); if (gl.onmousemove) gl.onmousemove(e); e.preventDefault(); } + function mouseup(e) { gl = context; buttons[e.which] = false; @@ -301,19 +352,61 @@ function addEventListeners() { if (gl.onmouseup) gl.onmouseup(e); e.preventDefault(); } + function mousewheel(e) { gl = context; e = augment(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); @@ -321,6 +414,9 @@ function addEventListeners() { 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); } From dd458dad681099c4e940573ad037bbf6b50924c7 Mon Sep 17 00:00:00 2001 From: Tomi Pekka Aarnio Date: Tue, 11 Jun 2013 10:48:09 +0300 Subject: [PATCH 11/12] XHR GET now appends a random query string to the URI to avoid obsolete resources from being returned from a cache --- src/shader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader.js b/src/shader.js index 802549e..4c84a88 100644 --- a/src/shader.js +++ b/src/shader.js @@ -273,7 +273,7 @@ Shader.fromURL = function(vsURL, fsURL) { var XMLHttpRequestGet = function (uri) { var mHttpReq = new XMLHttpRequest(); - mHttpReq.open("GET", uri, false); + mHttpReq.open("GET", uri + "?" + Math.random(), false); mHttpReq.send(null); if (mHttpReq.status !== 200) { throw 'could not load ' + uri; From 3bfc770097552a99da901cb2a0105be603368307 Mon Sep 17 00:00:00 2001 From: Tomi Pekka Aarnio Date: Tue, 11 Jun 2013 13:01:01 +0300 Subject: [PATCH 12/12] Mouse and touch events now correctly positioned also if the GL canvas has a relative (percentage) width or height --- src/main.js | 87 ++++++++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/src/main.js b/src/main.js index 03c13f0..8b9cc7b 100644 --- a/src/main.js +++ b/src/main.js @@ -21,6 +21,7 @@ var GL = { 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(); @@ -225,12 +226,13 @@ 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') { @@ -244,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; @@ -260,59 +272,26 @@ 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 = {}; - for (var name in original) { - if (typeof original[name] == 'function') { - e[name] = (function(callback) { - return function() { - callback.apply(original, arguments); - }; - })(original[name]); - } else { - e[name] = original[name]; - } - } - e.original = original; - + var e = duplicateEvent(original); if (e.targetTouches.length > 0) { var touch = e.targetTouches[0]; e.x = touch.pageX; e.y = touch.pageY; - - for (var obj = gl.canvas; obj; obj = obj.offsetParent) { - e.x -= obj.offsetLeft; - e.y -= obj.offsetTop; - } - if (hasOld) { - e.deltaX = e.x - oldX; - e.deltaY = e.y - oldY; - } else { - e.deltaX = 0; - e.deltaY = 0; - hasOld = true; - } - oldX = e.x; - oldY = e.y; + addRelativeCoords(e); e.dragging = true; } - - e.preventDefault = function() { - e.original.preventDefault(); - }; - e.stopPropagation = function() { - e.original.stopPropagation(); - }; return e; } @@ -326,14 +305,14 @@ 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(); } @@ -348,14 +327,14 @@ 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 = augment(e); + e = augmentMouseEvent(e); if (gl.onmousewheel) gl.onmousewheel(e); e.preventDefault(); }