diff --git a/core/Node.js b/core/Node.js index cf8c7b36..dfe139c6 100644 --- a/core/Node.js +++ b/core/Node.js @@ -641,8 +641,8 @@ Node.prototype.getComponent = function getComponent (index) { * * @method removeComponent * - * @param {Object} component An component that has previously been added - * using {@link Node#addComponent}. + * @param {Object} component A component that has previously been added + * using @{@link addComponent}. * * @return {Node} this */ @@ -661,6 +661,33 @@ Node.prototype.removeComponent = function removeComponent (component) { return component; }; +/** +* Removes a node's subscription to a particular UIEvent. All components +* on the node will have the opportunity to remove all listeners depending +* on this event. +* +* @method +* +* @param {String} eventName the name of the event +* +* @return {Node} this +*/ +Node.prototype.removeUIEvent = function removeUIEvent (eventName) { + var UIEvents = this.getUIEvents(); + var components = this._components; + var component; + + var index = UIEvents.indexOf(eventName); + if (index !== -1) { + UIEvents.splice(index, 1); + for (var i = 0, len = components.length ; i < len ; i++) { + component = components[i]; + if (component && component.onRemoveUIEvent) component.onRemoveUIEvent(eventName); + } + } + return this; +}; + /** * Subscribes a node to a UI Event. All components on the node * will have the opportunity to begin listening to that event @@ -670,7 +697,7 @@ Node.prototype.removeComponent = function removeComponent (component) { * * @param {String} eventName the name of the event * - * @return {undefined} undefined + * @return {Node} this */ Node.prototype.addUIEvent = function addUIEvent (eventName) { var UIEvents = this.getUIEvents(); @@ -685,6 +712,7 @@ Node.prototype.addUIEvent = function addUIEvent (eventName) { if (component && component.onAddUIEvent) component.onAddUIEvent(eventName); } } + return this; }; /** @@ -1105,7 +1133,7 @@ Node.prototype.setOpacity = function setOpacity (val) { * Sets the size mode being used for determining the node's final width, height * and depth. * Size modes are a way to define the way the node's size is being calculated. - * Size modes are enums set on the {@link Size} constructor (and aliased on + * Size modes are enums set on the @{@link Size} constructor (and aliased on * the Node). * * @example diff --git a/renderers/Context.js b/renderers/Context.js index 0481c48d..c90e97ef 100644 --- a/renderers/Context.js +++ b/renderers/Context.js @@ -147,7 +147,7 @@ Context.prototype.getRootSize = function getRootSize() { Context.prototype.initWebGL = function initWebGL() { this.canvas = document.createElement('canvas'); this._rootEl.appendChild(this.canvas); - this.WebGLRenderer = new WebGLRenderer(this.canvas, this._compositor); + this.WebGLRenderer = new WebGLRenderer(this.canvas, this._compositor, this._domLayerEl); this.updateSize(); }; @@ -261,6 +261,16 @@ Context.prototype.receive = function receive(path, commands, iterator) { this.DOMRenderer.allowDefault(commands[++localIterator]); break; + case 'GL_SUBSCRIBE': + if (!this.WebGLRenderer) this.initWebGL(); + this.WebGLRenderer.subscribe(path, commands[++localIterator]); + break; + + case 'GL_UNSUBSCRIBE': + if (!this.WebGLRenderer) this.initWebGL(); + this.WebGLRenderer.unsubscribe(path, commands[++localIterator]); + break; + case 'GL_SET_DRAW_OPTIONS': if (!this.WebGLRenderer) this.initWebGL(); this.WebGLRenderer.setMeshOptions(path, commands[++localIterator]); diff --git a/webgl-renderables/Mesh.js b/webgl-renderables/Mesh.js index ee7b4b25..02049ba3 100644 --- a/webgl-renderables/Mesh.js +++ b/webgl-renderables/Mesh.js @@ -24,6 +24,7 @@ 'use strict'; var Geometry = require('../webgl-geometries'); +var CallbackStore = require('../utilities/CallbackStore'); /** * The Mesh class is responsible for providing the API for how @@ -42,9 +43,13 @@ var Geometry = require('../webgl-geometries'); function Mesh (node, options) { this._node = node; this._changeQueue = []; + this._UIEvents = []; + this._callbacks = new CallbackStore(); + this._initialized = false; this._requestingUpdate = false; this._inDraw = false; + this.value = { drawOptions: null, color: null, @@ -57,6 +62,7 @@ function Mesh (node, options) { }; if (options) this.setDrawOptions(options); + this._id = node.addComponent(this); } @@ -583,16 +589,113 @@ Mesh.prototype.onOpacityChange = function onOpacityChange (opacity) { }; /** - * Adds functionality for UI events (TODO) + * Method to be invoked by the node as soon as a new UIEvent is being added. + * This results into an `SUBSCRIBE` command being sent. + * + * @param {String} UIEvent UIEvent to be subscribed to (e.g. `click`) + * + * @return {Mesh} this + */ +Mesh.prototype.onAddUIEvent = function onAddUIEvent(UIEvent) { + if (this._UIEvents.indexOf(UIEvent) === -1) { + this._subscribe(UIEvent); + this._UIEvents.push(UIEvent); + } + else if (this._inDraw) { + this._subscribe(UIEvent); + } + return this; +}; + +/** + * Method to be invoked by the node as soon as a UIEvent is removed from + * the node. This results into an `UNSUBSCRIBE` command being sent. + * + * @param {String} UIEvent UIEvent to be removed (e.g. `click`) + * + * @return {Mesh} this + */ +Mesh.prototype.onRemoveUIEvent = function onRemoveUIEvent(UIEvent) { + var index = this._UIEvents.indexOf(UIEvent); + if (index !== -1) { + this._unsubscribe(UIEvent); + this._UIEvents.splice(index, 1); + } + else if (this._inDraw) { + this._unsubscribe(UIEvent); + } + return this; +}; + +/** + * Appends an `SUBSCRIBE` command to the command queue. * * @method + * @private * - * @param {String} UIEvent UI Event + * @param {String} UIEvent Event type (e.g. `click`) * * @return {undefined} undefined */ -Mesh.prototype.onAddUIEvent = function onAddUIEvent (UIEvent) { - //TODO +Mesh.prototype._subscribe = function _subscribe(UIEvent) { + if (this._initialized) { + this._changeQueue.push('GL_SUBSCRIBE', UIEvent); + } + + if (!this._requestingUpdate) this._requestUpdate(); +}; + +/** + * Appends an `UNSUBSCRIBE` command to the command queue. + * + * @method + * @private + * + * @param {String} UIEvent Event type (e.g. `click`) + * + * @return {undefined} undefined + */ +Mesh.prototype._unsubscribe = function _unsubscribe (UIEvent) { + if (this._initialized) { + this._changeQueue.push('GL_UNSUBSCRIBE', UIEvent); + } + + if (!this._requestingUpdate) this._requestUpdate(); +}; + +/** + * Function to be invoked by the Node whenever an event is being received. + * There are two different ways to subscribe for those events: + * + * 1. By overriding the onReceive method (and possibly using `switch` in order + * to differentiate between the different event types). + * 2. By using Mesh and using the built-in CallbackStore. + * + * @method + * + * @param {String} event Event type (e.g. `click`) + * @param {Object} payload Event object. + * + * @return {undefined} undefined + */ +Mesh.prototype.onReceive = function onReceive(event, payload) { + this._callbacks.trigger(event, payload); +}; + +/** + * Subscribes to a Mesh using + * + * @method on + * + * @param {String} event The event type (e.g. `click`). + * @param {Function} listener Handler function for the specified event type + * in which the payload event object will be + * passed into. + * + * @return {Function} A function to call if you want to remove the callback + */ +Mesh.prototype.on = function on(event, listener) { + return this._callbacks.on(event, listener); }; /** diff --git a/webgl-renderers/Program.js b/webgl-renderers/Program.js index 526f8e64..912478ea 100644 --- a/webgl-renderers/Program.js +++ b/webgl-renderers/Program.js @@ -80,7 +80,9 @@ var uniforms = keyValueToArrays({ u_lightColor: identityMatrix, u_ambientLight: [0, 0, 0], u_flatShading: 0, - u_numLights: 0 + u_numLights: 0, + u_pickingMode: 0, + u_meshIdColor: [-1, -1, -1, -1] }); /** diff --git a/webgl-renderers/WebGLRenderer.js b/webgl-renderers/WebGLRenderer.js index bda15f38..3be5c733 100644 --- a/webgl-renderers/WebGLRenderer.js +++ b/webgl-renderers/WebGLRenderer.js @@ -55,14 +55,17 @@ var globalUniforms = keyValueToArrays({ * * @param {Element} canvas The DOM element that GL will paint itself onto. * @param {Compositor} compositor Compositor used for querying the time from. + * @param {Element} eventDiv The main DOM element that is used for capturing events. * * @return {undefined} undefined */ -function WebGLRenderer(canvas, compositor) { +function WebGLRenderer(canvas, compositor, eventDiv) { canvas.classList.add('famous-webgl-renderer'); + var _this = this; this.canvas = canvas; this.compositor = compositor; + this.pixelRatio = window.devicePixelRatio || 1; var gl = this.gl = this.getWebGLContext(this.canvas); @@ -134,8 +137,137 @@ function WebGLRenderer(canvas, compositor) { this.bufferRegistry.allocate(cutout.spec.id, 'a_texCoord', cutout.spec.bufferValues[1], 2); this.bufferRegistry.allocate(cutout.spec.id, 'a_normals', cutout.spec.bufferValues[2], 3); this.bufferRegistry.allocate(cutout.spec.id, 'indices', cutout.spec.bufferValues[3], 1); + + /** + * WebGL Picking by caputing events (e.g. 'click') using the + * main famous HTML element for capturing events. + * + * TODO: Famous config file should contain these options for flexibility. + */ + this.listeners = {}; + this.meshIds = 0; + this.eventsMap = { + click: true, + dblclick: true, + mousewheel: false, + touchstart: true, + keyup: true, + keydown: true, + mousedown: false, + mouseup: false, + scroll: false, + select: false, + touchend: false, + wheel: false + }; + + // If event is tracked, set the listener + for(var ev in this.eventsMap) { + if (!this.eventsMap[ev]) continue; + this.listeners[ev] = []; + eventDiv.addEventListener(ev, function(e) { + _this.handleEvent(e); + }); + } } +/** + * Handle mousedown clicks on Canvas for determining the position of the cursor, in order to do WebGL picking. + * + * @method + * + * @param {Object} ev Event object + * + * @return {undefined} undefined + */ +WebGLRenderer.prototype.handleEvent = function handleEvent(ev) { + var x = ev.clientX; + var y = ev.clientY; + var canvasX; + var canvasY; + + var rect = ev.target.getBoundingClientRect(); + + if (rect.left <= x && x < rect.right && + rect.top <= y && y < rect.bottom) { + + canvasX = (x - rect.left) * this.pixelRatio; + canvasY = (rect.bottom - y) * this.pixelRatio; + this.checkEvent(canvasX, canvasY, ev.type); + } + + return this; +}; + +/** + * Determine the alpha channel, given an x and y coordinate for the WebGL canvas. + * + * @method + * + * @param {Number} x X coordinate + * @param {Number} y Y coordinate + * @param {String} type Event type + * + * @return {undefined} undefined + */ +WebGLRenderer.prototype.checkEvent = function checkEvent(x, y, type) { + var gl = this.gl; + + gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + this.program.setUniforms(['u_pickingMode'], [1.0]); + this.drawMeshes(); + + var pixels = new Uint8Array(4); + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + + this.program.setUniforms(['u_pickingMode'], [0.0]); + this.drawMeshes(); + + var meshId = this.decodeMeshIdColor(pixels, 255); + var picked = this.listeners[type][meshId]; + + if (picked) { + this.compositor.sendEvent(picked.path, type, {}); + } + + return picked; +}; + +/** + * Attaches an EventListener to the element associated with the passed in path. + * + * @method + * + * @param {String} path Path for the given Mesh. + * @param {String} type Event type (e.g. 'click'). + * + * @return {undefined} undefined + */ +WebGLRenderer.prototype.subscribe = function subscribe(path, type) { + if (this.eventsMap[type]) { + var mesh = this.meshRegistry[path]; + this.listeners[type][mesh.id] = mesh; + } +}; + +/** + * Removes an EventListener of given type from the element on which it was + * registered. + * + * @method + * + * @param {String} path Path for the given Mesh. + * @param {String} type Event type (e.g. 'click'). + * + * @return {undefined} undefined + */ +WebGLRenderer.prototype.unsubscribe = function unsubscribe(path, type) { + if (this.eventsMap[type]) { + var mesh = this.meshRegistry[path]; + this.listeners[type].splice(mesh.id, 1); + } +}; + /** * Attempts to retreive the WebGLRenderer context using several * accessors. For browser compatability. Throws on error. @@ -185,6 +317,55 @@ WebGLRenderer.prototype.createLight = function createLight(path) { return this.lightRegistry[path]; }; +/** + * Algorithm for encoding an ID to the normalized RGBA hash in base 255. + * + * @method + * + * @param {Number} meshId Mesh ID number + * @param {Number} base Base for conversion + * + * @returns {Array} Encoded value + */ +WebGLRenderer.prototype.encodeMeshIdColor = function encodeMeshIdColor(meshId, base) { + var result = []; + var normalizedRemainder; + + while (meshId) { + normalizedRemainder = (meshId%base) / 255; + result.unshift(normalizedRemainder); + meshId = (meshId / base) | 0; + } + + while (result.length !== 4) { + result.unshift(0); + } + + return result; +}; + +/** + * Algorithm for decoding the mesh ID from the WebGL shader. + * + * @method + * + * @param {Buffer} pixelsBuffer Pixel buffer from the WebGL shader + * @param {Number} base Base for conversion + * + * @returns {Number} Mesh ID + */ +WebGLRenderer.prototype.decodeMeshIdColor = function decodeMeshIdColor(pixelsBuffer, base) { + var result = 0; + var baseColumn = 0; + + var len = pixelsBuffer.length - 1; + for(var i = len; i >= 0; i--) { + result += pixelsBuffer[i] * Math.pow(base, baseColumn++); + } + + return result; +}; + /** * Adds a new base spec to the mesh registry at a given path. * @@ -205,7 +386,8 @@ WebGLRenderer.prototype.createMesh = function createMesh(path) { u_positionOffset: [0, 0, 0], u_normals: [0, 0, 0], u_flatShading: 0, - u_glossiness: [0, 0, 0, 0] + u_glossiness: [0, 0, 0, 0], + u_meshIdColor: this.encodeMeshIdColor(++this.meshIds, 255) }); this.meshRegistry[path] = { depth: null, @@ -215,7 +397,9 @@ WebGLRenderer.prototype.createMesh = function createMesh(path) { geometry: null, drawType: null, textures: [], - visible: true + visible: true, + path: path, + id: this.meshIds }; return this.meshRegistry[path]; }; @@ -545,7 +729,8 @@ WebGLRenderer.prototype.drawMeshes = function drawMeshes() { var buffers; var mesh; - for(var i = 0; i < this.meshRegistryKeys.length; i++) { + var len = this.meshRegistryKeys.length; + for(var i = 0; i < len; i++) { mesh = this.meshRegistry[this.meshRegistryKeys[i]]; buffers = this.bufferRegistry.registry[mesh.geometry]; @@ -779,9 +964,9 @@ WebGLRenderer.prototype.drawBuffers = function drawBuffers(vertexBuffers, mode, */ WebGLRenderer.prototype.updateSize = function updateSize(size) { if (size) { - var pixelRatio = window.devicePixelRatio || 1; - var displayWidth = ~~(size[0] * pixelRatio); - var displayHeight = ~~(size[1] * pixelRatio); + this.pixelRatio = window.devicePixelRatio || 1; + var displayWidth = (size[0] * this.pixelRatio) | 0; + var displayHeight = (size[1] * this.pixelRatio) | 0; this.canvas.width = displayWidth; this.canvas.height = displayHeight; this.gl.viewport(0, 0, displayWidth, displayHeight); diff --git a/webgl-shaders/FragmentShader.glsl b/webgl-shaders/FragmentShader.glsl index a0eed7ce..9e350923 100644 --- a/webgl-shaders/FragmentShader.glsl +++ b/webgl-shaders/FragmentShader.glsl @@ -1,18 +1,18 @@ /** * The MIT License (MIT) - * + * * Copyright (c) 2015 Famous Industries Inc. - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -35,19 +35,29 @@ * */ void main() { - vec4 material = u_baseColor.r >= 0.0 ? u_baseColor : applyMaterial(u_baseColor); /** - * Apply lights only if flat shading is false - * and at least one light is added to the scene + * Writes the encoded Mesh IDs into the color channels, + * when the WebGL canvas has been clicked. */ - bool lightsEnabled = (u_flatShading == 0.0) && (u_numLights > 0.0 || length(u_ambientLight) > 0.0); + if (u_pickingMode > 0.0) { + gl_FragColor = u_meshIdColor; + } + else { + vec4 material = u_baseColor.r >= 0.0 ? u_baseColor : applyMaterial(u_baseColor); + + /** + * Apply lights only if flat shading is false + * and at least one light is added to the scene + */ + bool lightsEnabled = (u_flatShading == 0.0) && (u_numLights > 0.0 || length(u_ambientLight) > 0.0); - vec3 normal = normalize(v_normal); - vec4 glossiness = u_glossiness.x < 0.0 ? applyMaterial(u_glossiness) : u_glossiness; + vec3 normal = normalize(v_normal); + vec4 glossiness = u_glossiness.x < 0.0 ? applyMaterial(u_glossiness) : u_glossiness; - vec4 color = lightsEnabled ? applyLight(material, normalize(v_normal), glossiness) : material; + vec4 color = lightsEnabled ? applyLight(material, normalize(v_normal), glossiness) : material; - gl_FragColor = color; - gl_FragColor.a *= u_opacity; + gl_FragColor = color; + gl_FragColor.a *= u_opacity; + } }