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