1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/tilt/tilt-visualizer.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,2253 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 +"use strict"; 1.10 + 1.11 +const {Cu, Ci, ChromeWorker} = require("chrome"); 1.12 + 1.13 +let TiltGL = require("devtools/tilt/tilt-gl"); 1.14 +let TiltUtils = require("devtools/tilt/tilt-utils"); 1.15 +let TiltVisualizerStyle = require("devtools/tilt/tilt-visualizer-style"); 1.16 +let {EPSILON, TiltMath, vec3, mat4, quat4} = require("devtools/tilt/tilt-math"); 1.17 +let {TargetFactory} = require("devtools/framework/target"); 1.18 + 1.19 +Cu.import("resource://gre/modules/Services.jsm"); 1.20 +Cu.import("resource:///modules/devtools/gDevTools.jsm"); 1.21 + 1.22 +const ELEMENT_MIN_SIZE = 4; 1.23 +const INVISIBLE_ELEMENTS = { 1.24 + "head": true, 1.25 + "base": true, 1.26 + "basefont": true, 1.27 + "isindex": true, 1.28 + "link": true, 1.29 + "meta": true, 1.30 + "option": true, 1.31 + "script": true, 1.32 + "style": true, 1.33 + "title": true 1.34 +}; 1.35 + 1.36 +// a node is represented in the visualization mesh as a rectangular stack 1.37 +// of 5 quads composed of 12 vertices; we draw these as triangles using an 1.38 +// index buffer of 12 unsigned int elements, obviously one for each vertex; 1.39 +// if a webpage has enough nodes to overflow the index buffer elements size, 1.40 +// weird things may happen; thus, when necessary, we'll split into groups 1.41 +const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1; 1.42 + 1.43 +const WIREFRAME_COLOR = [0, 0, 0, 0.25]; 1.44 +const INTRO_TRANSITION_DURATION = 1000; 1.45 +const OUTRO_TRANSITION_DURATION = 800; 1.46 +const INITIAL_Z_TRANSLATION = 400; 1.47 +const MOVE_INTO_VIEW_ACCURACY = 50; 1.48 + 1.49 +const MOUSE_CLICK_THRESHOLD = 10; 1.50 +const MOUSE_INTRO_DELAY = 200; 1.51 +const ARCBALL_SENSITIVITY = 0.5; 1.52 +const ARCBALL_ROTATION_STEP = 0.15; 1.53 +const ARCBALL_TRANSLATION_STEP = 35; 1.54 +const ARCBALL_ZOOM_STEP = 0.1; 1.55 +const ARCBALL_ZOOM_MIN = -3000; 1.56 +const ARCBALL_ZOOM_MAX = 500; 1.57 +const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1; 1.58 +const ARCBALL_RESET_LINEAR_FACTOR = 0.01; 1.59 + 1.60 +const TILT_CRAFTER = "resource:///modules/devtools/tilt/TiltWorkerCrafter.js"; 1.61 +const TILT_PICKER = "resource:///modules/devtools/tilt/TiltWorkerPicker.js"; 1.62 + 1.63 + 1.64 +/** 1.65 + * Initializes the visualization presenter and controller. 1.66 + * 1.67 + * @param {Object} aProperties 1.68 + * an object containing the following properties: 1.69 + * {Window} chromeWindow: a reference to the top level window 1.70 + * {Window} contentWindow: the content window holding the visualized doc 1.71 + * {Element} parentNode: the parent node to hold the visualization 1.72 + * {Object} notifications: necessary notifications for Tilt 1.73 + * {Function} onError: optional, function called if initialization failed 1.74 + * {Function} onLoad: optional, function called if initialization worked 1.75 + */ 1.76 +function TiltVisualizer(aProperties) 1.77 +{ 1.78 + // make sure the properties parameter is a valid object 1.79 + aProperties = aProperties || {}; 1.80 + 1.81 + /** 1.82 + * Save a reference to the top-level window. 1.83 + */ 1.84 + this.chromeWindow = aProperties.chromeWindow; 1.85 + this.tab = aProperties.tab; 1.86 + 1.87 + /** 1.88 + * The canvas element used for rendering the visualization. 1.89 + */ 1.90 + this.canvas = TiltUtils.DOM.initCanvas(aProperties.parentNode, { 1.91 + focusable: true, 1.92 + append: true 1.93 + }); 1.94 + 1.95 + /** 1.96 + * Visualization logic and drawing loop. 1.97 + */ 1.98 + this.presenter = new TiltVisualizer.Presenter(this.canvas, 1.99 + aProperties.chromeWindow, 1.100 + aProperties.contentWindow, 1.101 + aProperties.notifications, 1.102 + aProperties.onError || null, 1.103 + aProperties.onLoad || null); 1.104 + 1.105 + /** 1.106 + * Visualization mouse and keyboard controller. 1.107 + */ 1.108 + this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter); 1.109 +} 1.110 + 1.111 +exports.TiltVisualizer = TiltVisualizer; 1.112 + 1.113 +TiltVisualizer.prototype = { 1.114 + 1.115 + /** 1.116 + * Initializes the visualizer. 1.117 + */ 1.118 + init: function TV_init() 1.119 + { 1.120 + this.presenter.init(); 1.121 + this.bindToInspector(this.tab); 1.122 + }, 1.123 + 1.124 + /** 1.125 + * Checks if this object was initialized properly. 1.126 + * 1.127 + * @return {Boolean} true if the object was initialized properly 1.128 + */ 1.129 + isInitialized: function TV_isInitialized() 1.130 + { 1.131 + return this.presenter && this.presenter.isInitialized() && 1.132 + this.controller && this.controller.isInitialized(); 1.133 + }, 1.134 + 1.135 + /** 1.136 + * Removes the overlay canvas used for rendering the visualization. 1.137 + */ 1.138 + removeOverlay: function TV_removeOverlay() 1.139 + { 1.140 + if (this.canvas && this.canvas.parentNode) { 1.141 + this.canvas.parentNode.removeChild(this.canvas); 1.142 + } 1.143 + }, 1.144 + 1.145 + /** 1.146 + * Explicitly cleans up this visualizer and sets everything to null. 1.147 + */ 1.148 + cleanup: function TV_cleanup() 1.149 + { 1.150 + this.unbindInspector(); 1.151 + 1.152 + if (this.controller) { 1.153 + TiltUtils.destroyObject(this.controller); 1.154 + } 1.155 + if (this.presenter) { 1.156 + TiltUtils.destroyObject(this.presenter); 1.157 + } 1.158 + 1.159 + let chromeWindow = this.chromeWindow; 1.160 + 1.161 + TiltUtils.destroyObject(this); 1.162 + TiltUtils.clearCache(); 1.163 + TiltUtils.gc(chromeWindow); 1.164 + }, 1.165 + 1.166 + /** 1.167 + * Listen to the inspector activity. 1.168 + */ 1.169 + bindToInspector: function TV_bindToInspector(aTab) 1.170 + { 1.171 + this._browserTab = aTab; 1.172 + 1.173 + this.onNewNodeFromInspector = this.onNewNodeFromInspector.bind(this); 1.174 + this.onNewNodeFromTilt = this.onNewNodeFromTilt.bind(this); 1.175 + this.onInspectorReady = this.onInspectorReady.bind(this); 1.176 + this.onToolboxDestroyed = this.onToolboxDestroyed.bind(this); 1.177 + 1.178 + gDevTools.on("inspector-ready", this.onInspectorReady); 1.179 + gDevTools.on("toolbox-destroyed", this.onToolboxDestroyed); 1.180 + 1.181 + Services.obs.addObserver(this.onNewNodeFromTilt, 1.182 + this.presenter.NOTIFICATIONS.HIGHLIGHTING, 1.183 + false); 1.184 + Services.obs.addObserver(this.onNewNodeFromTilt, 1.185 + this.presenter.NOTIFICATIONS.UNHIGHLIGHTING, 1.186 + false); 1.187 + 1.188 + let target = TargetFactory.forTab(aTab); 1.189 + let toolbox = gDevTools.getToolbox(target); 1.190 + if (toolbox) { 1.191 + let panel = toolbox.getPanel("inspector"); 1.192 + if (panel) { 1.193 + this.inspector = panel; 1.194 + this.inspector.selection.on("new-node", this.onNewNodeFromInspector); 1.195 + this.onNewNodeFromInspector(); 1.196 + } 1.197 + } 1.198 + }, 1.199 + 1.200 + /** 1.201 + * Unregister inspector event listeners. 1.202 + */ 1.203 + unbindInspector: function TV_unbindInspector() 1.204 + { 1.205 + this._browserTab = null; 1.206 + 1.207 + if (this.inspector) { 1.208 + if (this.inspector.selection) { 1.209 + this.inspector.selection.off("new-node", this.onNewNodeFromInspector); 1.210 + } 1.211 + this.inspector = null; 1.212 + } 1.213 + 1.214 + gDevTools.off("inspector-ready", this.onInspectorReady); 1.215 + gDevTools.off("toolbox-destroyed", this.onToolboxDestroyed); 1.216 + 1.217 + Services.obs.removeObserver(this.onNewNodeFromTilt, 1.218 + this.presenter.NOTIFICATIONS.HIGHLIGHTING); 1.219 + Services.obs.removeObserver(this.onNewNodeFromTilt, 1.220 + this.presenter.NOTIFICATIONS.UNHIGHLIGHTING); 1.221 + }, 1.222 + 1.223 + /** 1.224 + * When a new inspector is started. 1.225 + */ 1.226 + onInspectorReady: function TV_onInspectorReady(event, toolbox, panel) 1.227 + { 1.228 + if (toolbox.target.tab === this._browserTab) { 1.229 + this.inspector = panel; 1.230 + this.inspector.selection.on("new-node", this.onNewNodeFromInspector); 1.231 + this.onNewNodeFromTilt(); 1.232 + } 1.233 + }, 1.234 + 1.235 + /** 1.236 + * When the toolbox, therefor the inspector, is closed. 1.237 + */ 1.238 + onToolboxDestroyed: function TV_onToolboxDestroyed(event, tab) 1.239 + { 1.240 + if (tab === this._browserTab && 1.241 + this.inspector) { 1.242 + if (this.inspector.selection) { 1.243 + this.inspector.selection.off("new-node", this.onNewNodeFromInspector); 1.244 + } 1.245 + this.inspector = null; 1.246 + } 1.247 + }, 1.248 + 1.249 + /** 1.250 + * When a new node is selected in the inspector. 1.251 + */ 1.252 + onNewNodeFromInspector: function TV_onNewNodeFromInspector() 1.253 + { 1.254 + if (this.inspector && 1.255 + this.inspector.selection.reason != "tilt") { 1.256 + let selection = this.inspector.selection; 1.257 + let canHighlightNode = selection.isNode() && 1.258 + selection.isConnected() && 1.259 + selection.isElementNode(); 1.260 + if (canHighlightNode) { 1.261 + this.presenter.highlightNode(selection.node); 1.262 + } else { 1.263 + this.presenter.highlightNodeFor(-1); 1.264 + } 1.265 + } 1.266 + }, 1.267 + 1.268 + /** 1.269 + * When a new node is selected in Tilt. 1.270 + */ 1.271 + onNewNodeFromTilt: function TV_onNewNodeFromTilt() 1.272 + { 1.273 + if (!this.inspector) { 1.274 + return; 1.275 + } 1.276 + let nodeIndex = this.presenter._currentSelection; 1.277 + if (nodeIndex < 0) { 1.278 + this.inspector.selection.setNodeFront(null, "tilt"); 1.279 + } 1.280 + let node = this.presenter._traverseData.nodes[nodeIndex]; 1.281 + node = this.inspector.walker.frontForRawNode(node); 1.282 + this.inspector.selection.setNodeFront(node, "tilt"); 1.283 + }, 1.284 +}; 1.285 + 1.286 +/** 1.287 + * This object manages the visualization logic and drawing loop. 1.288 + * 1.289 + * @param {HTMLCanvasElement} aCanvas 1.290 + * the canvas element used for rendering 1.291 + * @param {Window} aChromeWindow 1.292 + * a reference to the top-level window 1.293 + * @param {Window} aContentWindow 1.294 + * the content window holding the document to be visualized 1.295 + * @param {Object} aNotifications 1.296 + * necessary notifications for Tilt 1.297 + * @param {Function} onError 1.298 + * function called if initialization failed 1.299 + * @param {Function} onLoad 1.300 + * function called if initialization worked 1.301 + */ 1.302 +TiltVisualizer.Presenter = function TV_Presenter( 1.303 + aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad) 1.304 +{ 1.305 + /** 1.306 + * A canvas overlay used for drawing the visualization. 1.307 + */ 1.308 + this.canvas = aCanvas; 1.309 + 1.310 + /** 1.311 + * Save a reference to the top-level window, to access Tilt. 1.312 + */ 1.313 + this.chromeWindow = aChromeWindow; 1.314 + 1.315 + /** 1.316 + * The content window generating the visualization 1.317 + */ 1.318 + this.contentWindow = aContentWindow; 1.319 + 1.320 + /** 1.321 + * Shortcut for accessing notifications strings. 1.322 + */ 1.323 + this.NOTIFICATIONS = aNotifications; 1.324 + 1.325 + /** 1.326 + * Use the default node callback function 1.327 + */ 1.328 + this.nodeCallback = null; 1.329 + 1.330 + /** 1.331 + * Create the renderer, containing useful functions for easy drawing. 1.332 + */ 1.333 + this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad); 1.334 + 1.335 + /** 1.336 + * A custom shader used for drawing the visualization mesh. 1.337 + */ 1.338 + this._visualizationProgram = null; 1.339 + 1.340 + /** 1.341 + * The combined mesh representing the document visualization. 1.342 + */ 1.343 + this._texture = null; 1.344 + this._meshData = null; 1.345 + this._meshStacks = null; 1.346 + this._meshWireframe = null; 1.347 + this._traverseData = null; 1.348 + 1.349 + /** 1.350 + * A highlight quad drawn over a stacked dom node. 1.351 + */ 1.352 + this._highlight = { 1.353 + disabled: true, 1.354 + v0: vec3.create(), 1.355 + v1: vec3.create(), 1.356 + v2: vec3.create(), 1.357 + v3: vec3.create() 1.358 + }; 1.359 + 1.360 + /** 1.361 + * Scene transformations, exposing offset, translation and rotation. 1.362 + * Modified by events in the controller through delegate functions. 1.363 + */ 1.364 + this.transforms = { 1.365 + zoom: 1, 1.366 + offset: vec3.create(), // mesh offset, aligned to the viewport center 1.367 + translation: vec3.create(), // scene translation, on the [x, y, z] axis 1.368 + rotation: quat4.create() // scene rotation, expressed as a quaternion 1.369 + }; 1.370 + 1.371 + /** 1.372 + * Variables holding information about the initial and current node selected. 1.373 + */ 1.374 + this._currentSelection = -1; // the selected node index 1.375 + this._initialMeshConfiguration = false; // true if the 3D mesh was configured 1.376 + 1.377 + /** 1.378 + * Variable specifying if the scene should be redrawn. 1.379 + * This should happen usually when the visualization is translated/rotated. 1.380 + */ 1.381 + this._redraw = true; 1.382 + 1.383 + /** 1.384 + * Total time passed since the rendering started. 1.385 + * If the rendering is paused, this property won't get updated. 1.386 + */ 1.387 + this._time = 0; 1.388 + 1.389 + /** 1.390 + * Frame delta time (the ammount of time passed for each frame). 1.391 + * This is used to smoothly interpolate animation transfroms. 1.392 + */ 1.393 + this._delta = 0; 1.394 + this._prevFrameTime = 0; 1.395 + this._currFrameTime = 0; 1.396 +}; 1.397 + 1.398 +TiltVisualizer.Presenter.prototype = { 1.399 + 1.400 + /** 1.401 + * Initializes the presenter and starts the animation loop 1.402 + */ 1.403 + init: function TVP_init() 1.404 + { 1.405 + this._setup(); 1.406 + this._loop(); 1.407 + }, 1.408 + 1.409 + /** 1.410 + * The initialization logic. 1.411 + */ 1.412 + _setup: function TVP__setup() 1.413 + { 1.414 + let renderer = this._renderer; 1.415 + 1.416 + // if the renderer was destroyed, don't continue setup 1.417 + if (!renderer || !renderer.context) { 1.418 + return; 1.419 + } 1.420 + 1.421 + // create the visualization shaders and program to draw the stacks mesh 1.422 + this._visualizationProgram = new renderer.Program({ 1.423 + vs: TiltVisualizer.MeshShader.vs, 1.424 + fs: TiltVisualizer.MeshShader.fs, 1.425 + attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"], 1.426 + uniforms: ["mvMatrix", "projMatrix", "sampler"] 1.427 + }); 1.428 + 1.429 + // get the document zoom to properly scale the visualization 1.430 + this.transforms.zoom = this._getPageZoom(); 1.431 + 1.432 + // bind the owner object to the necessary functions 1.433 + TiltUtils.bindObjectFunc(this, "^_on"); 1.434 + TiltUtils.bindObjectFunc(this, "_loop"); 1.435 + 1.436 + this._setupTexture(); 1.437 + this._setupMeshData(); 1.438 + this._setupEventListeners(); 1.439 + this.canvas.focus(); 1.440 + }, 1.441 + 1.442 + /** 1.443 + * Get page zoom factor. 1.444 + * @return {Number} 1.445 + */ 1.446 + _getPageZoom: function TVP__getPageZoom() { 1.447 + return this.contentWindow 1.448 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.449 + .getInterface(Ci.nsIDOMWindowUtils) 1.450 + .fullZoom; 1.451 + }, 1.452 + 1.453 + /** 1.454 + * The animation logic. 1.455 + */ 1.456 + _loop: function TVP__loop() 1.457 + { 1.458 + let renderer = this._renderer; 1.459 + 1.460 + // if the renderer was destroyed, don't continue rendering 1.461 + if (!renderer || !renderer.context) { 1.462 + return; 1.463 + } 1.464 + 1.465 + // prepare for the next frame of the animation loop 1.466 + this.chromeWindow.mozRequestAnimationFrame(this._loop); 1.467 + 1.468 + // only redraw if we really have to 1.469 + if (this._redraw) { 1.470 + this._redraw = false; 1.471 + this._drawVisualization(); 1.472 + } 1.473 + 1.474 + // update the current presenter transfroms from the controller 1.475 + if ("function" === typeof this._controllerUpdate) { 1.476 + this._controllerUpdate(this._time, this._delta); 1.477 + } 1.478 + 1.479 + this._handleFrameDelta(); 1.480 + this._handleKeyframeNotifications(); 1.481 + }, 1.482 + 1.483 + /** 1.484 + * Calculates the current frame delta time. 1.485 + */ 1.486 + _handleFrameDelta: function TVP__handleFrameDelta() 1.487 + { 1.488 + this._prevFrameTime = this._currFrameTime; 1.489 + this._currFrameTime = this.chromeWindow.mozAnimationStartTime; 1.490 + this._delta = this._currFrameTime - this._prevFrameTime; 1.491 + }, 1.492 + 1.493 + /** 1.494 + * Draws the visualization mesh and highlight quad. 1.495 + */ 1.496 + _drawVisualization: function TVP__drawVisualization() 1.497 + { 1.498 + let renderer = this._renderer; 1.499 + let transforms = this.transforms; 1.500 + let w = renderer.width; 1.501 + let h = renderer.height; 1.502 + let ih = renderer.initialHeight; 1.503 + 1.504 + // if the mesh wasn't created yet, don't continue rendering 1.505 + if (!this._meshStacks || !this._meshWireframe) { 1.506 + return; 1.507 + } 1.508 + 1.509 + // clear the context to an opaque black background 1.510 + renderer.clear(); 1.511 + renderer.perspective(); 1.512 + 1.513 + // apply a transition transformation using an ortho and perspective matrix 1.514 + let ortho = mat4.ortho(0, w, h, 0, -1000, 1000); 1.515 + 1.516 + if (!this._isExecutingDestruction) { 1.517 + let f = this._time / INTRO_TRANSITION_DURATION; 1.518 + renderer.lerp(renderer.projMatrix, ortho, f, 8); 1.519 + } else { 1.520 + let f = this._time / OUTRO_TRANSITION_DURATION; 1.521 + renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8); 1.522 + } 1.523 + 1.524 + // apply the preliminary transformations to the model view 1.525 + renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION); 1.526 + 1.527 + // calculate the camera matrix using the rotation and translation 1.528 + renderer.translate(transforms.translation[0], 0, 1.529 + transforms.translation[2]); 1.530 + 1.531 + renderer.transform(quat4.toMat4(transforms.rotation)); 1.532 + 1.533 + // offset the visualization mesh to center 1.534 + renderer.translate(transforms.offset[0], 1.535 + transforms.offset[1] + transforms.translation[1], 0); 1.536 + 1.537 + renderer.scale(transforms.zoom, transforms.zoom); 1.538 + 1.539 + // draw the visualization mesh 1.540 + renderer.strokeWeight(2); 1.541 + renderer.depthTest(true); 1.542 + this._drawMeshStacks(); 1.543 + this._drawMeshWireframe(); 1.544 + this._drawHighlight(); 1.545 + 1.546 + // make sure the initial transition is drawn until finished 1.547 + if (this._time < INTRO_TRANSITION_DURATION || 1.548 + this._time < OUTRO_TRANSITION_DURATION) { 1.549 + this._redraw = true; 1.550 + } 1.551 + this._time += this._delta; 1.552 + }, 1.553 + 1.554 + /** 1.555 + * Draws the meshStacks object. 1.556 + */ 1.557 + _drawMeshStacks: function TVP__drawMeshStacks() 1.558 + { 1.559 + let renderer = this._renderer; 1.560 + let mesh = this._meshStacks; 1.561 + 1.562 + let visualizationProgram = this._visualizationProgram; 1.563 + let texture = this._texture; 1.564 + let mvMatrix = renderer.mvMatrix; 1.565 + let projMatrix = renderer.projMatrix; 1.566 + 1.567 + // use the necessary shader 1.568 + visualizationProgram.use(); 1.569 + 1.570 + for (let i = 0, len = mesh.length; i < len; i++) { 1.571 + let group = mesh[i]; 1.572 + 1.573 + // bind the attributes and uniforms as necessary 1.574 + visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices); 1.575 + visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord); 1.576 + visualizationProgram.bindVertexBuffer("vertexColor", group.color); 1.577 + 1.578 + visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix); 1.579 + visualizationProgram.bindUniformMatrix("projMatrix", projMatrix); 1.580 + visualizationProgram.bindTexture("sampler", texture); 1.581 + 1.582 + // draw the vertices as TRIANGLES indexed elements 1.583 + renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices); 1.584 + } 1.585 + 1.586 + // save the current model view and projection matrices 1.587 + mesh.mvMatrix = mat4.create(mvMatrix); 1.588 + mesh.projMatrix = mat4.create(projMatrix); 1.589 + }, 1.590 + 1.591 + /** 1.592 + * Draws the meshWireframe object. 1.593 + */ 1.594 + _drawMeshWireframe: function TVP__drawMeshWireframe() 1.595 + { 1.596 + let renderer = this._renderer; 1.597 + let mesh = this._meshWireframe; 1.598 + 1.599 + for (let i = 0, len = mesh.length; i < len; i++) { 1.600 + let group = mesh[i]; 1.601 + 1.602 + // use the necessary shader 1.603 + renderer.useColorShader(group.vertices, WIREFRAME_COLOR); 1.604 + 1.605 + // draw the vertices as LINES indexed elements 1.606 + renderer.drawIndexedVertices(renderer.context.LINES, group.indices); 1.607 + } 1.608 + }, 1.609 + 1.610 + /** 1.611 + * Draws a highlighted quad around a currently selected node. 1.612 + */ 1.613 + _drawHighlight: function TVP__drawHighlight() 1.614 + { 1.615 + // check if there's anything to highlight (i.e any node is selected) 1.616 + if (!this._highlight.disabled) { 1.617 + 1.618 + // set the corresponding state to draw the highlight quad 1.619 + let renderer = this._renderer; 1.620 + let highlight = this._highlight; 1.621 + 1.622 + renderer.depthTest(false); 1.623 + renderer.fill(highlight.fill, 0.5); 1.624 + renderer.stroke(highlight.stroke); 1.625 + renderer.strokeWeight(highlight.strokeWeight); 1.626 + renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3); 1.627 + } 1.628 + }, 1.629 + 1.630 + /** 1.631 + * Creates or refreshes the texture applied to the visualization mesh. 1.632 + */ 1.633 + _setupTexture: function TVP__setupTexture() 1.634 + { 1.635 + let renderer = this._renderer; 1.636 + 1.637 + // destroy any previously created texture 1.638 + TiltUtils.destroyObject(this._texture); this._texture = null; 1.639 + 1.640 + // if the renderer was destroyed, don't continue setup 1.641 + if (!renderer || !renderer.context) { 1.642 + return; 1.643 + } 1.644 + 1.645 + // get the maximum texture size 1.646 + this._maxTextureSize = 1.647 + renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE); 1.648 + 1.649 + // use a simple shim to get the image representation of the document 1.650 + // this will be removed once the MOZ_window_region_texture bug #653656 1.651 + // is finished; currently just converting the document image to a texture 1.652 + // applied to the mesh 1.653 + this._texture = new renderer.Texture({ 1.654 + source: TiltGL.TextureUtils.createContentImage(this.contentWindow, 1.655 + this._maxTextureSize), 1.656 + format: "RGB" 1.657 + }); 1.658 + 1.659 + if ("function" === typeof this._onSetupTexture) { 1.660 + this._onSetupTexture(); 1.661 + this._onSetupTexture = null; 1.662 + } 1.663 + }, 1.664 + 1.665 + /** 1.666 + * Create the combined mesh representing the document visualization by 1.667 + * traversing the document & adding a stack for each node that is drawable. 1.668 + * 1.669 + * @param {Object} aMeshData 1.670 + * object containing the necessary mesh verts, texcoord etc. 1.671 + */ 1.672 + _setupMesh: function TVP__setupMesh(aMeshData) 1.673 + { 1.674 + let renderer = this._renderer; 1.675 + 1.676 + // destroy any previously created mesh 1.677 + TiltUtils.destroyObject(this._meshStacks); this._meshStacks = []; 1.678 + TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = []; 1.679 + 1.680 + // if the renderer was destroyed, don't continue setup 1.681 + if (!renderer || !renderer.context) { 1.682 + return; 1.683 + } 1.684 + 1.685 + // save the mesh data for future use 1.686 + this._meshData = aMeshData; 1.687 + 1.688 + // create a sub-mesh for each group in the mesh data 1.689 + for (let i = 0, len = aMeshData.groups.length; i < len; i++) { 1.690 + let group = aMeshData.groups[i]; 1.691 + 1.692 + // create the visualization mesh using the vertices, texture coordinates 1.693 + // and indices computed when traversing the document object model 1.694 + this._meshStacks.push({ 1.695 + vertices: new renderer.VertexBuffer(group.vertices, 3), 1.696 + texCoord: new renderer.VertexBuffer(group.texCoord, 2), 1.697 + color: new renderer.VertexBuffer(group.color, 3), 1.698 + indices: new renderer.IndexBuffer(group.stacksIndices) 1.699 + }); 1.700 + 1.701 + // additionally, create a wireframe representation to make the 1.702 + // visualization a bit more pretty 1.703 + this._meshWireframe.push({ 1.704 + vertices: this._meshStacks[i].vertices, 1.705 + indices: new renderer.IndexBuffer(group.wireframeIndices) 1.706 + }); 1.707 + } 1.708 + 1.709 + // configure the required mesh transformations and background only once 1.710 + if (!this._initialMeshConfiguration) { 1.711 + this._initialMeshConfiguration = true; 1.712 + 1.713 + // set the necessary mesh offsets 1.714 + this.transforms.offset[0] = -renderer.width * 0.5; 1.715 + this.transforms.offset[1] = -renderer.height * 0.5; 1.716 + 1.717 + // make sure the canvas is opaque now that the initialization is finished 1.718 + this.canvas.style.background = TiltVisualizerStyle.canvas.background; 1.719 + 1.720 + this._drawVisualization(); 1.721 + this._redraw = true; 1.722 + } 1.723 + 1.724 + if ("function" === typeof this._onSetupMesh) { 1.725 + this._onSetupMesh(); 1.726 + this._onSetupMesh = null; 1.727 + } 1.728 + }, 1.729 + 1.730 + /** 1.731 + * Computes the mesh vertices, texture coordinates etc. by groups of nodes. 1.732 + */ 1.733 + _setupMeshData: function TVP__setupMeshData() 1.734 + { 1.735 + let renderer = this._renderer; 1.736 + 1.737 + // if the renderer was destroyed, don't continue setup 1.738 + if (!renderer || !renderer.context) { 1.739 + return; 1.740 + } 1.741 + 1.742 + // traverse the document and get the depths, coordinates and local names 1.743 + this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, { 1.744 + nodeCallback: this.nodeCallback, 1.745 + invisibleElements: INVISIBLE_ELEMENTS, 1.746 + minSize: ELEMENT_MIN_SIZE, 1.747 + maxX: this._texture.width, 1.748 + maxY: this._texture.height 1.749 + }); 1.750 + 1.751 + let worker = new ChromeWorker(TILT_CRAFTER); 1.752 + 1.753 + worker.addEventListener("message", function TVP_onMessage(event) { 1.754 + this._setupMesh(event.data); 1.755 + }.bind(this), false); 1.756 + 1.757 + // calculate necessary information regarding vertices, texture coordinates 1.758 + // etc. in a separate thread, as this process may take a while 1.759 + worker.postMessage({ 1.760 + maxGroupNodes: MAX_GROUP_NODES, 1.761 + style: TiltVisualizerStyle.nodes, 1.762 + texWidth: this._texture.width, 1.763 + texHeight: this._texture.height, 1.764 + nodesInfo: this._traverseData.info 1.765 + }); 1.766 + }, 1.767 + 1.768 + /** 1.769 + * Sets up event listeners necessary for the presenter. 1.770 + */ 1.771 + _setupEventListeners: function TVP__setupEventListeners() 1.772 + { 1.773 + this.contentWindow.addEventListener("resize", this._onResize, false); 1.774 + }, 1.775 + 1.776 + /** 1.777 + * Called when the content window of the current browser is resized. 1.778 + */ 1.779 + _onResize: function TVP_onResize(e) 1.780 + { 1.781 + let zoom = this._getPageZoom(); 1.782 + let width = e.target.innerWidth * zoom; 1.783 + let height = e.target.innerHeight * zoom; 1.784 + 1.785 + // handle aspect ratio changes to update the projection matrix 1.786 + this._renderer.width = width; 1.787 + this._renderer.height = height; 1.788 + 1.789 + this._redraw = true; 1.790 + }, 1.791 + 1.792 + /** 1.793 + * Highlights a specific node. 1.794 + * 1.795 + * @param {Element} aNode 1.796 + * the html node to be highlighted 1.797 + * @param {String} aFlags 1.798 + * flags specifying highlighting options 1.799 + */ 1.800 + highlightNode: function TVP_highlightNode(aNode, aFlags) 1.801 + { 1.802 + this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags); 1.803 + }, 1.804 + 1.805 + /** 1.806 + * Picks a stacked dom node at the x and y screen coordinates and highlights 1.807 + * the selected node in the mesh. 1.808 + * 1.809 + * @param {Number} x 1.810 + * the current horizontal coordinate of the mouse 1.811 + * @param {Number} y 1.812 + * the current vertical coordinate of the mouse 1.813 + * @param {Object} aProperties 1.814 + * an object containing the following properties: 1.815 + * {Function} onpick: function to be called after picking succeeded 1.816 + * {Function} onfail: function to be called after picking failed 1.817 + */ 1.818 + highlightNodeAt: function TVP_highlightNodeAt(x, y, aProperties) 1.819 + { 1.820 + // make sure the properties parameter is a valid object 1.821 + aProperties = aProperties || {}; 1.822 + 1.823 + // try to pick a mesh node using the current x, y coordinates 1.824 + this.pickNode(x, y, { 1.825 + 1.826 + /** 1.827 + * Mesh picking failed (nothing was found for the picked point). 1.828 + */ 1.829 + onfail: function TVP_onHighlightFail() 1.830 + { 1.831 + this.highlightNodeFor(-1); 1.832 + 1.833 + if ("function" === typeof aProperties.onfail) { 1.834 + aProperties.onfail(); 1.835 + } 1.836 + }.bind(this), 1.837 + 1.838 + /** 1.839 + * Mesh picking succeeded. 1.840 + * 1.841 + * @param {Object} aIntersection 1.842 + * object containing the intersection details 1.843 + */ 1.844 + onpick: function TVP_onHighlightPick(aIntersection) 1.845 + { 1.846 + this.highlightNodeFor(aIntersection.index); 1.847 + 1.848 + if ("function" === typeof aProperties.onpick) { 1.849 + aProperties.onpick(); 1.850 + } 1.851 + }.bind(this) 1.852 + }); 1.853 + }, 1.854 + 1.855 + /** 1.856 + * Sets the corresponding highlight coordinates and color based on the 1.857 + * information supplied. 1.858 + * 1.859 + * @param {Number} aNodeIndex 1.860 + * the index of the node in the this._traverseData array 1.861 + * @param {String} aFlags 1.862 + * flags specifying highlighting options 1.863 + */ 1.864 + highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags) 1.865 + { 1.866 + this._redraw = true; 1.867 + 1.868 + // if the node was already selected, don't do anything 1.869 + if (this._currentSelection === aNodeIndex) { 1.870 + return; 1.871 + } 1.872 + 1.873 + // if an invalid or nonexisted node is specified, disable the highlight 1.874 + if (aNodeIndex < 0) { 1.875 + this._currentSelection = -1; 1.876 + this._highlight.disabled = true; 1.877 + 1.878 + Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.UNHIGHLIGHTING, null); 1.879 + return; 1.880 + } 1.881 + 1.882 + let highlight = this._highlight; 1.883 + let info = this._traverseData.info[aNodeIndex]; 1.884 + let style = TiltVisualizerStyle.nodes; 1.885 + 1.886 + highlight.disabled = false; 1.887 + highlight.fill = style[info.name] || style.highlight.defaultFill; 1.888 + highlight.stroke = style.highlight.defaultStroke; 1.889 + highlight.strokeWeight = style.highlight.defaultStrokeWeight; 1.890 + 1.891 + let x = info.coord.left; 1.892 + let y = info.coord.top; 1.893 + let w = info.coord.width; 1.894 + let h = info.coord.height; 1.895 + let z = info.coord.depth + info.coord.thickness; 1.896 + 1.897 + vec3.set([x, y, z], highlight.v0); 1.898 + vec3.set([x + w, y, z], highlight.v1); 1.899 + vec3.set([x + w, y + h, z], highlight.v2); 1.900 + vec3.set([x, y + h, z], highlight.v3); 1.901 + 1.902 + this._currentSelection = aNodeIndex; 1.903 + 1.904 + // if something is highlighted, make sure it's inside the current viewport; 1.905 + // the point which should be moved into view is considered the center [x, y] 1.906 + // position along the top edge of the currently selected node 1.907 + 1.908 + if (aFlags && aFlags.indexOf("moveIntoView") !== -1) 1.909 + { 1.910 + this.controller.arcball.moveIntoView(vec3.lerp( 1.911 + vec3.scale(this._highlight.v0, this.transforms.zoom, []), 1.912 + vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5)); 1.913 + } 1.914 + 1.915 + Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.HIGHLIGHTING, null); 1.916 + }, 1.917 + 1.918 + /** 1.919 + * Deletes a node from the visualization mesh. 1.920 + * 1.921 + * @param {Number} aNodeIndex 1.922 + * the index of the node in the this._traverseData array; 1.923 + * if not specified, it will default to the current selection 1.924 + */ 1.925 + deleteNode: function TVP_deleteNode(aNodeIndex) 1.926 + { 1.927 + // we probably don't want to delete the html or body node.. just sayin' 1.928 + if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) { 1.929 + return; 1.930 + } 1.931 + 1.932 + let renderer = this._renderer; 1.933 + 1.934 + let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES); 1.935 + let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES); 1.936 + let group = this._meshStacks[groupIndex]; 1.937 + let vertices = group.vertices.components; 1.938 + 1.939 + for (let i = 0, k = 36 * nodeIndex; i < 36; i++) { 1.940 + vertices[i + k] = 0; 1.941 + } 1.942 + 1.943 + group.vertices = new renderer.VertexBuffer(vertices, 3); 1.944 + this._highlight.disabled = true; 1.945 + this._redraw = true; 1.946 + 1.947 + Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.NODE_REMOVED, null); 1.948 + }, 1.949 + 1.950 + /** 1.951 + * Picks a stacked dom node at the x and y screen coordinates and issues 1.952 + * a callback function with the found intersection. 1.953 + * 1.954 + * @param {Number} x 1.955 + * the current horizontal coordinate of the mouse 1.956 + * @param {Number} y 1.957 + * the current vertical coordinate of the mouse 1.958 + * @param {Object} aProperties 1.959 + * an object containing the following properties: 1.960 + * {Function} onpick: function to be called at intersection 1.961 + * {Function} onfail: function to be called if no intersections 1.962 + */ 1.963 + pickNode: function TVP_pickNode(x, y, aProperties) 1.964 + { 1.965 + // make sure the properties parameter is a valid object 1.966 + aProperties = aProperties || {}; 1.967 + 1.968 + // if the mesh wasn't created yet, don't continue picking 1.969 + if (!this._meshStacks || !this._meshWireframe) { 1.970 + return; 1.971 + } 1.972 + 1.973 + let worker = new ChromeWorker(TILT_PICKER); 1.974 + 1.975 + worker.addEventListener("message", function TVP_onMessage(event) { 1.976 + if (event.data) { 1.977 + if ("function" === typeof aProperties.onpick) { 1.978 + aProperties.onpick(event.data); 1.979 + } 1.980 + } else { 1.981 + if ("function" === typeof aProperties.onfail) { 1.982 + aProperties.onfail(); 1.983 + } 1.984 + } 1.985 + }, false); 1.986 + 1.987 + let zoom = this._getPageZoom(); 1.988 + let width = this._renderer.width * zoom; 1.989 + let height = this._renderer.height * zoom; 1.990 + x *= zoom; 1.991 + y *= zoom; 1.992 + 1.993 + // create a ray following the mouse direction from the near clipping plane 1.994 + // to the far clipping plane, to check for intersections with the mesh, 1.995 + // and do all the heavy lifting in a separate thread 1.996 + worker.postMessage({ 1.997 + vertices: this._meshData.allVertices, 1.998 + 1.999 + // create the ray destined for 3D picking 1.1000 + ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height], 1.1001 + this._meshStacks.mvMatrix, 1.1002 + this._meshStacks.projMatrix) 1.1003 + }); 1.1004 + }, 1.1005 + 1.1006 + /** 1.1007 + * Delegate translation method, used by the controller. 1.1008 + * 1.1009 + * @param {Array} aTranslation 1.1010 + * the new translation on the [x, y, z] axis 1.1011 + */ 1.1012 + setTranslation: function TVP_setTranslation(aTranslation) 1.1013 + { 1.1014 + let x = aTranslation[0]; 1.1015 + let y = aTranslation[1]; 1.1016 + let z = aTranslation[2]; 1.1017 + let transforms = this.transforms; 1.1018 + 1.1019 + // only update the translation if it's not already set 1.1020 + if (transforms.translation[0] !== x || 1.1021 + transforms.translation[1] !== y || 1.1022 + transforms.translation[2] !== z) { 1.1023 + 1.1024 + vec3.set(aTranslation, transforms.translation); 1.1025 + this._redraw = true; 1.1026 + } 1.1027 + }, 1.1028 + 1.1029 + /** 1.1030 + * Delegate rotation method, used by the controller. 1.1031 + * 1.1032 + * @param {Array} aQuaternion 1.1033 + * the rotation quaternion, as [x, y, z, w] 1.1034 + */ 1.1035 + setRotation: function TVP_setRotation(aQuaternion) 1.1036 + { 1.1037 + let x = aQuaternion[0]; 1.1038 + let y = aQuaternion[1]; 1.1039 + let z = aQuaternion[2]; 1.1040 + let w = aQuaternion[3]; 1.1041 + let transforms = this.transforms; 1.1042 + 1.1043 + // only update the rotation if it's not already set 1.1044 + if (transforms.rotation[0] !== x || 1.1045 + transforms.rotation[1] !== y || 1.1046 + transforms.rotation[2] !== z || 1.1047 + transforms.rotation[3] !== w) { 1.1048 + 1.1049 + quat4.set(aQuaternion, transforms.rotation); 1.1050 + this._redraw = true; 1.1051 + } 1.1052 + }, 1.1053 + 1.1054 + /** 1.1055 + * Handles notifications at specific frame counts. 1.1056 + */ 1.1057 + _handleKeyframeNotifications: function TV__handleKeyframeNotifications() 1.1058 + { 1.1059 + if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) { 1.1060 + this._time = INTRO_TRANSITION_DURATION; 1.1061 + } 1.1062 + if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) { 1.1063 + this._time = OUTRO_TRANSITION_DURATION; 1.1064 + } 1.1065 + 1.1066 + if (this._time >= INTRO_TRANSITION_DURATION && 1.1067 + !this._isInitializationFinished && 1.1068 + !this._isExecutingDestruction) { 1.1069 + 1.1070 + this._isInitializationFinished = true; 1.1071 + Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.INITIALIZED, null); 1.1072 + 1.1073 + if ("function" === typeof this._onInitializationFinished) { 1.1074 + this._onInitializationFinished(); 1.1075 + } 1.1076 + } 1.1077 + 1.1078 + if (this._time >= OUTRO_TRANSITION_DURATION && 1.1079 + !this._isDestructionFinished && 1.1080 + this._isExecutingDestruction) { 1.1081 + 1.1082 + this._isDestructionFinished = true; 1.1083 + Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.BEFORE_DESTROYED, null); 1.1084 + 1.1085 + if ("function" === typeof this._onDestructionFinished) { 1.1086 + this._onDestructionFinished(); 1.1087 + } 1.1088 + } 1.1089 + }, 1.1090 + 1.1091 + /** 1.1092 + * Starts executing the destruction sequence and issues a callback function 1.1093 + * when finished. 1.1094 + * 1.1095 + * @param {Function} aCallback 1.1096 + * the destruction finished callback 1.1097 + */ 1.1098 + executeDestruction: function TV_executeDestruction(aCallback) 1.1099 + { 1.1100 + if (!this._isExecutingDestruction) { 1.1101 + this._isExecutingDestruction = true; 1.1102 + this._onDestructionFinished = aCallback; 1.1103 + 1.1104 + // if we execute the destruction after the initialization finishes, 1.1105 + // proceed normally; otherwise, skip everything and immediately issue 1.1106 + // the callback 1.1107 + 1.1108 + if (this._time > OUTRO_TRANSITION_DURATION) { 1.1109 + this._time = 0; 1.1110 + this._redraw = true; 1.1111 + } else { 1.1112 + aCallback(); 1.1113 + } 1.1114 + } 1.1115 + }, 1.1116 + 1.1117 + /** 1.1118 + * Checks if this object was initialized properly. 1.1119 + * 1.1120 + * @return {Boolean} true if the object was initialized properly 1.1121 + */ 1.1122 + isInitialized: function TVP_isInitialized() 1.1123 + { 1.1124 + return this._renderer && this._renderer.context; 1.1125 + }, 1.1126 + 1.1127 + /** 1.1128 + * Function called when this object is destroyed. 1.1129 + */ 1.1130 + _finalize: function TVP__finalize() 1.1131 + { 1.1132 + TiltUtils.destroyObject(this._visualizationProgram); 1.1133 + TiltUtils.destroyObject(this._texture); 1.1134 + 1.1135 + if (this._meshStacks) { 1.1136 + this._meshStacks.forEach(function(group) { 1.1137 + TiltUtils.destroyObject(group.vertices); 1.1138 + TiltUtils.destroyObject(group.texCoord); 1.1139 + TiltUtils.destroyObject(group.color); 1.1140 + TiltUtils.destroyObject(group.indices); 1.1141 + }); 1.1142 + } 1.1143 + if (this._meshWireframe) { 1.1144 + this._meshWireframe.forEach(function(group) { 1.1145 + TiltUtils.destroyObject(group.indices); 1.1146 + }); 1.1147 + } 1.1148 + 1.1149 + TiltUtils.destroyObject(this._renderer); 1.1150 + 1.1151 + // Closing the tab would result in contentWindow being a dead object, 1.1152 + // so operations like removing event listeners won't work anymore. 1.1153 + if (this.contentWindow == this.chromeWindow.content) { 1.1154 + this.contentWindow.removeEventListener("resize", this._onResize, false); 1.1155 + } 1.1156 + } 1.1157 +}; 1.1158 + 1.1159 +/** 1.1160 + * A mouse and keyboard controller implementation. 1.1161 + * 1.1162 + * @param {HTMLCanvasElement} aCanvas 1.1163 + * the visualization canvas element 1.1164 + * @param {TiltVisualizer.Presenter} aPresenter 1.1165 + * the presenter instance to control 1.1166 + */ 1.1167 +TiltVisualizer.Controller = function TV_Controller(aCanvas, aPresenter) 1.1168 +{ 1.1169 + /** 1.1170 + * A canvas overlay on which mouse and keyboard event listeners are attached. 1.1171 + */ 1.1172 + this.canvas = aCanvas; 1.1173 + 1.1174 + /** 1.1175 + * Save a reference to the presenter to modify its model-view transforms. 1.1176 + */ 1.1177 + this.presenter = aPresenter; 1.1178 + this.presenter.controller = this; 1.1179 + 1.1180 + /** 1.1181 + * The initial controller dimensions and offset, in pixels. 1.1182 + */ 1.1183 + this._zoom = aPresenter.transforms.zoom; 1.1184 + this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom; 1.1185 + this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom; 1.1186 + this._width = aCanvas.width; 1.1187 + this._height = aCanvas.height; 1.1188 + 1.1189 + /** 1.1190 + * Arcball used to control the visualization using the mouse. 1.1191 + */ 1.1192 + this.arcball = new TiltVisualizer.Arcball( 1.1193 + this.presenter.chromeWindow, this._width, this._height, 0, 1.1194 + [ 1.1195 + this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0, 1.1196 + this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0 1.1197 + ]); 1.1198 + 1.1199 + /** 1.1200 + * Object containing the rotation quaternion and the translation amount. 1.1201 + */ 1.1202 + this._coordinates = null; 1.1203 + 1.1204 + // bind the owner object to the necessary functions 1.1205 + TiltUtils.bindObjectFunc(this, "_update"); 1.1206 + TiltUtils.bindObjectFunc(this, "^_on"); 1.1207 + 1.1208 + // add the necessary event listeners 1.1209 + this.addEventListeners(); 1.1210 + 1.1211 + // attach this controller's update function to the presenter ondraw event 1.1212 + this.presenter._controllerUpdate = this._update; 1.1213 +}; 1.1214 + 1.1215 +TiltVisualizer.Controller.prototype = { 1.1216 + 1.1217 + /** 1.1218 + * Adds events listeners required by this controller. 1.1219 + */ 1.1220 + addEventListeners: function TVC_addEventListeners() 1.1221 + { 1.1222 + let canvas = this.canvas; 1.1223 + let presenter = this.presenter; 1.1224 + 1.1225 + // bind commonly used mouse and keyboard events with the controller 1.1226 + canvas.addEventListener("mousedown", this._onMouseDown, false); 1.1227 + canvas.addEventListener("mouseup", this._onMouseUp, false); 1.1228 + canvas.addEventListener("mousemove", this._onMouseMove, false); 1.1229 + canvas.addEventListener("mouseover", this._onMouseOver, false); 1.1230 + canvas.addEventListener("mouseout", this._onMouseOut, false); 1.1231 + canvas.addEventListener("MozMousePixelScroll", this._onMozScroll, false); 1.1232 + canvas.addEventListener("keydown", this._onKeyDown, false); 1.1233 + canvas.addEventListener("keyup", this._onKeyUp, false); 1.1234 + canvas.addEventListener("blur", this._onBlur, false); 1.1235 + 1.1236 + // handle resize events to change the arcball dimensions 1.1237 + presenter.contentWindow.addEventListener("resize", this._onResize, false); 1.1238 + }, 1.1239 + 1.1240 + /** 1.1241 + * Removes all added events listeners required by this controller. 1.1242 + */ 1.1243 + removeEventListeners: function TVC_removeEventListeners() 1.1244 + { 1.1245 + let canvas = this.canvas; 1.1246 + let presenter = this.presenter; 1.1247 + 1.1248 + canvas.removeEventListener("mousedown", this._onMouseDown, false); 1.1249 + canvas.removeEventListener("mouseup", this._onMouseUp, false); 1.1250 + canvas.removeEventListener("mousemove", this._onMouseMove, false); 1.1251 + canvas.removeEventListener("mouseover", this._onMouseOver, false); 1.1252 + canvas.removeEventListener("mouseout", this._onMouseOut, false); 1.1253 + canvas.removeEventListener("MozMousePixelScroll", this._onMozScroll, false); 1.1254 + canvas.removeEventListener("keydown", this._onKeyDown, false); 1.1255 + canvas.removeEventListener("keyup", this._onKeyUp, false); 1.1256 + canvas.removeEventListener("blur", this._onBlur, false); 1.1257 + 1.1258 + // Closing the tab would result in contentWindow being a dead object, 1.1259 + // so operations like removing event listeners won't work anymore. 1.1260 + if (presenter.contentWindow == presenter.chromeWindow.content) { 1.1261 + presenter.contentWindow.removeEventListener("resize", this._onResize, false); 1.1262 + } 1.1263 + }, 1.1264 + 1.1265 + /** 1.1266 + * Function called each frame, updating the visualization camera transforms. 1.1267 + * 1.1268 + * @param {Number} aTime 1.1269 + * total time passed since rendering started 1.1270 + * @param {Number} aDelta 1.1271 + * the current animation frame delta 1.1272 + */ 1.1273 + _update: function TVC__update(aTime, aDelta) 1.1274 + { 1.1275 + this._time = aTime; 1.1276 + this._coordinates = this.arcball.update(aDelta); 1.1277 + 1.1278 + this.presenter.setRotation(this._coordinates.rotation); 1.1279 + this.presenter.setTranslation(this._coordinates.translation); 1.1280 + }, 1.1281 + 1.1282 + /** 1.1283 + * Called once after every time a mouse button is pressed. 1.1284 + */ 1.1285 + _onMouseDown: function TVC__onMouseDown(e) 1.1286 + { 1.1287 + e.target.focus(); 1.1288 + e.preventDefault(); 1.1289 + e.stopPropagation(); 1.1290 + 1.1291 + if (this._time < MOUSE_INTRO_DELAY) { 1.1292 + return; 1.1293 + } 1.1294 + 1.1295 + // calculate x and y coordinates using using the client and target offset 1.1296 + let button = e.which; 1.1297 + this._downX = e.clientX - e.target.offsetLeft; 1.1298 + this._downY = e.clientY - e.target.offsetTop; 1.1299 + 1.1300 + this.arcball.mouseDown(this._downX, this._downY, button); 1.1301 + }, 1.1302 + 1.1303 + /** 1.1304 + * Called every time a mouse button is released. 1.1305 + */ 1.1306 + _onMouseUp: function TVC__onMouseUp(e) 1.1307 + { 1.1308 + e.preventDefault(); 1.1309 + e.stopPropagation(); 1.1310 + 1.1311 + if (this._time < MOUSE_INTRO_DELAY) { 1.1312 + return; 1.1313 + } 1.1314 + 1.1315 + // calculate x and y coordinates using using the client and target offset 1.1316 + let button = e.which; 1.1317 + let upX = e.clientX - e.target.offsetLeft; 1.1318 + let upY = e.clientY - e.target.offsetTop; 1.1319 + 1.1320 + // a click in Tilt is issued only when the mouse pointer stays in 1.1321 + // relatively the same position 1.1322 + if (Math.abs(this._downX - upX) < MOUSE_CLICK_THRESHOLD && 1.1323 + Math.abs(this._downY - upY) < MOUSE_CLICK_THRESHOLD) { 1.1324 + 1.1325 + this.presenter.highlightNodeAt(upX, upY); 1.1326 + } 1.1327 + 1.1328 + this.arcball.mouseUp(upX, upY, button); 1.1329 + }, 1.1330 + 1.1331 + /** 1.1332 + * Called every time the mouse moves. 1.1333 + */ 1.1334 + _onMouseMove: function TVC__onMouseMove(e) 1.1335 + { 1.1336 + e.preventDefault(); 1.1337 + e.stopPropagation(); 1.1338 + 1.1339 + if (this._time < MOUSE_INTRO_DELAY) { 1.1340 + return; 1.1341 + } 1.1342 + 1.1343 + // calculate x and y coordinates using using the client and target offset 1.1344 + let moveX = e.clientX - e.target.offsetLeft; 1.1345 + let moveY = e.clientY - e.target.offsetTop; 1.1346 + 1.1347 + this.arcball.mouseMove(moveX, moveY); 1.1348 + }, 1.1349 + 1.1350 + /** 1.1351 + * Called when the mouse leaves the visualization bounds. 1.1352 + */ 1.1353 + _onMouseOver: function TVC__onMouseOver(e) 1.1354 + { 1.1355 + e.preventDefault(); 1.1356 + e.stopPropagation(); 1.1357 + 1.1358 + this.arcball.mouseOver(); 1.1359 + }, 1.1360 + 1.1361 + /** 1.1362 + * Called when the mouse leaves the visualization bounds. 1.1363 + */ 1.1364 + _onMouseOut: function TVC__onMouseOut(e) 1.1365 + { 1.1366 + e.preventDefault(); 1.1367 + e.stopPropagation(); 1.1368 + 1.1369 + this.arcball.mouseOut(); 1.1370 + }, 1.1371 + 1.1372 + /** 1.1373 + * Called when the mouse wheel is used. 1.1374 + */ 1.1375 + _onMozScroll: function TVC__onMozScroll(e) 1.1376 + { 1.1377 + e.preventDefault(); 1.1378 + e.stopPropagation(); 1.1379 + 1.1380 + this.arcball.zoom(e.detail); 1.1381 + }, 1.1382 + 1.1383 + /** 1.1384 + * Called when a key is pressed. 1.1385 + */ 1.1386 + _onKeyDown: function TVC__onKeyDown(e) 1.1387 + { 1.1388 + let code = e.keyCode || e.which; 1.1389 + 1.1390 + if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { 1.1391 + e.preventDefault(); 1.1392 + e.stopPropagation(); 1.1393 + this.arcball.keyDown(code); 1.1394 + } else { 1.1395 + this.arcball.cancelKeyEvents(); 1.1396 + } 1.1397 + 1.1398 + if (e.keyCode === e.DOM_VK_ESCAPE) { 1.1399 + let {TiltManager} = require("devtools/tilt/tilt"); 1.1400 + let tilt = 1.1401 + TiltManager.getTiltForBrowser(this.presenter.chromeWindow); 1.1402 + e.preventDefault(); 1.1403 + e.stopPropagation(); 1.1404 + tilt.destroy(tilt.currentWindowId, true); 1.1405 + } 1.1406 + }, 1.1407 + 1.1408 + /** 1.1409 + * Called when a key is released. 1.1410 + */ 1.1411 + _onKeyUp: function TVC__onKeyUp(e) 1.1412 + { 1.1413 + let code = e.keyCode || e.which; 1.1414 + 1.1415 + if (code === e.DOM_VK_X) { 1.1416 + this.presenter.deleteNode(); 1.1417 + } 1.1418 + if (code === e.DOM_VK_F) { 1.1419 + let highlight = this.presenter._highlight; 1.1420 + let zoom = this.presenter.transforms.zoom; 1.1421 + 1.1422 + this.arcball.moveIntoView(vec3.lerp( 1.1423 + vec3.scale(highlight.v0, zoom, []), 1.1424 + vec3.scale(highlight.v1, zoom, []), 0.5)); 1.1425 + } 1.1426 + if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { 1.1427 + e.preventDefault(); 1.1428 + e.stopPropagation(); 1.1429 + this.arcball.keyUp(code); 1.1430 + } 1.1431 + }, 1.1432 + 1.1433 + /** 1.1434 + * Called when the canvas looses focus. 1.1435 + */ 1.1436 + _onBlur: function TVC__onBlur(e) { 1.1437 + this.arcball.cancelKeyEvents(); 1.1438 + }, 1.1439 + 1.1440 + /** 1.1441 + * Called when the content window of the current browser is resized. 1.1442 + */ 1.1443 + _onResize: function TVC__onResize(e) 1.1444 + { 1.1445 + let zoom = this.presenter._getPageZoom(); 1.1446 + let width = e.target.innerWidth * zoom; 1.1447 + let height = e.target.innerHeight * zoom; 1.1448 + 1.1449 + this.arcball.resize(width, height); 1.1450 + }, 1.1451 + 1.1452 + /** 1.1453 + * Checks if this object was initialized properly. 1.1454 + * 1.1455 + * @return {Boolean} true if the object was initialized properly 1.1456 + */ 1.1457 + isInitialized: function TVC_isInitialized() 1.1458 + { 1.1459 + return this.arcball ? true : false; 1.1460 + }, 1.1461 + 1.1462 + /** 1.1463 + * Function called when this object is destroyed. 1.1464 + */ 1.1465 + _finalize: function TVC__finalize() 1.1466 + { 1.1467 + TiltUtils.destroyObject(this.arcball); 1.1468 + TiltUtils.destroyObject(this._coordinates); 1.1469 + 1.1470 + this.removeEventListeners(); 1.1471 + this.presenter.controller = null; 1.1472 + this.presenter._controllerUpdate = null; 1.1473 + } 1.1474 +}; 1.1475 + 1.1476 +/** 1.1477 + * This is a general purpose 3D rotation controller described by Ken Shoemake 1.1478 + * in the Graphics Interface ’92 Proceedings. It features good behavior 1.1479 + * easy implementation, cheap execution. 1.1480 + * 1.1481 + * @param {Window} aChromeWindow 1.1482 + * a reference to the top-level window 1.1483 + * @param {Number} aWidth 1.1484 + * the width of canvas 1.1485 + * @param {Number} aHeight 1.1486 + * the height of canvas 1.1487 + * @param {Number} aRadius 1.1488 + * optional, the radius of the arcball 1.1489 + * @param {Array} aInitialTrans 1.1490 + * optional, initial vector translation 1.1491 + * @param {Array} aInitialRot 1.1492 + * optional, initial quaternion rotation 1.1493 + */ 1.1494 +TiltVisualizer.Arcball = function TV_Arcball( 1.1495 + aChromeWindow, aWidth, aHeight, aRadius, aInitialTrans, aInitialRot) 1.1496 +{ 1.1497 + /** 1.1498 + * Save a reference to the top-level window to set/remove intervals. 1.1499 + */ 1.1500 + this.chromeWindow = aChromeWindow; 1.1501 + 1.1502 + /** 1.1503 + * Values retaining the current horizontal and vertical mouse coordinates. 1.1504 + */ 1.1505 + this._mousePress = vec3.create(); 1.1506 + this._mouseRelease = vec3.create(); 1.1507 + this._mouseMove = vec3.create(); 1.1508 + this._mouseLerp = vec3.create(); 1.1509 + this._mouseButton = -1; 1.1510 + 1.1511 + /** 1.1512 + * Object retaining the current pressed key codes. 1.1513 + */ 1.1514 + this._keyCode = {}; 1.1515 + 1.1516 + /** 1.1517 + * The vectors representing the mouse coordinates mapped on the arcball 1.1518 + * and their perpendicular converted from (x, y) to (x, y, z) at specific 1.1519 + * events like mousePressed and mouseDragged. 1.1520 + */ 1.1521 + this._startVec = vec3.create(); 1.1522 + this._endVec = vec3.create(); 1.1523 + this._pVec = vec3.create(); 1.1524 + 1.1525 + /** 1.1526 + * The corresponding rotation quaternions. 1.1527 + */ 1.1528 + this._lastRot = quat4.create(); 1.1529 + this._deltaRot = quat4.create(); 1.1530 + this._currentRot = quat4.create(aInitialRot); 1.1531 + 1.1532 + /** 1.1533 + * The current camera translation coordinates. 1.1534 + */ 1.1535 + this._lastTrans = vec3.create(); 1.1536 + this._deltaTrans = vec3.create(); 1.1537 + this._currentTrans = vec3.create(aInitialTrans); 1.1538 + this._zoomAmount = 0; 1.1539 + 1.1540 + /** 1.1541 + * Additional rotation and translation vectors. 1.1542 + */ 1.1543 + this._additionalRot = vec3.create(); 1.1544 + this._additionalTrans = vec3.create(); 1.1545 + this._deltaAdditionalRot = quat4.create(); 1.1546 + this._deltaAdditionalTrans = vec3.create(); 1.1547 + 1.1548 + // load the keys controlling the arcball 1.1549 + this._loadKeys(); 1.1550 + 1.1551 + // set the current dimensions of the arcball 1.1552 + this.resize(aWidth, aHeight, aRadius); 1.1553 +}; 1.1554 + 1.1555 +TiltVisualizer.Arcball.prototype = { 1.1556 + 1.1557 + /** 1.1558 + * Call this function whenever you need the updated rotation quaternion 1.1559 + * and the zoom amount. These values will be returned as "rotation" and 1.1560 + * "translation" properties inside an object. 1.1561 + * 1.1562 + * @param {Number} aDelta 1.1563 + * the current animation frame delta 1.1564 + * 1.1565 + * @return {Object} the rotation quaternion and the translation amount 1.1566 + */ 1.1567 + update: function TVA_update(aDelta) 1.1568 + { 1.1569 + let mousePress = this._mousePress; 1.1570 + let mouseRelease = this._mouseRelease; 1.1571 + let mouseMove = this._mouseMove; 1.1572 + let mouseLerp = this._mouseLerp; 1.1573 + let mouseButton = this._mouseButton; 1.1574 + 1.1575 + // smoothly update the mouse coordinates 1.1576 + mouseLerp[0] += (mouseMove[0] - mouseLerp[0]) * ARCBALL_SENSITIVITY; 1.1577 + mouseLerp[1] += (mouseMove[1] - mouseLerp[1]) * ARCBALL_SENSITIVITY; 1.1578 + 1.1579 + // cache the interpolated mouse coordinates 1.1580 + let x = mouseLerp[0]; 1.1581 + let y = mouseLerp[1]; 1.1582 + 1.1583 + // the smoothed arcball rotation may not be finished when the mouse is 1.1584 + // pressed again, so cancel the rotation if other events occur or the 1.1585 + // animation finishes 1.1586 + if (mouseButton === 3 || x === mouseRelease[0] && y === mouseRelease[1]) { 1.1587 + this._rotating = false; 1.1588 + } 1.1589 + 1.1590 + let startVec = this._startVec; 1.1591 + let endVec = this._endVec; 1.1592 + let pVec = this._pVec; 1.1593 + 1.1594 + let lastRot = this._lastRot; 1.1595 + let deltaRot = this._deltaRot; 1.1596 + let currentRot = this._currentRot; 1.1597 + 1.1598 + // left mouse button handles rotation 1.1599 + if (mouseButton === 1 || this._rotating) { 1.1600 + // the rotation doesn't stop immediately after the left mouse button is 1.1601 + // released, so add a flag to smoothly continue it until it ends 1.1602 + this._rotating = true; 1.1603 + 1.1604 + // find the sphere coordinates of the mouse positions 1.1605 + this._pointToSphere(x, y, this.width, this.height, this.radius, endVec); 1.1606 + 1.1607 + // compute the vector perpendicular to the start & end vectors 1.1608 + vec3.cross(startVec, endVec, pVec); 1.1609 + 1.1610 + // if the begin and end vectors don't coincide 1.1611 + if (vec3.length(pVec) > 0) { 1.1612 + deltaRot[0] = pVec[0]; 1.1613 + deltaRot[1] = pVec[1]; 1.1614 + deltaRot[2] = pVec[2]; 1.1615 + 1.1616 + // in the quaternion values, w is cosine (theta / 2), 1.1617 + // where theta is the rotation angle 1.1618 + deltaRot[3] = -vec3.dot(startVec, endVec); 1.1619 + } else { 1.1620 + // return an identity rotation quaternion 1.1621 + deltaRot[0] = 0; 1.1622 + deltaRot[1] = 0; 1.1623 + deltaRot[2] = 0; 1.1624 + deltaRot[3] = 1; 1.1625 + } 1.1626 + 1.1627 + // calculate the current rotation based on the mouse click events 1.1628 + quat4.multiply(lastRot, deltaRot, currentRot); 1.1629 + } else { 1.1630 + // save the current quaternion to stack rotations 1.1631 + quat4.set(currentRot, lastRot); 1.1632 + } 1.1633 + 1.1634 + let lastTrans = this._lastTrans; 1.1635 + let deltaTrans = this._deltaTrans; 1.1636 + let currentTrans = this._currentTrans; 1.1637 + 1.1638 + // right mouse button handles panning 1.1639 + if (mouseButton === 3) { 1.1640 + // calculate a delta translation between the new and old mouse position 1.1641 + // and save it to the current translation 1.1642 + deltaTrans[0] = mouseMove[0] - mousePress[0]; 1.1643 + deltaTrans[1] = mouseMove[1] - mousePress[1]; 1.1644 + 1.1645 + currentTrans[0] = lastTrans[0] + deltaTrans[0]; 1.1646 + currentTrans[1] = lastTrans[1] + deltaTrans[1]; 1.1647 + } else { 1.1648 + // save the current panning to stack translations 1.1649 + lastTrans[0] = currentTrans[0]; 1.1650 + lastTrans[1] = currentTrans[1]; 1.1651 + } 1.1652 + 1.1653 + let zoomAmount = this._zoomAmount; 1.1654 + let keyCode = this._keyCode; 1.1655 + 1.1656 + // mouse wheel handles zooming 1.1657 + deltaTrans[2] = (zoomAmount - currentTrans[2]) * ARCBALL_ZOOM_STEP; 1.1658 + currentTrans[2] += deltaTrans[2]; 1.1659 + 1.1660 + let additionalRot = this._additionalRot; 1.1661 + let additionalTrans = this._additionalTrans; 1.1662 + let deltaAdditionalRot = this._deltaAdditionalRot; 1.1663 + let deltaAdditionalTrans = this._deltaAdditionalTrans; 1.1664 + 1.1665 + let rotateKeys = this.rotateKeys; 1.1666 + let panKeys = this.panKeys; 1.1667 + let zoomKeys = this.zoomKeys; 1.1668 + let resetKey = this.resetKey; 1.1669 + 1.1670 + // handle additional rotation and translation by the keyboard 1.1671 + if (keyCode[rotateKeys.left]) { 1.1672 + additionalRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; 1.1673 + } 1.1674 + if (keyCode[rotateKeys.right]) { 1.1675 + additionalRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; 1.1676 + } 1.1677 + if (keyCode[rotateKeys.up]) { 1.1678 + additionalRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; 1.1679 + } 1.1680 + if (keyCode[rotateKeys.down]) { 1.1681 + additionalRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; 1.1682 + } 1.1683 + if (keyCode[panKeys.left]) { 1.1684 + additionalTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; 1.1685 + } 1.1686 + if (keyCode[panKeys.right]) { 1.1687 + additionalTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; 1.1688 + } 1.1689 + if (keyCode[panKeys.up]) { 1.1690 + additionalTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; 1.1691 + } 1.1692 + if (keyCode[panKeys.down]) { 1.1693 + additionalTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; 1.1694 + } 1.1695 + if (keyCode[zoomKeys["in"][0]] || 1.1696 + keyCode[zoomKeys["in"][1]] || 1.1697 + keyCode[zoomKeys["in"][2]]) { 1.1698 + this.zoom(-ARCBALL_TRANSLATION_STEP); 1.1699 + } 1.1700 + if (keyCode[zoomKeys["out"][0]] || 1.1701 + keyCode[zoomKeys["out"][1]]) { 1.1702 + this.zoom(ARCBALL_TRANSLATION_STEP); 1.1703 + } 1.1704 + if (keyCode[zoomKeys["unzoom"]]) { 1.1705 + this._zoomAmount = 0; 1.1706 + } 1.1707 + if (keyCode[resetKey]) { 1.1708 + this.reset(); 1.1709 + } 1.1710 + 1.1711 + // update the delta key rotations and translations 1.1712 + deltaAdditionalRot[0] += 1.1713 + (additionalRot[0] - deltaAdditionalRot[0]) * ARCBALL_SENSITIVITY; 1.1714 + deltaAdditionalRot[1] += 1.1715 + (additionalRot[1] - deltaAdditionalRot[1]) * ARCBALL_SENSITIVITY; 1.1716 + deltaAdditionalRot[2] += 1.1717 + (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY; 1.1718 + 1.1719 + deltaAdditionalTrans[0] += 1.1720 + (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY; 1.1721 + deltaAdditionalTrans[1] += 1.1722 + (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY; 1.1723 + 1.1724 + // create an additional rotation based on the key events 1.1725 + quat4.fromEuler( 1.1726 + deltaAdditionalRot[0], 1.1727 + deltaAdditionalRot[1], 1.1728 + deltaAdditionalRot[2], deltaRot); 1.1729 + 1.1730 + // create an additional translation based on the key events 1.1731 + vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans); 1.1732 + 1.1733 + // handle the reset animation steps if necessary 1.1734 + if (this._resetInProgress) { 1.1735 + this._nextResetStep(aDelta || 1); 1.1736 + } 1.1737 + 1.1738 + // return the current rotation and translation 1.1739 + return { 1.1740 + rotation: quat4.multiply(deltaRot, currentRot), 1.1741 + translation: vec3.add(deltaTrans, currentTrans) 1.1742 + }; 1.1743 + }, 1.1744 + 1.1745 + /** 1.1746 + * Function handling the mouseDown event. 1.1747 + * Call this when the mouse was pressed. 1.1748 + * 1.1749 + * @param {Number} x 1.1750 + * the current horizontal coordinate of the mouse 1.1751 + * @param {Number} y 1.1752 + * the current vertical coordinate of the mouse 1.1753 + * @param {Number} aButton 1.1754 + * which mouse button was pressed 1.1755 + */ 1.1756 + mouseDown: function TVA_mouseDown(x, y, aButton) 1.1757 + { 1.1758 + // save the mouse down state and prepare for rotations or translations 1.1759 + this._mousePress[0] = x; 1.1760 + this._mousePress[1] = y; 1.1761 + this._mouseButton = aButton; 1.1762 + this._cancelReset(); 1.1763 + this._save(); 1.1764 + 1.1765 + // find the sphere coordinates of the mouse positions 1.1766 + this._pointToSphere( 1.1767 + x, y, this.width, this.height, this.radius, this._startVec); 1.1768 + 1.1769 + quat4.set(this._currentRot, this._lastRot); 1.1770 + }, 1.1771 + 1.1772 + /** 1.1773 + * Function handling the mouseUp event. 1.1774 + * Call this when a mouse button was released. 1.1775 + * 1.1776 + * @param {Number} x 1.1777 + * the current horizontal coordinate of the mouse 1.1778 + * @param {Number} y 1.1779 + * the current vertical coordinate of the mouse 1.1780 + */ 1.1781 + mouseUp: function TVA_mouseUp(x, y) 1.1782 + { 1.1783 + // save the mouse up state and prepare for rotations or translations 1.1784 + this._mouseRelease[0] = x; 1.1785 + this._mouseRelease[1] = y; 1.1786 + this._mouseButton = -1; 1.1787 + }, 1.1788 + 1.1789 + /** 1.1790 + * Function handling the mouseMove event. 1.1791 + * Call this when the mouse was moved. 1.1792 + * 1.1793 + * @param {Number} x 1.1794 + * the current horizontal coordinate of the mouse 1.1795 + * @param {Number} y 1.1796 + * the current vertical coordinate of the mouse 1.1797 + */ 1.1798 + mouseMove: function TVA_mouseMove(x, y) 1.1799 + { 1.1800 + // save the mouse move state and prepare for rotations or translations 1.1801 + // only if the mouse is pressed 1.1802 + if (this._mouseButton !== -1) { 1.1803 + this._mouseMove[0] = x; 1.1804 + this._mouseMove[1] = y; 1.1805 + } 1.1806 + }, 1.1807 + 1.1808 + /** 1.1809 + * Function handling the mouseOver event. 1.1810 + * Call this when the mouse enters the context bounds. 1.1811 + */ 1.1812 + mouseOver: function TVA_mouseOver() 1.1813 + { 1.1814 + // if the mouse just entered the parent bounds, stop the animation 1.1815 + this._mouseButton = -1; 1.1816 + }, 1.1817 + 1.1818 + /** 1.1819 + * Function handling the mouseOut event. 1.1820 + * Call this when the mouse leaves the context bounds. 1.1821 + */ 1.1822 + mouseOut: function TVA_mouseOut() 1.1823 + { 1.1824 + // if the mouse leaves the parent bounds, stop the animation 1.1825 + this._mouseButton = -1; 1.1826 + }, 1.1827 + 1.1828 + /** 1.1829 + * Function handling the arcball zoom amount. 1.1830 + * Call this, for example, when the mouse wheel was scrolled or zoom keys 1.1831 + * were pressed. 1.1832 + * 1.1833 + * @param {Number} aZoom 1.1834 + * the zoom direction and speed 1.1835 + */ 1.1836 + zoom: function TVA_zoom(aZoom) 1.1837 + { 1.1838 + this._cancelReset(); 1.1839 + this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom, 1.1840 + ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX); 1.1841 + }, 1.1842 + 1.1843 + /** 1.1844 + * Function handling the keyDown event. 1.1845 + * Call this when a key was pressed. 1.1846 + * 1.1847 + * @param {Number} aCode 1.1848 + * the code corresponding to the key pressed 1.1849 + */ 1.1850 + keyDown: function TVA_keyDown(aCode) 1.1851 + { 1.1852 + this._cancelReset(); 1.1853 + this._keyCode[aCode] = true; 1.1854 + }, 1.1855 + 1.1856 + /** 1.1857 + * Function handling the keyUp event. 1.1858 + * Call this when a key was released. 1.1859 + * 1.1860 + * @param {Number} aCode 1.1861 + * the code corresponding to the key released 1.1862 + */ 1.1863 + keyUp: function TVA_keyUp(aCode) 1.1864 + { 1.1865 + this._keyCode[aCode] = false; 1.1866 + }, 1.1867 + 1.1868 + /** 1.1869 + * Maps the 2d coordinates of the mouse location to a 3d point on a sphere. 1.1870 + * 1.1871 + * @param {Number} x 1.1872 + * the current horizontal coordinate of the mouse 1.1873 + * @param {Number} y 1.1874 + * the current vertical coordinate of the mouse 1.1875 + * @param {Number} aWidth 1.1876 + * the width of canvas 1.1877 + * @param {Number} aHeight 1.1878 + * the height of canvas 1.1879 + * @param {Number} aRadius 1.1880 + * optional, the radius of the arcball 1.1881 + * @param {Array} aSphereVec 1.1882 + * a 3d vector to store the sphere coordinates 1.1883 + */ 1.1884 + _pointToSphere: function TVA__pointToSphere( 1.1885 + x, y, aWidth, aHeight, aRadius, aSphereVec) 1.1886 + { 1.1887 + // adjust point coords and scale down to range of [-1..1] 1.1888 + x = (x - aWidth * 0.5) / aRadius; 1.1889 + y = (y - aHeight * 0.5) / aRadius; 1.1890 + 1.1891 + // compute the square length of the vector to the point from the center 1.1892 + let normal = 0; 1.1893 + let sqlength = x * x + y * y; 1.1894 + 1.1895 + // if the point is mapped outside of the sphere 1.1896 + if (sqlength > 1) { 1.1897 + // calculate the normalization factor 1.1898 + normal = 1 / Math.sqrt(sqlength); 1.1899 + 1.1900 + // set the normalized vector (a point on the sphere) 1.1901 + aSphereVec[0] = x * normal; 1.1902 + aSphereVec[1] = y * normal; 1.1903 + aSphereVec[2] = 0; 1.1904 + } else { 1.1905 + // set the vector to a point mapped inside the sphere 1.1906 + aSphereVec[0] = x; 1.1907 + aSphereVec[1] = y; 1.1908 + aSphereVec[2] = Math.sqrt(1 - sqlength); 1.1909 + } 1.1910 + }, 1.1911 + 1.1912 + /** 1.1913 + * Cancels all pending transformations caused by key events. 1.1914 + */ 1.1915 + cancelKeyEvents: function TVA_cancelKeyEvents() 1.1916 + { 1.1917 + this._keyCode = {}; 1.1918 + }, 1.1919 + 1.1920 + /** 1.1921 + * Cancels all pending transformations caused by mouse events. 1.1922 + */ 1.1923 + cancelMouseEvents: function TVA_cancelMouseEvents() 1.1924 + { 1.1925 + this._rotating = false; 1.1926 + this._mouseButton = -1; 1.1927 + }, 1.1928 + 1.1929 + /** 1.1930 + * Incremental translation method. 1.1931 + * 1.1932 + * @param {Array} aTranslation 1.1933 + * the translation ammount on the [x, y] axis 1.1934 + */ 1.1935 + translate: function TVP_translate(aTranslation) 1.1936 + { 1.1937 + this._additionalTrans[0] += aTranslation[0]; 1.1938 + this._additionalTrans[1] += aTranslation[1]; 1.1939 + }, 1.1940 + 1.1941 + /** 1.1942 + * Incremental rotation method. 1.1943 + * 1.1944 + * @param {Array} aRotation 1.1945 + * the rotation ammount along the [x, y, z] axis 1.1946 + */ 1.1947 + rotate: function TVP_rotate(aRotation) 1.1948 + { 1.1949 + // explicitly rotate along y, x, z values because they're eulerian angles 1.1950 + this._additionalRot[0] += TiltMath.radians(aRotation[1]); 1.1951 + this._additionalRot[1] += TiltMath.radians(aRotation[0]); 1.1952 + this._additionalRot[2] += TiltMath.radians(aRotation[2]); 1.1953 + }, 1.1954 + 1.1955 + /** 1.1956 + * Moves a target point into view only if it's outside the currently visible 1.1957 + * area bounds (in which case it also resets any additional transforms). 1.1958 + * 1.1959 + * @param {Arary} aPoint 1.1960 + * the [x, y] point which should be brought into view 1.1961 + */ 1.1962 + moveIntoView: function TVA_moveIntoView(aPoint) { 1.1963 + let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]); 1.1964 + let visiblePointY = -(this._currentTrans[1] + this._additionalTrans[1]); 1.1965 + 1.1966 + if (aPoint[1] - visiblePointY - MOVE_INTO_VIEW_ACCURACY > this.height || 1.1967 + aPoint[1] - visiblePointY + MOVE_INTO_VIEW_ACCURACY < 0 || 1.1968 + aPoint[0] - visiblePointX > this.width || 1.1969 + aPoint[0] - visiblePointX < 0) { 1.1970 + this.reset([0, -aPoint[1]]); 1.1971 + } 1.1972 + }, 1.1973 + 1.1974 + /** 1.1975 + * Resize this implementation to use different bounds. 1.1976 + * This function is automatically called when the arcball is created. 1.1977 + * 1.1978 + * @param {Number} newWidth 1.1979 + * the new width of canvas 1.1980 + * @param {Number} newHeight 1.1981 + * the new height of canvas 1.1982 + * @param {Number} newRadius 1.1983 + * optional, the new radius of the arcball 1.1984 + */ 1.1985 + resize: function TVA_resize(newWidth, newHeight, newRadius) 1.1986 + { 1.1987 + if (!newWidth || !newHeight) { 1.1988 + return; 1.1989 + } 1.1990 + 1.1991 + // set the new width, height and radius dimensions 1.1992 + this.width = newWidth; 1.1993 + this.height = newHeight; 1.1994 + this.radius = newRadius ? newRadius : Math.min(newWidth, newHeight); 1.1995 + this._save(); 1.1996 + }, 1.1997 + 1.1998 + /** 1.1999 + * Starts an animation resetting the arcball transformations to identity. 1.2000 + * 1.2001 + * @param {Array} aFinalTranslation 1.2002 + * optional, final vector translation 1.2003 + * @param {Array} aFinalRotation 1.2004 + * optional, final quaternion rotation 1.2005 + */ 1.2006 + reset: function TVA_reset(aFinalTranslation, aFinalRotation) 1.2007 + { 1.2008 + if ("function" === typeof this._onResetStart) { 1.2009 + this._onResetStart(); 1.2010 + this._onResetStart = null; 1.2011 + } 1.2012 + 1.2013 + this.cancelMouseEvents(); 1.2014 + this.cancelKeyEvents(); 1.2015 + this._cancelReset(); 1.2016 + 1.2017 + this._save(); 1.2018 + this._resetFinalTranslation = vec3.create(aFinalTranslation); 1.2019 + this._resetFinalRotation = quat4.create(aFinalRotation); 1.2020 + this._resetInProgress = true; 1.2021 + }, 1.2022 + 1.2023 + /** 1.2024 + * Cancels the current arcball reset animation if there is one. 1.2025 + */ 1.2026 + _cancelReset: function TVA__cancelReset() 1.2027 + { 1.2028 + if (this._resetInProgress) { 1.2029 + this._resetInProgress = false; 1.2030 + this._save(); 1.2031 + 1.2032 + if ("function" === typeof this._onResetFinish) { 1.2033 + this._onResetFinish(); 1.2034 + this._onResetFinish = null; 1.2035 + this._onResetStep = null; 1.2036 + } 1.2037 + } 1.2038 + }, 1.2039 + 1.2040 + /** 1.2041 + * Executes the next step in the arcball reset animation. 1.2042 + * 1.2043 + * @param {Number} aDelta 1.2044 + * the current animation frame delta 1.2045 + */ 1.2046 + _nextResetStep: function TVA__nextResetStep(aDelta) 1.2047 + { 1.2048 + // a very large animation frame delta (in case of seriously low framerate) 1.2049 + // would cause all the interpolations to become highly unstable 1.2050 + aDelta = TiltMath.clamp(aDelta, 1, 100); 1.2051 + 1.2052 + let fNearZero = EPSILON * EPSILON; 1.2053 + let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta; 1.2054 + let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR; 1.2055 + let fTran = this._resetFinalTranslation; 1.2056 + let fRot = this._resetFinalRotation; 1.2057 + 1.2058 + let t = vec3.create(fTran); 1.2059 + let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot); 1.2060 + 1.2061 + // reset the rotation quaternion and translation vector 1.2062 + vec3.lerp(this._currentTrans, t, fInterpLin); 1.2063 + quat4.slerp(this._currentRot, r, fInterpSph); 1.2064 + 1.2065 + // also reset any additional transforms by the keyboard or mouse 1.2066 + vec3.scale(this._additionalTrans, fInterpLin); 1.2067 + vec3.scale(this._additionalRot, fInterpLin); 1.2068 + this._zoomAmount *= fInterpLin; 1.2069 + 1.2070 + // clear the loop if the all values are very close to zero 1.2071 + if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero && 1.2072 + vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero && 1.2073 + vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero && 1.2074 + vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero && 1.2075 + vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero && 1.2076 + vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero && 1.2077 + vec3.length(this._additionalRot) < fNearZero && 1.2078 + vec3.length(this._additionalTrans) < fNearZero) { 1.2079 + 1.2080 + this._cancelReset(); 1.2081 + } 1.2082 + 1.2083 + if ("function" === typeof this._onResetStep) { 1.2084 + this._onResetStep(); 1.2085 + } 1.2086 + }, 1.2087 + 1.2088 + /** 1.2089 + * Loads the keys to control this arcball. 1.2090 + */ 1.2091 + _loadKeys: function TVA__loadKeys() 1.2092 + { 1.2093 + this.rotateKeys = { 1.2094 + "up": Ci.nsIDOMKeyEvent["DOM_VK_W"], 1.2095 + "down": Ci.nsIDOMKeyEvent["DOM_VK_S"], 1.2096 + "left": Ci.nsIDOMKeyEvent["DOM_VK_A"], 1.2097 + "right": Ci.nsIDOMKeyEvent["DOM_VK_D"], 1.2098 + }; 1.2099 + this.panKeys = { 1.2100 + "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"], 1.2101 + "down": Ci.nsIDOMKeyEvent["DOM_VK_DOWN"], 1.2102 + "left": Ci.nsIDOMKeyEvent["DOM_VK_LEFT"], 1.2103 + "right": Ci.nsIDOMKeyEvent["DOM_VK_RIGHT"], 1.2104 + }; 1.2105 + this.zoomKeys = { 1.2106 + "in": [ 1.2107 + Ci.nsIDOMKeyEvent["DOM_VK_I"], 1.2108 + Ci.nsIDOMKeyEvent["DOM_VK_ADD"], 1.2109 + Ci.nsIDOMKeyEvent["DOM_VK_EQUALS"], 1.2110 + ], 1.2111 + "out": [ 1.2112 + Ci.nsIDOMKeyEvent["DOM_VK_O"], 1.2113 + Ci.nsIDOMKeyEvent["DOM_VK_SUBTRACT"], 1.2114 + ], 1.2115 + "unzoom": Ci.nsIDOMKeyEvent["DOM_VK_0"] 1.2116 + }; 1.2117 + this.resetKey = Ci.nsIDOMKeyEvent["DOM_VK_R"]; 1.2118 + }, 1.2119 + 1.2120 + /** 1.2121 + * Saves the current arcball state, typically after resize or mouse events. 1.2122 + */ 1.2123 + _save: function TVA__save() 1.2124 + { 1.2125 + if (this._mousePress) { 1.2126 + let x = this._mousePress[0]; 1.2127 + let y = this._mousePress[1]; 1.2128 + 1.2129 + this._mouseMove[0] = x; 1.2130 + this._mouseMove[1] = y; 1.2131 + this._mouseRelease[0] = x; 1.2132 + this._mouseRelease[1] = y; 1.2133 + this._mouseLerp[0] = x; 1.2134 + this._mouseLerp[1] = y; 1.2135 + } 1.2136 + }, 1.2137 + 1.2138 + /** 1.2139 + * Function called when this object is destroyed. 1.2140 + */ 1.2141 + _finalize: function TVA__finalize() 1.2142 + { 1.2143 + this._cancelReset(); 1.2144 + } 1.2145 +}; 1.2146 + 1.2147 +/** 1.2148 + * Tilt configuration preferences. 1.2149 + */ 1.2150 +TiltVisualizer.Prefs = { 1.2151 + 1.2152 + /** 1.2153 + * Specifies if Tilt is enabled or not. 1.2154 + */ 1.2155 + get enabled() 1.2156 + { 1.2157 + return this._enabled; 1.2158 + }, 1.2159 + 1.2160 + set enabled(value) 1.2161 + { 1.2162 + TiltUtils.Preferences.set("enabled", "boolean", value); 1.2163 + this._enabled = value; 1.2164 + }, 1.2165 + 1.2166 + get introTransition() 1.2167 + { 1.2168 + return this._introTransition; 1.2169 + }, 1.2170 + 1.2171 + set introTransition(value) 1.2172 + { 1.2173 + TiltUtils.Preferences.set("intro_transition", "boolean", value); 1.2174 + this._introTransition = value; 1.2175 + }, 1.2176 + 1.2177 + get outroTransition() 1.2178 + { 1.2179 + return this._outroTransition; 1.2180 + }, 1.2181 + 1.2182 + set outroTransition(value) 1.2183 + { 1.2184 + TiltUtils.Preferences.set("outro_transition", "boolean", value); 1.2185 + this._outroTransition = value; 1.2186 + }, 1.2187 + 1.2188 + /** 1.2189 + * Loads the preferences. 1.2190 + */ 1.2191 + load: function TVC_load() 1.2192 + { 1.2193 + let prefs = TiltVisualizer.Prefs; 1.2194 + let get = TiltUtils.Preferences.get; 1.2195 + 1.2196 + prefs._enabled = get("enabled", "boolean"); 1.2197 + prefs._introTransition = get("intro_transition", "boolean"); 1.2198 + prefs._outroTransition = get("outro_transition", "boolean"); 1.2199 + } 1.2200 +}; 1.2201 + 1.2202 +/** 1.2203 + * A custom visualization shader. 1.2204 + * 1.2205 + * @param {Attribute} vertexPosition: the vertex position 1.2206 + * @param {Attribute} vertexTexCoord: texture coordinates used by the sampler 1.2207 + * @param {Attribute} vertexColor: specific [r, g, b] color for each vertex 1.2208 + * @param {Uniform} mvMatrix: the model view matrix 1.2209 + * @param {Uniform} projMatrix: the projection matrix 1.2210 + * @param {Uniform} sampler: the texture sampler to fetch the pixels from 1.2211 + */ 1.2212 +TiltVisualizer.MeshShader = { 1.2213 + 1.2214 + /** 1.2215 + * Vertex shader. 1.2216 + */ 1.2217 + vs: [ 1.2218 + "attribute vec3 vertexPosition;", 1.2219 + "attribute vec2 vertexTexCoord;", 1.2220 + "attribute vec3 vertexColor;", 1.2221 + 1.2222 + "uniform mat4 mvMatrix;", 1.2223 + "uniform mat4 projMatrix;", 1.2224 + 1.2225 + "varying vec2 texCoord;", 1.2226 + "varying vec3 color;", 1.2227 + 1.2228 + "void main() {", 1.2229 + " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);", 1.2230 + " texCoord = vertexTexCoord;", 1.2231 + " color = vertexColor;", 1.2232 + "}" 1.2233 + ].join("\n"), 1.2234 + 1.2235 + /** 1.2236 + * Fragment shader. 1.2237 + */ 1.2238 + fs: [ 1.2239 + "#ifdef GL_ES", 1.2240 + "precision lowp float;", 1.2241 + "#endif", 1.2242 + 1.2243 + "uniform sampler2D sampler;", 1.2244 + 1.2245 + "varying vec2 texCoord;", 1.2246 + "varying vec3 color;", 1.2247 + 1.2248 + "void main() {", 1.2249 + " if (texCoord.x < 0.0) {", 1.2250 + " gl_FragColor = vec4(color, 1.0);", 1.2251 + " } else {", 1.2252 + " gl_FragColor = vec4(texture2D(sampler, texCoord).rgb, 1.0);", 1.2253 + " }", 1.2254 + "}" 1.2255 + ].join("\n") 1.2256 +};