/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cu, Ci, ChromeWorker} = require("chrome"); let TiltGL = require("devtools/tilt/tilt-gl"); let TiltUtils = require("devtools/tilt/tilt-utils"); let TiltVisualizerStyle = require("devtools/tilt/tilt-visualizer-style"); let {EPSILON, TiltMath, vec3, mat4, quat4} = require("devtools/tilt/tilt-math"); let {TargetFactory} = require("devtools/framework/target"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/devtools/gDevTools.jsm"); const ELEMENT_MIN_SIZE = 4; const INVISIBLE_ELEMENTS = { "head": true, "base": true, "basefont": true, "isindex": true, "link": true, "meta": true, "option": true, "script": true, "style": true, "title": true }; // a node is represented in the visualization mesh as a rectangular stack // of 5 quads composed of 12 vertices; we draw these as triangles using an // index buffer of 12 unsigned int elements, obviously one for each vertex; // if a webpage has enough nodes to overflow the index buffer elements size, // weird things may happen; thus, when necessary, we'll split into groups const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1; const WIREFRAME_COLOR = [0, 0, 0, 0.25]; const INTRO_TRANSITION_DURATION = 1000; const OUTRO_TRANSITION_DURATION = 800; const INITIAL_Z_TRANSLATION = 400; const MOVE_INTO_VIEW_ACCURACY = 50; const MOUSE_CLICK_THRESHOLD = 10; const MOUSE_INTRO_DELAY = 200; const ARCBALL_SENSITIVITY = 0.5; const ARCBALL_ROTATION_STEP = 0.15; const ARCBALL_TRANSLATION_STEP = 35; const ARCBALL_ZOOM_STEP = 0.1; const ARCBALL_ZOOM_MIN = -3000; const ARCBALL_ZOOM_MAX = 500; const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1; const ARCBALL_RESET_LINEAR_FACTOR = 0.01; const TILT_CRAFTER = "resource:///modules/devtools/tilt/TiltWorkerCrafter.js"; const TILT_PICKER = "resource:///modules/devtools/tilt/TiltWorkerPicker.js"; /** * Initializes the visualization presenter and controller. * * @param {Object} aProperties * an object containing the following properties: * {Window} chromeWindow: a reference to the top level window * {Window} contentWindow: the content window holding the visualized doc * {Element} parentNode: the parent node to hold the visualization * {Object} notifications: necessary notifications for Tilt * {Function} onError: optional, function called if initialization failed * {Function} onLoad: optional, function called if initialization worked */ function TiltVisualizer(aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; /** * Save a reference to the top-level window. */ this.chromeWindow = aProperties.chromeWindow; this.tab = aProperties.tab; /** * The canvas element used for rendering the visualization. */ this.canvas = TiltUtils.DOM.initCanvas(aProperties.parentNode, { focusable: true, append: true }); /** * Visualization logic and drawing loop. */ this.presenter = new TiltVisualizer.Presenter(this.canvas, aProperties.chromeWindow, aProperties.contentWindow, aProperties.notifications, aProperties.onError || null, aProperties.onLoad || null); /** * Visualization mouse and keyboard controller. */ this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter); } exports.TiltVisualizer = TiltVisualizer; TiltVisualizer.prototype = { /** * Initializes the visualizer. */ init: function TV_init() { this.presenter.init(); this.bindToInspector(this.tab); }, /** * Checks if this object was initialized properly. * * @return {Boolean} true if the object was initialized properly */ isInitialized: function TV_isInitialized() { return this.presenter && this.presenter.isInitialized() && this.controller && this.controller.isInitialized(); }, /** * Removes the overlay canvas used for rendering the visualization. */ removeOverlay: function TV_removeOverlay() { if (this.canvas && this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); } }, /** * Explicitly cleans up this visualizer and sets everything to null. */ cleanup: function TV_cleanup() { this.unbindInspector(); if (this.controller) { TiltUtils.destroyObject(this.controller); } if (this.presenter) { TiltUtils.destroyObject(this.presenter); } let chromeWindow = this.chromeWindow; TiltUtils.destroyObject(this); TiltUtils.clearCache(); TiltUtils.gc(chromeWindow); }, /** * Listen to the inspector activity. */ bindToInspector: function TV_bindToInspector(aTab) { this._browserTab = aTab; this.onNewNodeFromInspector = this.onNewNodeFromInspector.bind(this); this.onNewNodeFromTilt = this.onNewNodeFromTilt.bind(this); this.onInspectorReady = this.onInspectorReady.bind(this); this.onToolboxDestroyed = this.onToolboxDestroyed.bind(this); gDevTools.on("inspector-ready", this.onInspectorReady); gDevTools.on("toolbox-destroyed", this.onToolboxDestroyed); Services.obs.addObserver(this.onNewNodeFromTilt, this.presenter.NOTIFICATIONS.HIGHLIGHTING, false); Services.obs.addObserver(this.onNewNodeFromTilt, this.presenter.NOTIFICATIONS.UNHIGHLIGHTING, false); let target = TargetFactory.forTab(aTab); let toolbox = gDevTools.getToolbox(target); if (toolbox) { let panel = toolbox.getPanel("inspector"); if (panel) { this.inspector = panel; this.inspector.selection.on("new-node", this.onNewNodeFromInspector); this.onNewNodeFromInspector(); } } }, /** * Unregister inspector event listeners. */ unbindInspector: function TV_unbindInspector() { this._browserTab = null; if (this.inspector) { if (this.inspector.selection) { this.inspector.selection.off("new-node", this.onNewNodeFromInspector); } this.inspector = null; } gDevTools.off("inspector-ready", this.onInspectorReady); gDevTools.off("toolbox-destroyed", this.onToolboxDestroyed); Services.obs.removeObserver(this.onNewNodeFromTilt, this.presenter.NOTIFICATIONS.HIGHLIGHTING); Services.obs.removeObserver(this.onNewNodeFromTilt, this.presenter.NOTIFICATIONS.UNHIGHLIGHTING); }, /** * When a new inspector is started. */ onInspectorReady: function TV_onInspectorReady(event, toolbox, panel) { if (toolbox.target.tab === this._browserTab) { this.inspector = panel; this.inspector.selection.on("new-node", this.onNewNodeFromInspector); this.onNewNodeFromTilt(); } }, /** * When the toolbox, therefor the inspector, is closed. */ onToolboxDestroyed: function TV_onToolboxDestroyed(event, tab) { if (tab === this._browserTab && this.inspector) { if (this.inspector.selection) { this.inspector.selection.off("new-node", this.onNewNodeFromInspector); } this.inspector = null; } }, /** * When a new node is selected in the inspector. */ onNewNodeFromInspector: function TV_onNewNodeFromInspector() { if (this.inspector && this.inspector.selection.reason != "tilt") { let selection = this.inspector.selection; let canHighlightNode = selection.isNode() && selection.isConnected() && selection.isElementNode(); if (canHighlightNode) { this.presenter.highlightNode(selection.node); } else { this.presenter.highlightNodeFor(-1); } } }, /** * When a new node is selected in Tilt. */ onNewNodeFromTilt: function TV_onNewNodeFromTilt() { if (!this.inspector) { return; } let nodeIndex = this.presenter._currentSelection; if (nodeIndex < 0) { this.inspector.selection.setNodeFront(null, "tilt"); } let node = this.presenter._traverseData.nodes[nodeIndex]; node = this.inspector.walker.frontForRawNode(node); this.inspector.selection.setNodeFront(node, "tilt"); }, }; /** * This object manages the visualization logic and drawing loop. * * @param {HTMLCanvasElement} aCanvas * the canvas element used for rendering * @param {Window} aChromeWindow * a reference to the top-level window * @param {Window} aContentWindow * the content window holding the document to be visualized * @param {Object} aNotifications * necessary notifications for Tilt * @param {Function} onError * function called if initialization failed * @param {Function} onLoad * function called if initialization worked */ TiltVisualizer.Presenter = function TV_Presenter( aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad) { /** * A canvas overlay used for drawing the visualization. */ this.canvas = aCanvas; /** * Save a reference to the top-level window, to access Tilt. */ this.chromeWindow = aChromeWindow; /** * The content window generating the visualization */ this.contentWindow = aContentWindow; /** * Shortcut for accessing notifications strings. */ this.NOTIFICATIONS = aNotifications; /** * Use the default node callback function */ this.nodeCallback = null; /** * Create the renderer, containing useful functions for easy drawing. */ this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad); /** * A custom shader used for drawing the visualization mesh. */ this._visualizationProgram = null; /** * The combined mesh representing the document visualization. */ this._texture = null; this._meshData = null; this._meshStacks = null; this._meshWireframe = null; this._traverseData = null; /** * A highlight quad drawn over a stacked dom node. */ this._highlight = { disabled: true, v0: vec3.create(), v1: vec3.create(), v2: vec3.create(), v3: vec3.create() }; /** * Scene transformations, exposing offset, translation and rotation. * Modified by events in the controller through delegate functions. */ this.transforms = { zoom: 1, offset: vec3.create(), // mesh offset, aligned to the viewport center translation: vec3.create(), // scene translation, on the [x, y, z] axis rotation: quat4.create() // scene rotation, expressed as a quaternion }; /** * Variables holding information about the initial and current node selected. */ this._currentSelection = -1; // the selected node index this._initialMeshConfiguration = false; // true if the 3D mesh was configured /** * Variable specifying if the scene should be redrawn. * This should happen usually when the visualization is translated/rotated. */ this._redraw = true; /** * Total time passed since the rendering started. * If the rendering is paused, this property won't get updated. */ this._time = 0; /** * Frame delta time (the ammount of time passed for each frame). * This is used to smoothly interpolate animation transfroms. */ this._delta = 0; this._prevFrameTime = 0; this._currFrameTime = 0; }; TiltVisualizer.Presenter.prototype = { /** * Initializes the presenter and starts the animation loop */ init: function TVP_init() { this._setup(); this._loop(); }, /** * The initialization logic. */ _setup: function TVP__setup() { let renderer = this._renderer; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // create the visualization shaders and program to draw the stacks mesh this._visualizationProgram = new renderer.Program({ vs: TiltVisualizer.MeshShader.vs, fs: TiltVisualizer.MeshShader.fs, attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"], uniforms: ["mvMatrix", "projMatrix", "sampler"] }); // get the document zoom to properly scale the visualization this.transforms.zoom = this._getPageZoom(); // bind the owner object to the necessary functions TiltUtils.bindObjectFunc(this, "^_on"); TiltUtils.bindObjectFunc(this, "_loop"); this._setupTexture(); this._setupMeshData(); this._setupEventListeners(); this.canvas.focus(); }, /** * Get page zoom factor. * @return {Number} */ _getPageZoom: function TVP__getPageZoom() { return this.contentWindow .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .fullZoom; }, /** * The animation logic. */ _loop: function TVP__loop() { let renderer = this._renderer; // if the renderer was destroyed, don't continue rendering if (!renderer || !renderer.context) { return; } // prepare for the next frame of the animation loop this.chromeWindow.mozRequestAnimationFrame(this._loop); // only redraw if we really have to if (this._redraw) { this._redraw = false; this._drawVisualization(); } // update the current presenter transfroms from the controller if ("function" === typeof this._controllerUpdate) { this._controllerUpdate(this._time, this._delta); } this._handleFrameDelta(); this._handleKeyframeNotifications(); }, /** * Calculates the current frame delta time. */ _handleFrameDelta: function TVP__handleFrameDelta() { this._prevFrameTime = this._currFrameTime; this._currFrameTime = this.chromeWindow.mozAnimationStartTime; this._delta = this._currFrameTime - this._prevFrameTime; }, /** * Draws the visualization mesh and highlight quad. */ _drawVisualization: function TVP__drawVisualization() { let renderer = this._renderer; let transforms = this.transforms; let w = renderer.width; let h = renderer.height; let ih = renderer.initialHeight; // if the mesh wasn't created yet, don't continue rendering if (!this._meshStacks || !this._meshWireframe) { return; } // clear the context to an opaque black background renderer.clear(); renderer.perspective(); // apply a transition transformation using an ortho and perspective matrix let ortho = mat4.ortho(0, w, h, 0, -1000, 1000); if (!this._isExecutingDestruction) { let f = this._time / INTRO_TRANSITION_DURATION; renderer.lerp(renderer.projMatrix, ortho, f, 8); } else { let f = this._time / OUTRO_TRANSITION_DURATION; renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8); } // apply the preliminary transformations to the model view renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION); // calculate the camera matrix using the rotation and translation renderer.translate(transforms.translation[0], 0, transforms.translation[2]); renderer.transform(quat4.toMat4(transforms.rotation)); // offset the visualization mesh to center renderer.translate(transforms.offset[0], transforms.offset[1] + transforms.translation[1], 0); renderer.scale(transforms.zoom, transforms.zoom); // draw the visualization mesh renderer.strokeWeight(2); renderer.depthTest(true); this._drawMeshStacks(); this._drawMeshWireframe(); this._drawHighlight(); // make sure the initial transition is drawn until finished if (this._time < INTRO_TRANSITION_DURATION || this._time < OUTRO_TRANSITION_DURATION) { this._redraw = true; } this._time += this._delta; }, /** * Draws the meshStacks object. */ _drawMeshStacks: function TVP__drawMeshStacks() { let renderer = this._renderer; let mesh = this._meshStacks; let visualizationProgram = this._visualizationProgram; let texture = this._texture; let mvMatrix = renderer.mvMatrix; let projMatrix = renderer.projMatrix; // use the necessary shader visualizationProgram.use(); for (let i = 0, len = mesh.length; i < len; i++) { let group = mesh[i]; // bind the attributes and uniforms as necessary visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices); visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord); visualizationProgram.bindVertexBuffer("vertexColor", group.color); visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix); visualizationProgram.bindUniformMatrix("projMatrix", projMatrix); visualizationProgram.bindTexture("sampler", texture); // draw the vertices as TRIANGLES indexed elements renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices); } // save the current model view and projection matrices mesh.mvMatrix = mat4.create(mvMatrix); mesh.projMatrix = mat4.create(projMatrix); }, /** * Draws the meshWireframe object. */ _drawMeshWireframe: function TVP__drawMeshWireframe() { let renderer = this._renderer; let mesh = this._meshWireframe; for (let i = 0, len = mesh.length; i < len; i++) { let group = mesh[i]; // use the necessary shader renderer.useColorShader(group.vertices, WIREFRAME_COLOR); // draw the vertices as LINES indexed elements renderer.drawIndexedVertices(renderer.context.LINES, group.indices); } }, /** * Draws a highlighted quad around a currently selected node. */ _drawHighlight: function TVP__drawHighlight() { // check if there's anything to highlight (i.e any node is selected) if (!this._highlight.disabled) { // set the corresponding state to draw the highlight quad let renderer = this._renderer; let highlight = this._highlight; renderer.depthTest(false); renderer.fill(highlight.fill, 0.5); renderer.stroke(highlight.stroke); renderer.strokeWeight(highlight.strokeWeight); renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3); } }, /** * Creates or refreshes the texture applied to the visualization mesh. */ _setupTexture: function TVP__setupTexture() { let renderer = this._renderer; // destroy any previously created texture TiltUtils.destroyObject(this._texture); this._texture = null; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // get the maximum texture size this._maxTextureSize = renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE); // use a simple shim to get the image representation of the document // this will be removed once the MOZ_window_region_texture bug #653656 // is finished; currently just converting the document image to a texture // applied to the mesh this._texture = new renderer.Texture({ source: TiltGL.TextureUtils.createContentImage(this.contentWindow, this._maxTextureSize), format: "RGB" }); if ("function" === typeof this._onSetupTexture) { this._onSetupTexture(); this._onSetupTexture = null; } }, /** * Create the combined mesh representing the document visualization by * traversing the document & adding a stack for each node that is drawable. * * @param {Object} aMeshData * object containing the necessary mesh verts, texcoord etc. */ _setupMesh: function TVP__setupMesh(aMeshData) { let renderer = this._renderer; // destroy any previously created mesh TiltUtils.destroyObject(this._meshStacks); this._meshStacks = []; TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = []; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // save the mesh data for future use this._meshData = aMeshData; // create a sub-mesh for each group in the mesh data for (let i = 0, len = aMeshData.groups.length; i < len; i++) { let group = aMeshData.groups[i]; // create the visualization mesh using the vertices, texture coordinates // and indices computed when traversing the document object model this._meshStacks.push({ vertices: new renderer.VertexBuffer(group.vertices, 3), texCoord: new renderer.VertexBuffer(group.texCoord, 2), color: new renderer.VertexBuffer(group.color, 3), indices: new renderer.IndexBuffer(group.stacksIndices) }); // additionally, create a wireframe representation to make the // visualization a bit more pretty this._meshWireframe.push({ vertices: this._meshStacks[i].vertices, indices: new renderer.IndexBuffer(group.wireframeIndices) }); } // configure the required mesh transformations and background only once if (!this._initialMeshConfiguration) { this._initialMeshConfiguration = true; // set the necessary mesh offsets this.transforms.offset[0] = -renderer.width * 0.5; this.transforms.offset[1] = -renderer.height * 0.5; // make sure the canvas is opaque now that the initialization is finished this.canvas.style.background = TiltVisualizerStyle.canvas.background; this._drawVisualization(); this._redraw = true; } if ("function" === typeof this._onSetupMesh) { this._onSetupMesh(); this._onSetupMesh = null; } }, /** * Computes the mesh vertices, texture coordinates etc. by groups of nodes. */ _setupMeshData: function TVP__setupMeshData() { let renderer = this._renderer; // if the renderer was destroyed, don't continue setup if (!renderer || !renderer.context) { return; } // traverse the document and get the depths, coordinates and local names this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, { nodeCallback: this.nodeCallback, invisibleElements: INVISIBLE_ELEMENTS, minSize: ELEMENT_MIN_SIZE, maxX: this._texture.width, maxY: this._texture.height }); let worker = new ChromeWorker(TILT_CRAFTER); worker.addEventListener("message", function TVP_onMessage(event) { this._setupMesh(event.data); }.bind(this), false); // calculate necessary information regarding vertices, texture coordinates // etc. in a separate thread, as this process may take a while worker.postMessage({ maxGroupNodes: MAX_GROUP_NODES, style: TiltVisualizerStyle.nodes, texWidth: this._texture.width, texHeight: this._texture.height, nodesInfo: this._traverseData.info }); }, /** * Sets up event listeners necessary for the presenter. */ _setupEventListeners: function TVP__setupEventListeners() { this.contentWindow.addEventListener("resize", this._onResize, false); }, /** * Called when the content window of the current browser is resized. */ _onResize: function TVP_onResize(e) { let zoom = this._getPageZoom(); let width = e.target.innerWidth * zoom; let height = e.target.innerHeight * zoom; // handle aspect ratio changes to update the projection matrix this._renderer.width = width; this._renderer.height = height; this._redraw = true; }, /** * Highlights a specific node. * * @param {Element} aNode * the html node to be highlighted * @param {String} aFlags * flags specifying highlighting options */ highlightNode: function TVP_highlightNode(aNode, aFlags) { this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags); }, /** * Picks a stacked dom node at the x and y screen coordinates and highlights * the selected node in the mesh. * * @param {Number} x * the current horizontal coordinate of the mouse * @param {Number} y * the current vertical coordinate of the mouse * @param {Object} aProperties * an object containing the following properties: * {Function} onpick: function to be called after picking succeeded * {Function} onfail: function to be called after picking failed */ highlightNodeAt: function TVP_highlightNodeAt(x, y, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; // try to pick a mesh node using the current x, y coordinates this.pickNode(x, y, { /** * Mesh picking failed (nothing was found for the picked point). */ onfail: function TVP_onHighlightFail() { this.highlightNodeFor(-1); if ("function" === typeof aProperties.onfail) { aProperties.onfail(); } }.bind(this), /** * Mesh picking succeeded. * * @param {Object} aIntersection * object containing the intersection details */ onpick: function TVP_onHighlightPick(aIntersection) { this.highlightNodeFor(aIntersection.index); if ("function" === typeof aProperties.onpick) { aProperties.onpick(); } }.bind(this) }); }, /** * Sets the corresponding highlight coordinates and color based on the * information supplied. * * @param {Number} aNodeIndex * the index of the node in the this._traverseData array * @param {String} aFlags * flags specifying highlighting options */ highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags) { this._redraw = true; // if the node was already selected, don't do anything if (this._currentSelection === aNodeIndex) { return; } // if an invalid or nonexisted node is specified, disable the highlight if (aNodeIndex < 0) { this._currentSelection = -1; this._highlight.disabled = true; Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.UNHIGHLIGHTING, null); return; } let highlight = this._highlight; let info = this._traverseData.info[aNodeIndex]; let style = TiltVisualizerStyle.nodes; highlight.disabled = false; highlight.fill = style[info.name] || style.highlight.defaultFill; highlight.stroke = style.highlight.defaultStroke; highlight.strokeWeight = style.highlight.defaultStrokeWeight; let x = info.coord.left; let y = info.coord.top; let w = info.coord.width; let h = info.coord.height; let z = info.coord.depth + info.coord.thickness; vec3.set([x, y, z], highlight.v0); vec3.set([x + w, y, z], highlight.v1); vec3.set([x + w, y + h, z], highlight.v2); vec3.set([x, y + h, z], highlight.v3); this._currentSelection = aNodeIndex; // if something is highlighted, make sure it's inside the current viewport; // the point which should be moved into view is considered the center [x, y] // position along the top edge of the currently selected node if (aFlags && aFlags.indexOf("moveIntoView") !== -1) { this.controller.arcball.moveIntoView(vec3.lerp( vec3.scale(this._highlight.v0, this.transforms.zoom, []), vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5)); } Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.HIGHLIGHTING, null); }, /** * Deletes a node from the visualization mesh. * * @param {Number} aNodeIndex * the index of the node in the this._traverseData array; * if not specified, it will default to the current selection */ deleteNode: function TVP_deleteNode(aNodeIndex) { // we probably don't want to delete the html or body node.. just sayin' if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) { return; } let renderer = this._renderer; let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES); let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES); let group = this._meshStacks[groupIndex]; let vertices = group.vertices.components; for (let i = 0, k = 36 * nodeIndex; i < 36; i++) { vertices[i + k] = 0; } group.vertices = new renderer.VertexBuffer(vertices, 3); this._highlight.disabled = true; this._redraw = true; Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.NODE_REMOVED, null); }, /** * Picks a stacked dom node at the x and y screen coordinates and issues * a callback function with the found intersection. * * @param {Number} x * the current horizontal coordinate of the mouse * @param {Number} y * the current vertical coordinate of the mouse * @param {Object} aProperties * an object containing the following properties: * {Function} onpick: function to be called at intersection * {Function} onfail: function to be called if no intersections */ pickNode: function TVP_pickNode(x, y, aProperties) { // make sure the properties parameter is a valid object aProperties = aProperties || {}; // if the mesh wasn't created yet, don't continue picking if (!this._meshStacks || !this._meshWireframe) { return; } let worker = new ChromeWorker(TILT_PICKER); worker.addEventListener("message", function TVP_onMessage(event) { if (event.data) { if ("function" === typeof aProperties.onpick) { aProperties.onpick(event.data); } } else { if ("function" === typeof aProperties.onfail) { aProperties.onfail(); } } }, false); let zoom = this._getPageZoom(); let width = this._renderer.width * zoom; let height = this._renderer.height * zoom; x *= zoom; y *= zoom; // create a ray following the mouse direction from the near clipping plane // to the far clipping plane, to check for intersections with the mesh, // and do all the heavy lifting in a separate thread worker.postMessage({ vertices: this._meshData.allVertices, // create the ray destined for 3D picking ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height], this._meshStacks.mvMatrix, this._meshStacks.projMatrix) }); }, /** * Delegate translation method, used by the controller. * * @param {Array} aTranslation * the new translation on the [x, y, z] axis */ setTranslation: function TVP_setTranslation(aTranslation) { let x = aTranslation[0]; let y = aTranslation[1]; let z = aTranslation[2]; let transforms = this.transforms; // only update the translation if it's not already set if (transforms.translation[0] !== x || transforms.translation[1] !== y || transforms.translation[2] !== z) { vec3.set(aTranslation, transforms.translation); this._redraw = true; } }, /** * Delegate rotation method, used by the controller. * * @param {Array} aQuaternion * the rotation quaternion, as [x, y, z, w] */ setRotation: function TVP_setRotation(aQuaternion) { let x = aQuaternion[0]; let y = aQuaternion[1]; let z = aQuaternion[2]; let w = aQuaternion[3]; let transforms = this.transforms; // only update the rotation if it's not already set if (transforms.rotation[0] !== x || transforms.rotation[1] !== y || transforms.rotation[2] !== z || transforms.rotation[3] !== w) { quat4.set(aQuaternion, transforms.rotation); this._redraw = true; } }, /** * Handles notifications at specific frame counts. */ _handleKeyframeNotifications: function TV__handleKeyframeNotifications() { if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) { this._time = INTRO_TRANSITION_DURATION; } if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) { this._time = OUTRO_TRANSITION_DURATION; } if (this._time >= INTRO_TRANSITION_DURATION && !this._isInitializationFinished && !this._isExecutingDestruction) { this._isInitializationFinished = true; Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.INITIALIZED, null); if ("function" === typeof this._onInitializationFinished) { this._onInitializationFinished(); } } if (this._time >= OUTRO_TRANSITION_DURATION && !this._isDestructionFinished && this._isExecutingDestruction) { this._isDestructionFinished = true; Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.BEFORE_DESTROYED, null); if ("function" === typeof this._onDestructionFinished) { this._onDestructionFinished(); } } }, /** * Starts executing the destruction sequence and issues a callback function * when finished. * * @param {Function} aCallback * the destruction finished callback */ executeDestruction: function TV_executeDestruction(aCallback) { if (!this._isExecutingDestruction) { this._isExecutingDestruction = true; this._onDestructionFinished = aCallback; // if we execute the destruction after the initialization finishes, // proceed normally; otherwise, skip everything and immediately issue // the callback if (this._time > OUTRO_TRANSITION_DURATION) { this._time = 0; this._redraw = true; } else { aCallback(); } } }, /** * Checks if this object was initialized properly. * * @return {Boolean} true if the object was initialized properly */ isInitialized: function TVP_isInitialized() { return this._renderer && this._renderer.context; }, /** * Function called when this object is destroyed. */ _finalize: function TVP__finalize() { TiltUtils.destroyObject(this._visualizationProgram); TiltUtils.destroyObject(this._texture); if (this._meshStacks) { this._meshStacks.forEach(function(group) { TiltUtils.destroyObject(group.vertices); TiltUtils.destroyObject(group.texCoord); TiltUtils.destroyObject(group.color); TiltUtils.destroyObject(group.indices); }); } if (this._meshWireframe) { this._meshWireframe.forEach(function(group) { TiltUtils.destroyObject(group.indices); }); } TiltUtils.destroyObject(this._renderer); // Closing the tab would result in contentWindow being a dead object, // so operations like removing event listeners won't work anymore. if (this.contentWindow == this.chromeWindow.content) { this.contentWindow.removeEventListener("resize", this._onResize, false); } } }; /** * A mouse and keyboard controller implementation. * * @param {HTMLCanvasElement} aCanvas * the visualization canvas element * @param {TiltVisualizer.Presenter} aPresenter * the presenter instance to control */ TiltVisualizer.Controller = function TV_Controller(aCanvas, aPresenter) { /** * A canvas overlay on which mouse and keyboard event listeners are attached. */ this.canvas = aCanvas; /** * Save a reference to the presenter to modify its model-view transforms. */ this.presenter = aPresenter; this.presenter.controller = this; /** * The initial controller dimensions and offset, in pixels. */ this._zoom = aPresenter.transforms.zoom; this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom; this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom; this._width = aCanvas.width; this._height = aCanvas.height; /** * Arcball used to control the visualization using the mouse. */ this.arcball = new TiltVisualizer.Arcball( this.presenter.chromeWindow, this._width, this._height, 0, [ this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0, this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0 ]); /** * Object containing the rotation quaternion and the translation amount. */ this._coordinates = null; // bind the owner object to the necessary functions TiltUtils.bindObjectFunc(this, "_update"); TiltUtils.bindObjectFunc(this, "^_on"); // add the necessary event listeners this.addEventListeners(); // attach this controller's update function to the presenter ondraw event this.presenter._controllerUpdate = this._update; }; TiltVisualizer.Controller.prototype = { /** * Adds events listeners required by this controller. */ addEventListeners: function TVC_addEventListeners() { let canvas = this.canvas; let presenter = this.presenter; // bind commonly used mouse and keyboard events with the controller canvas.addEventListener("mousedown", this._onMouseDown, false); canvas.addEventListener("mouseup", this._onMouseUp, false); canvas.addEventListener("mousemove", this._onMouseMove, false); canvas.addEventListener("mouseover", this._onMouseOver, false); canvas.addEventListener("mouseout", this._onMouseOut, false); canvas.addEventListener("MozMousePixelScroll", this._onMozScroll, false); canvas.addEventListener("keydown", this._onKeyDown, false); canvas.addEventListener("keyup", this._onKeyUp, false); canvas.addEventListener("blur", this._onBlur, false); // handle resize events to change the arcball dimensions presenter.contentWindow.addEventListener("resize", this._onResize, false); }, /** * Removes all added events listeners required by this controller. */ removeEventListeners: function TVC_removeEventListeners() { let canvas = this.canvas; let presenter = this.presenter; canvas.removeEventListener("mousedown", this._onMouseDown, false); canvas.removeEventListener("mouseup", this._onMouseUp, false); canvas.removeEventListener("mousemove", this._onMouseMove, false); canvas.removeEventListener("mouseover", this._onMouseOver, false); canvas.removeEventListener("mouseout", this._onMouseOut, false); canvas.removeEventListener("MozMousePixelScroll", this._onMozScroll, false); canvas.removeEventListener("keydown", this._onKeyDown, false); canvas.removeEventListener("keyup", this._onKeyUp, false); canvas.removeEventListener("blur", this._onBlur, false); // Closing the tab would result in contentWindow being a dead object, // so operations like removing event listeners won't work anymore. if (presenter.contentWindow == presenter.chromeWindow.content) { presenter.contentWindow.removeEventListener("resize", this._onResize, false); } }, /** * Function called each frame, updating the visualization camera transforms. * * @param {Number} aTime * total time passed since rendering started * @param {Number} aDelta * the current animation frame delta */ _update: function TVC__update(aTime, aDelta) { this._time = aTime; this._coordinates = this.arcball.update(aDelta); this.presenter.setRotation(this._coordinates.rotation); this.presenter.setTranslation(this._coordinates.translation); }, /** * Called once after every time a mouse button is pressed. */ _onMouseDown: function TVC__onMouseDown(e) { e.target.focus(); e.preventDefault(); e.stopPropagation(); if (this._time < MOUSE_INTRO_DELAY) { return; } // calculate x and y coordinates using using the client and target offset let button = e.which; this._downX = e.clientX - e.target.offsetLeft; this._downY = e.clientY - e.target.offsetTop; this.arcball.mouseDown(this._downX, this._downY, button); }, /** * Called every time a mouse button is released. */ _onMouseUp: function TVC__onMouseUp(e) { e.preventDefault(); e.stopPropagation(); if (this._time < MOUSE_INTRO_DELAY) { return; } // calculate x and y coordinates using using the client and target offset let button = e.which; let upX = e.clientX - e.target.offsetLeft; let upY = e.clientY - e.target.offsetTop; // a click in Tilt is issued only when the mouse pointer stays in // relatively the same position if (Math.abs(this._downX - upX) < MOUSE_CLICK_THRESHOLD && Math.abs(this._downY - upY) < MOUSE_CLICK_THRESHOLD) { this.presenter.highlightNodeAt(upX, upY); } this.arcball.mouseUp(upX, upY, button); }, /** * Called every time the mouse moves. */ _onMouseMove: function TVC__onMouseMove(e) { e.preventDefault(); e.stopPropagation(); if (this._time < MOUSE_INTRO_DELAY) { return; } // calculate x and y coordinates using using the client and target offset let moveX = e.clientX - e.target.offsetLeft; let moveY = e.clientY - e.target.offsetTop; this.arcball.mouseMove(moveX, moveY); }, /** * Called when the mouse leaves the visualization bounds. */ _onMouseOver: function TVC__onMouseOver(e) { e.preventDefault(); e.stopPropagation(); this.arcball.mouseOver(); }, /** * Called when the mouse leaves the visualization bounds. */ _onMouseOut: function TVC__onMouseOut(e) { e.preventDefault(); e.stopPropagation(); this.arcball.mouseOut(); }, /** * Called when the mouse wheel is used. */ _onMozScroll: function TVC__onMozScroll(e) { e.preventDefault(); e.stopPropagation(); this.arcball.zoom(e.detail); }, /** * Called when a key is pressed. */ _onKeyDown: function TVC__onKeyDown(e) { let code = e.keyCode || e.which; if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); this.arcball.keyDown(code); } else { this.arcball.cancelKeyEvents(); } if (e.keyCode === e.DOM_VK_ESCAPE) { let {TiltManager} = require("devtools/tilt/tilt"); let tilt = TiltManager.getTiltForBrowser(this.presenter.chromeWindow); e.preventDefault(); e.stopPropagation(); tilt.destroy(tilt.currentWindowId, true); } }, /** * Called when a key is released. */ _onKeyUp: function TVC__onKeyUp(e) { let code = e.keyCode || e.which; if (code === e.DOM_VK_X) { this.presenter.deleteNode(); } if (code === e.DOM_VK_F) { let highlight = this.presenter._highlight; let zoom = this.presenter.transforms.zoom; this.arcball.moveIntoView(vec3.lerp( vec3.scale(highlight.v0, zoom, []), vec3.scale(highlight.v1, zoom, []), 0.5)); } if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); this.arcball.keyUp(code); } }, /** * Called when the canvas looses focus. */ _onBlur: function TVC__onBlur(e) { this.arcball.cancelKeyEvents(); }, /** * Called when the content window of the current browser is resized. */ _onResize: function TVC__onResize(e) { let zoom = this.presenter._getPageZoom(); let width = e.target.innerWidth * zoom; let height = e.target.innerHeight * zoom; this.arcball.resize(width, height); }, /** * Checks if this object was initialized properly. * * @return {Boolean} true if the object was initialized properly */ isInitialized: function TVC_isInitialized() { return this.arcball ? true : false; }, /** * Function called when this object is destroyed. */ _finalize: function TVC__finalize() { TiltUtils.destroyObject(this.arcball); TiltUtils.destroyObject(this._coordinates); this.removeEventListeners(); this.presenter.controller = null; this.presenter._controllerUpdate = null; } }; /** * This is a general purpose 3D rotation controller described by Ken Shoemake * in the Graphics Interface ’92 Proceedings. It features good behavior * easy implementation, cheap execution. * * @param {Window} aChromeWindow * a reference to the top-level window * @param {Number} aWidth * the width of canvas * @param {Number} aHeight * the height of canvas * @param {Number} aRadius * optional, the radius of the arcball * @param {Array} aInitialTrans * optional, initial vector translation * @param {Array} aInitialRot * optional, initial quaternion rotation */ TiltVisualizer.Arcball = function TV_Arcball( aChromeWindow, aWidth, aHeight, aRadius, aInitialTrans, aInitialRot) { /** * Save a reference to the top-level window to set/remove intervals. */ this.chromeWindow = aChromeWindow; /** * Values retaining the current horizontal and vertical mouse coordinates. */ this._mousePress = vec3.create(); this._mouseRelease = vec3.create(); this._mouseMove = vec3.create(); this._mouseLerp = vec3.create(); this._mouseButton = -1; /** * Object retaining the current pressed key codes. */ this._keyCode = {}; /** * The vectors representing the mouse coordinates mapped on the arcball * and their perpendicular converted from (x, y) to (x, y, z) at specific * events like mousePressed and mouseDragged. */ this._startVec = vec3.create(); this._endVec = vec3.create(); this._pVec = vec3.create(); /** * The corresponding rotation quaternions. */ this._lastRot = quat4.create(); this._deltaRot = quat4.create(); this._currentRot = quat4.create(aInitialRot); /** * The current camera translation coordinates. */ this._lastTrans = vec3.create(); this._deltaTrans = vec3.create(); this._currentTrans = vec3.create(aInitialTrans); this._zoomAmount = 0; /** * Additional rotation and translation vectors. */ this._additionalRot = vec3.create(); this._additionalTrans = vec3.create(); this._deltaAdditionalRot = quat4.create(); this._deltaAdditionalTrans = vec3.create(); // load the keys controlling the arcball this._loadKeys(); // set the current dimensions of the arcball this.resize(aWidth, aHeight, aRadius); }; TiltVisualizer.Arcball.prototype = { /** * Call this function whenever you need the updated rotation quaternion * and the zoom amount. These values will be returned as "rotation" and * "translation" properties inside an object. * * @param {Number} aDelta * the current animation frame delta * * @return {Object} the rotation quaternion and the translation amount */ update: function TVA_update(aDelta) { let mousePress = this._mousePress; let mouseRelease = this._mouseRelease; let mouseMove = this._mouseMove; let mouseLerp = this._mouseLerp; let mouseButton = this._mouseButton; // smoothly update the mouse coordinates mouseLerp[0] += (mouseMove[0] - mouseLerp[0]) * ARCBALL_SENSITIVITY; mouseLerp[1] += (mouseMove[1] - mouseLerp[1]) * ARCBALL_SENSITIVITY; // cache the interpolated mouse coordinates let x = mouseLerp[0]; let y = mouseLerp[1]; // the smoothed arcball rotation may not be finished when the mouse is // pressed again, so cancel the rotation if other events occur or the // animation finishes if (mouseButton === 3 || x === mouseRelease[0] && y === mouseRelease[1]) { this._rotating = false; } let startVec = this._startVec; let endVec = this._endVec; let pVec = this._pVec; let lastRot = this._lastRot; let deltaRot = this._deltaRot; let currentRot = this._currentRot; // left mouse button handles rotation if (mouseButton === 1 || this._rotating) { // the rotation doesn't stop immediately after the left mouse button is // released, so add a flag to smoothly continue it until it ends this._rotating = true; // find the sphere coordinates of the mouse positions this._pointToSphere(x, y, this.width, this.height, this.radius, endVec); // compute the vector perpendicular to the start & end vectors vec3.cross(startVec, endVec, pVec); // if the begin and end vectors don't coincide if (vec3.length(pVec) > 0) { deltaRot[0] = pVec[0]; deltaRot[1] = pVec[1]; deltaRot[2] = pVec[2]; // in the quaternion values, w is cosine (theta / 2), // where theta is the rotation angle deltaRot[3] = -vec3.dot(startVec, endVec); } else { // return an identity rotation quaternion deltaRot[0] = 0; deltaRot[1] = 0; deltaRot[2] = 0; deltaRot[3] = 1; } // calculate the current rotation based on the mouse click events quat4.multiply(lastRot, deltaRot, currentRot); } else { // save the current quaternion to stack rotations quat4.set(currentRot, lastRot); } let lastTrans = this._lastTrans; let deltaTrans = this._deltaTrans; let currentTrans = this._currentTrans; // right mouse button handles panning if (mouseButton === 3) { // calculate a delta translation between the new and old mouse position // and save it to the current translation deltaTrans[0] = mouseMove[0] - mousePress[0]; deltaTrans[1] = mouseMove[1] - mousePress[1]; currentTrans[0] = lastTrans[0] + deltaTrans[0]; currentTrans[1] = lastTrans[1] + deltaTrans[1]; } else { // save the current panning to stack translations lastTrans[0] = currentTrans[0]; lastTrans[1] = currentTrans[1]; } let zoomAmount = this._zoomAmount; let keyCode = this._keyCode; // mouse wheel handles zooming deltaTrans[2] = (zoomAmount - currentTrans[2]) * ARCBALL_ZOOM_STEP; currentTrans[2] += deltaTrans[2]; let additionalRot = this._additionalRot; let additionalTrans = this._additionalTrans; let deltaAdditionalRot = this._deltaAdditionalRot; let deltaAdditionalTrans = this._deltaAdditionalTrans; let rotateKeys = this.rotateKeys; let panKeys = this.panKeys; let zoomKeys = this.zoomKeys; let resetKey = this.resetKey; // handle additional rotation and translation by the keyboard if (keyCode[rotateKeys.left]) { additionalRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; } if (keyCode[rotateKeys.right]) { additionalRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; } if (keyCode[rotateKeys.up]) { additionalRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; } if (keyCode[rotateKeys.down]) { additionalRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; } if (keyCode[panKeys.left]) { additionalTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; } if (keyCode[panKeys.right]) { additionalTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; } if (keyCode[panKeys.up]) { additionalTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; } if (keyCode[panKeys.down]) { additionalTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; } if (keyCode[zoomKeys["in"][0]] || keyCode[zoomKeys["in"][1]] || keyCode[zoomKeys["in"][2]]) { this.zoom(-ARCBALL_TRANSLATION_STEP); } if (keyCode[zoomKeys["out"][0]] || keyCode[zoomKeys["out"][1]]) { this.zoom(ARCBALL_TRANSLATION_STEP); } if (keyCode[zoomKeys["unzoom"]]) { this._zoomAmount = 0; } if (keyCode[resetKey]) { this.reset(); } // update the delta key rotations and translations deltaAdditionalRot[0] += (additionalRot[0] - deltaAdditionalRot[0]) * ARCBALL_SENSITIVITY; deltaAdditionalRot[1] += (additionalRot[1] - deltaAdditionalRot[1]) * ARCBALL_SENSITIVITY; deltaAdditionalRot[2] += (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY; deltaAdditionalTrans[0] += (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY; deltaAdditionalTrans[1] += (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY; // create an additional rotation based on the key events quat4.fromEuler( deltaAdditionalRot[0], deltaAdditionalRot[1], deltaAdditionalRot[2], deltaRot); // create an additional translation based on the key events vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans); // handle the reset animation steps if necessary if (this._resetInProgress) { this._nextResetStep(aDelta || 1); } // return the current rotation and translation return { rotation: quat4.multiply(deltaRot, currentRot), translation: vec3.add(deltaTrans, currentTrans) }; }, /** * Function handling the mouseDown event. * Call this when the mouse was pressed. * * @param {Number} x * the current horizontal coordinate of the mouse * @param {Number} y * the current vertical coordinate of the mouse * @param {Number} aButton * which mouse button was pressed */ mouseDown: function TVA_mouseDown(x, y, aButton) { // save the mouse down state and prepare for rotations or translations this._mousePress[0] = x; this._mousePress[1] = y; this._mouseButton = aButton; this._cancelReset(); this._save(); // find the sphere coordinates of the mouse positions this._pointToSphere( x, y, this.width, this.height, this.radius, this._startVec); quat4.set(this._currentRot, this._lastRot); }, /** * Function handling the mouseUp event. * Call this when a mouse button was released. * * @param {Number} x * the current horizontal coordinate of the mouse * @param {Number} y * the current vertical coordinate of the mouse */ mouseUp: function TVA_mouseUp(x, y) { // save the mouse up state and prepare for rotations or translations this._mouseRelease[0] = x; this._mouseRelease[1] = y; this._mouseButton = -1; }, /** * Function handling the mouseMove event. * Call this when the mouse was moved. * * @param {Number} x * the current horizontal coordinate of the mouse * @param {Number} y * the current vertical coordinate of the mouse */ mouseMove: function TVA_mouseMove(x, y) { // save the mouse move state and prepare for rotations or translations // only if the mouse is pressed if (this._mouseButton !== -1) { this._mouseMove[0] = x; this._mouseMove[1] = y; } }, /** * Function handling the mouseOver event. * Call this when the mouse enters the context bounds. */ mouseOver: function TVA_mouseOver() { // if the mouse just entered the parent bounds, stop the animation this._mouseButton = -1; }, /** * Function handling the mouseOut event. * Call this when the mouse leaves the context bounds. */ mouseOut: function TVA_mouseOut() { // if the mouse leaves the parent bounds, stop the animation this._mouseButton = -1; }, /** * Function handling the arcball zoom amount. * Call this, for example, when the mouse wheel was scrolled or zoom keys * were pressed. * * @param {Number} aZoom * the zoom direction and speed */ zoom: function TVA_zoom(aZoom) { this._cancelReset(); this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom, ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX); }, /** * Function handling the keyDown event. * Call this when a key was pressed. * * @param {Number} aCode * the code corresponding to the key pressed */ keyDown: function TVA_keyDown(aCode) { this._cancelReset(); this._keyCode[aCode] = true; }, /** * Function handling the keyUp event. * Call this when a key was released. * * @param {Number} aCode * the code corresponding to the key released */ keyUp: function TVA_keyUp(aCode) { this._keyCode[aCode] = false; }, /** * Maps the 2d coordinates of the mouse location to a 3d point on a sphere. * * @param {Number} x * the current horizontal coordinate of the mouse * @param {Number} y * the current vertical coordinate of the mouse * @param {Number} aWidth * the width of canvas * @param {Number} aHeight * the height of canvas * @param {Number} aRadius * optional, the radius of the arcball * @param {Array} aSphereVec * a 3d vector to store the sphere coordinates */ _pointToSphere: function TVA__pointToSphere( x, y, aWidth, aHeight, aRadius, aSphereVec) { // adjust point coords and scale down to range of [-1..1] x = (x - aWidth * 0.5) / aRadius; y = (y - aHeight * 0.5) / aRadius; // compute the square length of the vector to the point from the center let normal = 0; let sqlength = x * x + y * y; // if the point is mapped outside of the sphere if (sqlength > 1) { // calculate the normalization factor normal = 1 / Math.sqrt(sqlength); // set the normalized vector (a point on the sphere) aSphereVec[0] = x * normal; aSphereVec[1] = y * normal; aSphereVec[2] = 0; } else { // set the vector to a point mapped inside the sphere aSphereVec[0] = x; aSphereVec[1] = y; aSphereVec[2] = Math.sqrt(1 - sqlength); } }, /** * Cancels all pending transformations caused by key events. */ cancelKeyEvents: function TVA_cancelKeyEvents() { this._keyCode = {}; }, /** * Cancels all pending transformations caused by mouse events. */ cancelMouseEvents: function TVA_cancelMouseEvents() { this._rotating = false; this._mouseButton = -1; }, /** * Incremental translation method. * * @param {Array} aTranslation * the translation ammount on the [x, y] axis */ translate: function TVP_translate(aTranslation) { this._additionalTrans[0] += aTranslation[0]; this._additionalTrans[1] += aTranslation[1]; }, /** * Incremental rotation method. * * @param {Array} aRotation * the rotation ammount along the [x, y, z] axis */ rotate: function TVP_rotate(aRotation) { // explicitly rotate along y, x, z values because they're eulerian angles this._additionalRot[0] += TiltMath.radians(aRotation[1]); this._additionalRot[1] += TiltMath.radians(aRotation[0]); this._additionalRot[2] += TiltMath.radians(aRotation[2]); }, /** * Moves a target point into view only if it's outside the currently visible * area bounds (in which case it also resets any additional transforms). * * @param {Arary} aPoint * the [x, y] point which should be brought into view */ moveIntoView: function TVA_moveIntoView(aPoint) { let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]); let visiblePointY = -(this._currentTrans[1] + this._additionalTrans[1]); if (aPoint[1] - visiblePointY - MOVE_INTO_VIEW_ACCURACY > this.height || aPoint[1] - visiblePointY + MOVE_INTO_VIEW_ACCURACY < 0 || aPoint[0] - visiblePointX > this.width || aPoint[0] - visiblePointX < 0) { this.reset([0, -aPoint[1]]); } }, /** * Resize this implementation to use different bounds. * This function is automatically called when the arcball is created. * * @param {Number} newWidth * the new width of canvas * @param {Number} newHeight * the new height of canvas * @param {Number} newRadius * optional, the new radius of the arcball */ resize: function TVA_resize(newWidth, newHeight, newRadius) { if (!newWidth || !newHeight) { return; } // set the new width, height and radius dimensions this.width = newWidth; this.height = newHeight; this.radius = newRadius ? newRadius : Math.min(newWidth, newHeight); this._save(); }, /** * Starts an animation resetting the arcball transformations to identity. * * @param {Array} aFinalTranslation * optional, final vector translation * @param {Array} aFinalRotation * optional, final quaternion rotation */ reset: function TVA_reset(aFinalTranslation, aFinalRotation) { if ("function" === typeof this._onResetStart) { this._onResetStart(); this._onResetStart = null; } this.cancelMouseEvents(); this.cancelKeyEvents(); this._cancelReset(); this._save(); this._resetFinalTranslation = vec3.create(aFinalTranslation); this._resetFinalRotation = quat4.create(aFinalRotation); this._resetInProgress = true; }, /** * Cancels the current arcball reset animation if there is one. */ _cancelReset: function TVA__cancelReset() { if (this._resetInProgress) { this._resetInProgress = false; this._save(); if ("function" === typeof this._onResetFinish) { this._onResetFinish(); this._onResetFinish = null; this._onResetStep = null; } } }, /** * Executes the next step in the arcball reset animation. * * @param {Number} aDelta * the current animation frame delta */ _nextResetStep: function TVA__nextResetStep(aDelta) { // a very large animation frame delta (in case of seriously low framerate) // would cause all the interpolations to become highly unstable aDelta = TiltMath.clamp(aDelta, 1, 100); let fNearZero = EPSILON * EPSILON; let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta; let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR; let fTran = this._resetFinalTranslation; let fRot = this._resetFinalRotation; let t = vec3.create(fTran); let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot); // reset the rotation quaternion and translation vector vec3.lerp(this._currentTrans, t, fInterpLin); quat4.slerp(this._currentRot, r, fInterpSph); // also reset any additional transforms by the keyboard or mouse vec3.scale(this._additionalTrans, fInterpLin); vec3.scale(this._additionalRot, fInterpLin); this._zoomAmount *= fInterpLin; // clear the loop if the all values are very close to zero if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero && vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero && vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero && vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero && vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero && vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero && vec3.length(this._additionalRot) < fNearZero && vec3.length(this._additionalTrans) < fNearZero) { this._cancelReset(); } if ("function" === typeof this._onResetStep) { this._onResetStep(); } }, /** * Loads the keys to control this arcball. */ _loadKeys: function TVA__loadKeys() { this.rotateKeys = { "up": Ci.nsIDOMKeyEvent["DOM_VK_W"], "down": Ci.nsIDOMKeyEvent["DOM_VK_S"], "left": Ci.nsIDOMKeyEvent["DOM_VK_A"], "right": Ci.nsIDOMKeyEvent["DOM_VK_D"], }; this.panKeys = { "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"], "down": Ci.nsIDOMKeyEvent["DOM_VK_DOWN"], "left": Ci.nsIDOMKeyEvent["DOM_VK_LEFT"], "right": Ci.nsIDOMKeyEvent["DOM_VK_RIGHT"], }; this.zoomKeys = { "in": [ Ci.nsIDOMKeyEvent["DOM_VK_I"], Ci.nsIDOMKeyEvent["DOM_VK_ADD"], Ci.nsIDOMKeyEvent["DOM_VK_EQUALS"], ], "out": [ Ci.nsIDOMKeyEvent["DOM_VK_O"], Ci.nsIDOMKeyEvent["DOM_VK_SUBTRACT"], ], "unzoom": Ci.nsIDOMKeyEvent["DOM_VK_0"] }; this.resetKey = Ci.nsIDOMKeyEvent["DOM_VK_R"]; }, /** * Saves the current arcball state, typically after resize or mouse events. */ _save: function TVA__save() { if (this._mousePress) { let x = this._mousePress[0]; let y = this._mousePress[1]; this._mouseMove[0] = x; this._mouseMove[1] = y; this._mouseRelease[0] = x; this._mouseRelease[1] = y; this._mouseLerp[0] = x; this._mouseLerp[1] = y; } }, /** * Function called when this object is destroyed. */ _finalize: function TVA__finalize() { this._cancelReset(); } }; /** * Tilt configuration preferences. */ TiltVisualizer.Prefs = { /** * Specifies if Tilt is enabled or not. */ get enabled() { return this._enabled; }, set enabled(value) { TiltUtils.Preferences.set("enabled", "boolean", value); this._enabled = value; }, get introTransition() { return this._introTransition; }, set introTransition(value) { TiltUtils.Preferences.set("intro_transition", "boolean", value); this._introTransition = value; }, get outroTransition() { return this._outroTransition; }, set outroTransition(value) { TiltUtils.Preferences.set("outro_transition", "boolean", value); this._outroTransition = value; }, /** * Loads the preferences. */ load: function TVC_load() { let prefs = TiltVisualizer.Prefs; let get = TiltUtils.Preferences.get; prefs._enabled = get("enabled", "boolean"); prefs._introTransition = get("intro_transition", "boolean"); prefs._outroTransition = get("outro_transition", "boolean"); } }; /** * A custom visualization shader. * * @param {Attribute} vertexPosition: the vertex position * @param {Attribute} vertexTexCoord: texture coordinates used by the sampler * @param {Attribute} vertexColor: specific [r, g, b] color for each vertex * @param {Uniform} mvMatrix: the model view matrix * @param {Uniform} projMatrix: the projection matrix * @param {Uniform} sampler: the texture sampler to fetch the pixels from */ TiltVisualizer.MeshShader = { /** * Vertex shader. */ vs: [ "attribute vec3 vertexPosition;", "attribute vec2 vertexTexCoord;", "attribute vec3 vertexColor;", "uniform mat4 mvMatrix;", "uniform mat4 projMatrix;", "varying vec2 texCoord;", "varying vec3 color;", "void main() {", " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);", " texCoord = vertexTexCoord;", " color = vertexColor;", "}" ].join("\n"), /** * Fragment shader. */ fs: [ "#ifdef GL_ES", "precision lowp float;", "#endif", "uniform sampler2D sampler;", "varying vec2 texCoord;", "varying vec3 color;", "void main() {", " if (texCoord.x < 0.0) {", " gl_FragColor = vec4(color, 1.0);", " } else {", " gl_FragColor = vec4(texture2D(sampler, texCoord).rgb, 1.0);", " }", "}" ].join("\n") };