|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 "use strict"; |
|
7 |
|
8 const {Cu, Ci, ChromeWorker} = require("chrome"); |
|
9 |
|
10 let TiltGL = require("devtools/tilt/tilt-gl"); |
|
11 let TiltUtils = require("devtools/tilt/tilt-utils"); |
|
12 let TiltVisualizerStyle = require("devtools/tilt/tilt-visualizer-style"); |
|
13 let {EPSILON, TiltMath, vec3, mat4, quat4} = require("devtools/tilt/tilt-math"); |
|
14 let {TargetFactory} = require("devtools/framework/target"); |
|
15 |
|
16 Cu.import("resource://gre/modules/Services.jsm"); |
|
17 Cu.import("resource:///modules/devtools/gDevTools.jsm"); |
|
18 |
|
19 const ELEMENT_MIN_SIZE = 4; |
|
20 const INVISIBLE_ELEMENTS = { |
|
21 "head": true, |
|
22 "base": true, |
|
23 "basefont": true, |
|
24 "isindex": true, |
|
25 "link": true, |
|
26 "meta": true, |
|
27 "option": true, |
|
28 "script": true, |
|
29 "style": true, |
|
30 "title": true |
|
31 }; |
|
32 |
|
33 // a node is represented in the visualization mesh as a rectangular stack |
|
34 // of 5 quads composed of 12 vertices; we draw these as triangles using an |
|
35 // index buffer of 12 unsigned int elements, obviously one for each vertex; |
|
36 // if a webpage has enough nodes to overflow the index buffer elements size, |
|
37 // weird things may happen; thus, when necessary, we'll split into groups |
|
38 const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1; |
|
39 |
|
40 const WIREFRAME_COLOR = [0, 0, 0, 0.25]; |
|
41 const INTRO_TRANSITION_DURATION = 1000; |
|
42 const OUTRO_TRANSITION_DURATION = 800; |
|
43 const INITIAL_Z_TRANSLATION = 400; |
|
44 const MOVE_INTO_VIEW_ACCURACY = 50; |
|
45 |
|
46 const MOUSE_CLICK_THRESHOLD = 10; |
|
47 const MOUSE_INTRO_DELAY = 200; |
|
48 const ARCBALL_SENSITIVITY = 0.5; |
|
49 const ARCBALL_ROTATION_STEP = 0.15; |
|
50 const ARCBALL_TRANSLATION_STEP = 35; |
|
51 const ARCBALL_ZOOM_STEP = 0.1; |
|
52 const ARCBALL_ZOOM_MIN = -3000; |
|
53 const ARCBALL_ZOOM_MAX = 500; |
|
54 const ARCBALL_RESET_SPHERICAL_FACTOR = 0.1; |
|
55 const ARCBALL_RESET_LINEAR_FACTOR = 0.01; |
|
56 |
|
57 const TILT_CRAFTER = "resource:///modules/devtools/tilt/TiltWorkerCrafter.js"; |
|
58 const TILT_PICKER = "resource:///modules/devtools/tilt/TiltWorkerPicker.js"; |
|
59 |
|
60 |
|
61 /** |
|
62 * Initializes the visualization presenter and controller. |
|
63 * |
|
64 * @param {Object} aProperties |
|
65 * an object containing the following properties: |
|
66 * {Window} chromeWindow: a reference to the top level window |
|
67 * {Window} contentWindow: the content window holding the visualized doc |
|
68 * {Element} parentNode: the parent node to hold the visualization |
|
69 * {Object} notifications: necessary notifications for Tilt |
|
70 * {Function} onError: optional, function called if initialization failed |
|
71 * {Function} onLoad: optional, function called if initialization worked |
|
72 */ |
|
73 function TiltVisualizer(aProperties) |
|
74 { |
|
75 // make sure the properties parameter is a valid object |
|
76 aProperties = aProperties || {}; |
|
77 |
|
78 /** |
|
79 * Save a reference to the top-level window. |
|
80 */ |
|
81 this.chromeWindow = aProperties.chromeWindow; |
|
82 this.tab = aProperties.tab; |
|
83 |
|
84 /** |
|
85 * The canvas element used for rendering the visualization. |
|
86 */ |
|
87 this.canvas = TiltUtils.DOM.initCanvas(aProperties.parentNode, { |
|
88 focusable: true, |
|
89 append: true |
|
90 }); |
|
91 |
|
92 /** |
|
93 * Visualization logic and drawing loop. |
|
94 */ |
|
95 this.presenter = new TiltVisualizer.Presenter(this.canvas, |
|
96 aProperties.chromeWindow, |
|
97 aProperties.contentWindow, |
|
98 aProperties.notifications, |
|
99 aProperties.onError || null, |
|
100 aProperties.onLoad || null); |
|
101 |
|
102 /** |
|
103 * Visualization mouse and keyboard controller. |
|
104 */ |
|
105 this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter); |
|
106 } |
|
107 |
|
108 exports.TiltVisualizer = TiltVisualizer; |
|
109 |
|
110 TiltVisualizer.prototype = { |
|
111 |
|
112 /** |
|
113 * Initializes the visualizer. |
|
114 */ |
|
115 init: function TV_init() |
|
116 { |
|
117 this.presenter.init(); |
|
118 this.bindToInspector(this.tab); |
|
119 }, |
|
120 |
|
121 /** |
|
122 * Checks if this object was initialized properly. |
|
123 * |
|
124 * @return {Boolean} true if the object was initialized properly |
|
125 */ |
|
126 isInitialized: function TV_isInitialized() |
|
127 { |
|
128 return this.presenter && this.presenter.isInitialized() && |
|
129 this.controller && this.controller.isInitialized(); |
|
130 }, |
|
131 |
|
132 /** |
|
133 * Removes the overlay canvas used for rendering the visualization. |
|
134 */ |
|
135 removeOverlay: function TV_removeOverlay() |
|
136 { |
|
137 if (this.canvas && this.canvas.parentNode) { |
|
138 this.canvas.parentNode.removeChild(this.canvas); |
|
139 } |
|
140 }, |
|
141 |
|
142 /** |
|
143 * Explicitly cleans up this visualizer and sets everything to null. |
|
144 */ |
|
145 cleanup: function TV_cleanup() |
|
146 { |
|
147 this.unbindInspector(); |
|
148 |
|
149 if (this.controller) { |
|
150 TiltUtils.destroyObject(this.controller); |
|
151 } |
|
152 if (this.presenter) { |
|
153 TiltUtils.destroyObject(this.presenter); |
|
154 } |
|
155 |
|
156 let chromeWindow = this.chromeWindow; |
|
157 |
|
158 TiltUtils.destroyObject(this); |
|
159 TiltUtils.clearCache(); |
|
160 TiltUtils.gc(chromeWindow); |
|
161 }, |
|
162 |
|
163 /** |
|
164 * Listen to the inspector activity. |
|
165 */ |
|
166 bindToInspector: function TV_bindToInspector(aTab) |
|
167 { |
|
168 this._browserTab = aTab; |
|
169 |
|
170 this.onNewNodeFromInspector = this.onNewNodeFromInspector.bind(this); |
|
171 this.onNewNodeFromTilt = this.onNewNodeFromTilt.bind(this); |
|
172 this.onInspectorReady = this.onInspectorReady.bind(this); |
|
173 this.onToolboxDestroyed = this.onToolboxDestroyed.bind(this); |
|
174 |
|
175 gDevTools.on("inspector-ready", this.onInspectorReady); |
|
176 gDevTools.on("toolbox-destroyed", this.onToolboxDestroyed); |
|
177 |
|
178 Services.obs.addObserver(this.onNewNodeFromTilt, |
|
179 this.presenter.NOTIFICATIONS.HIGHLIGHTING, |
|
180 false); |
|
181 Services.obs.addObserver(this.onNewNodeFromTilt, |
|
182 this.presenter.NOTIFICATIONS.UNHIGHLIGHTING, |
|
183 false); |
|
184 |
|
185 let target = TargetFactory.forTab(aTab); |
|
186 let toolbox = gDevTools.getToolbox(target); |
|
187 if (toolbox) { |
|
188 let panel = toolbox.getPanel("inspector"); |
|
189 if (panel) { |
|
190 this.inspector = panel; |
|
191 this.inspector.selection.on("new-node", this.onNewNodeFromInspector); |
|
192 this.onNewNodeFromInspector(); |
|
193 } |
|
194 } |
|
195 }, |
|
196 |
|
197 /** |
|
198 * Unregister inspector event listeners. |
|
199 */ |
|
200 unbindInspector: function TV_unbindInspector() |
|
201 { |
|
202 this._browserTab = null; |
|
203 |
|
204 if (this.inspector) { |
|
205 if (this.inspector.selection) { |
|
206 this.inspector.selection.off("new-node", this.onNewNodeFromInspector); |
|
207 } |
|
208 this.inspector = null; |
|
209 } |
|
210 |
|
211 gDevTools.off("inspector-ready", this.onInspectorReady); |
|
212 gDevTools.off("toolbox-destroyed", this.onToolboxDestroyed); |
|
213 |
|
214 Services.obs.removeObserver(this.onNewNodeFromTilt, |
|
215 this.presenter.NOTIFICATIONS.HIGHLIGHTING); |
|
216 Services.obs.removeObserver(this.onNewNodeFromTilt, |
|
217 this.presenter.NOTIFICATIONS.UNHIGHLIGHTING); |
|
218 }, |
|
219 |
|
220 /** |
|
221 * When a new inspector is started. |
|
222 */ |
|
223 onInspectorReady: function TV_onInspectorReady(event, toolbox, panel) |
|
224 { |
|
225 if (toolbox.target.tab === this._browserTab) { |
|
226 this.inspector = panel; |
|
227 this.inspector.selection.on("new-node", this.onNewNodeFromInspector); |
|
228 this.onNewNodeFromTilt(); |
|
229 } |
|
230 }, |
|
231 |
|
232 /** |
|
233 * When the toolbox, therefor the inspector, is closed. |
|
234 */ |
|
235 onToolboxDestroyed: function TV_onToolboxDestroyed(event, tab) |
|
236 { |
|
237 if (tab === this._browserTab && |
|
238 this.inspector) { |
|
239 if (this.inspector.selection) { |
|
240 this.inspector.selection.off("new-node", this.onNewNodeFromInspector); |
|
241 } |
|
242 this.inspector = null; |
|
243 } |
|
244 }, |
|
245 |
|
246 /** |
|
247 * When a new node is selected in the inspector. |
|
248 */ |
|
249 onNewNodeFromInspector: function TV_onNewNodeFromInspector() |
|
250 { |
|
251 if (this.inspector && |
|
252 this.inspector.selection.reason != "tilt") { |
|
253 let selection = this.inspector.selection; |
|
254 let canHighlightNode = selection.isNode() && |
|
255 selection.isConnected() && |
|
256 selection.isElementNode(); |
|
257 if (canHighlightNode) { |
|
258 this.presenter.highlightNode(selection.node); |
|
259 } else { |
|
260 this.presenter.highlightNodeFor(-1); |
|
261 } |
|
262 } |
|
263 }, |
|
264 |
|
265 /** |
|
266 * When a new node is selected in Tilt. |
|
267 */ |
|
268 onNewNodeFromTilt: function TV_onNewNodeFromTilt() |
|
269 { |
|
270 if (!this.inspector) { |
|
271 return; |
|
272 } |
|
273 let nodeIndex = this.presenter._currentSelection; |
|
274 if (nodeIndex < 0) { |
|
275 this.inspector.selection.setNodeFront(null, "tilt"); |
|
276 } |
|
277 let node = this.presenter._traverseData.nodes[nodeIndex]; |
|
278 node = this.inspector.walker.frontForRawNode(node); |
|
279 this.inspector.selection.setNodeFront(node, "tilt"); |
|
280 }, |
|
281 }; |
|
282 |
|
283 /** |
|
284 * This object manages the visualization logic and drawing loop. |
|
285 * |
|
286 * @param {HTMLCanvasElement} aCanvas |
|
287 * the canvas element used for rendering |
|
288 * @param {Window} aChromeWindow |
|
289 * a reference to the top-level window |
|
290 * @param {Window} aContentWindow |
|
291 * the content window holding the document to be visualized |
|
292 * @param {Object} aNotifications |
|
293 * necessary notifications for Tilt |
|
294 * @param {Function} onError |
|
295 * function called if initialization failed |
|
296 * @param {Function} onLoad |
|
297 * function called if initialization worked |
|
298 */ |
|
299 TiltVisualizer.Presenter = function TV_Presenter( |
|
300 aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad) |
|
301 { |
|
302 /** |
|
303 * A canvas overlay used for drawing the visualization. |
|
304 */ |
|
305 this.canvas = aCanvas; |
|
306 |
|
307 /** |
|
308 * Save a reference to the top-level window, to access Tilt. |
|
309 */ |
|
310 this.chromeWindow = aChromeWindow; |
|
311 |
|
312 /** |
|
313 * The content window generating the visualization |
|
314 */ |
|
315 this.contentWindow = aContentWindow; |
|
316 |
|
317 /** |
|
318 * Shortcut for accessing notifications strings. |
|
319 */ |
|
320 this.NOTIFICATIONS = aNotifications; |
|
321 |
|
322 /** |
|
323 * Use the default node callback function |
|
324 */ |
|
325 this.nodeCallback = null; |
|
326 |
|
327 /** |
|
328 * Create the renderer, containing useful functions for easy drawing. |
|
329 */ |
|
330 this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad); |
|
331 |
|
332 /** |
|
333 * A custom shader used for drawing the visualization mesh. |
|
334 */ |
|
335 this._visualizationProgram = null; |
|
336 |
|
337 /** |
|
338 * The combined mesh representing the document visualization. |
|
339 */ |
|
340 this._texture = null; |
|
341 this._meshData = null; |
|
342 this._meshStacks = null; |
|
343 this._meshWireframe = null; |
|
344 this._traverseData = null; |
|
345 |
|
346 /** |
|
347 * A highlight quad drawn over a stacked dom node. |
|
348 */ |
|
349 this._highlight = { |
|
350 disabled: true, |
|
351 v0: vec3.create(), |
|
352 v1: vec3.create(), |
|
353 v2: vec3.create(), |
|
354 v3: vec3.create() |
|
355 }; |
|
356 |
|
357 /** |
|
358 * Scene transformations, exposing offset, translation and rotation. |
|
359 * Modified by events in the controller through delegate functions. |
|
360 */ |
|
361 this.transforms = { |
|
362 zoom: 1, |
|
363 offset: vec3.create(), // mesh offset, aligned to the viewport center |
|
364 translation: vec3.create(), // scene translation, on the [x, y, z] axis |
|
365 rotation: quat4.create() // scene rotation, expressed as a quaternion |
|
366 }; |
|
367 |
|
368 /** |
|
369 * Variables holding information about the initial and current node selected. |
|
370 */ |
|
371 this._currentSelection = -1; // the selected node index |
|
372 this._initialMeshConfiguration = false; // true if the 3D mesh was configured |
|
373 |
|
374 /** |
|
375 * Variable specifying if the scene should be redrawn. |
|
376 * This should happen usually when the visualization is translated/rotated. |
|
377 */ |
|
378 this._redraw = true; |
|
379 |
|
380 /** |
|
381 * Total time passed since the rendering started. |
|
382 * If the rendering is paused, this property won't get updated. |
|
383 */ |
|
384 this._time = 0; |
|
385 |
|
386 /** |
|
387 * Frame delta time (the ammount of time passed for each frame). |
|
388 * This is used to smoothly interpolate animation transfroms. |
|
389 */ |
|
390 this._delta = 0; |
|
391 this._prevFrameTime = 0; |
|
392 this._currFrameTime = 0; |
|
393 }; |
|
394 |
|
395 TiltVisualizer.Presenter.prototype = { |
|
396 |
|
397 /** |
|
398 * Initializes the presenter and starts the animation loop |
|
399 */ |
|
400 init: function TVP_init() |
|
401 { |
|
402 this._setup(); |
|
403 this._loop(); |
|
404 }, |
|
405 |
|
406 /** |
|
407 * The initialization logic. |
|
408 */ |
|
409 _setup: function TVP__setup() |
|
410 { |
|
411 let renderer = this._renderer; |
|
412 |
|
413 // if the renderer was destroyed, don't continue setup |
|
414 if (!renderer || !renderer.context) { |
|
415 return; |
|
416 } |
|
417 |
|
418 // create the visualization shaders and program to draw the stacks mesh |
|
419 this._visualizationProgram = new renderer.Program({ |
|
420 vs: TiltVisualizer.MeshShader.vs, |
|
421 fs: TiltVisualizer.MeshShader.fs, |
|
422 attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"], |
|
423 uniforms: ["mvMatrix", "projMatrix", "sampler"] |
|
424 }); |
|
425 |
|
426 // get the document zoom to properly scale the visualization |
|
427 this.transforms.zoom = this._getPageZoom(); |
|
428 |
|
429 // bind the owner object to the necessary functions |
|
430 TiltUtils.bindObjectFunc(this, "^_on"); |
|
431 TiltUtils.bindObjectFunc(this, "_loop"); |
|
432 |
|
433 this._setupTexture(); |
|
434 this._setupMeshData(); |
|
435 this._setupEventListeners(); |
|
436 this.canvas.focus(); |
|
437 }, |
|
438 |
|
439 /** |
|
440 * Get page zoom factor. |
|
441 * @return {Number} |
|
442 */ |
|
443 _getPageZoom: function TVP__getPageZoom() { |
|
444 return this.contentWindow |
|
445 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
446 .getInterface(Ci.nsIDOMWindowUtils) |
|
447 .fullZoom; |
|
448 }, |
|
449 |
|
450 /** |
|
451 * The animation logic. |
|
452 */ |
|
453 _loop: function TVP__loop() |
|
454 { |
|
455 let renderer = this._renderer; |
|
456 |
|
457 // if the renderer was destroyed, don't continue rendering |
|
458 if (!renderer || !renderer.context) { |
|
459 return; |
|
460 } |
|
461 |
|
462 // prepare for the next frame of the animation loop |
|
463 this.chromeWindow.mozRequestAnimationFrame(this._loop); |
|
464 |
|
465 // only redraw if we really have to |
|
466 if (this._redraw) { |
|
467 this._redraw = false; |
|
468 this._drawVisualization(); |
|
469 } |
|
470 |
|
471 // update the current presenter transfroms from the controller |
|
472 if ("function" === typeof this._controllerUpdate) { |
|
473 this._controllerUpdate(this._time, this._delta); |
|
474 } |
|
475 |
|
476 this._handleFrameDelta(); |
|
477 this._handleKeyframeNotifications(); |
|
478 }, |
|
479 |
|
480 /** |
|
481 * Calculates the current frame delta time. |
|
482 */ |
|
483 _handleFrameDelta: function TVP__handleFrameDelta() |
|
484 { |
|
485 this._prevFrameTime = this._currFrameTime; |
|
486 this._currFrameTime = this.chromeWindow.mozAnimationStartTime; |
|
487 this._delta = this._currFrameTime - this._prevFrameTime; |
|
488 }, |
|
489 |
|
490 /** |
|
491 * Draws the visualization mesh and highlight quad. |
|
492 */ |
|
493 _drawVisualization: function TVP__drawVisualization() |
|
494 { |
|
495 let renderer = this._renderer; |
|
496 let transforms = this.transforms; |
|
497 let w = renderer.width; |
|
498 let h = renderer.height; |
|
499 let ih = renderer.initialHeight; |
|
500 |
|
501 // if the mesh wasn't created yet, don't continue rendering |
|
502 if (!this._meshStacks || !this._meshWireframe) { |
|
503 return; |
|
504 } |
|
505 |
|
506 // clear the context to an opaque black background |
|
507 renderer.clear(); |
|
508 renderer.perspective(); |
|
509 |
|
510 // apply a transition transformation using an ortho and perspective matrix |
|
511 let ortho = mat4.ortho(0, w, h, 0, -1000, 1000); |
|
512 |
|
513 if (!this._isExecutingDestruction) { |
|
514 let f = this._time / INTRO_TRANSITION_DURATION; |
|
515 renderer.lerp(renderer.projMatrix, ortho, f, 8); |
|
516 } else { |
|
517 let f = this._time / OUTRO_TRANSITION_DURATION; |
|
518 renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8); |
|
519 } |
|
520 |
|
521 // apply the preliminary transformations to the model view |
|
522 renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION); |
|
523 |
|
524 // calculate the camera matrix using the rotation and translation |
|
525 renderer.translate(transforms.translation[0], 0, |
|
526 transforms.translation[2]); |
|
527 |
|
528 renderer.transform(quat4.toMat4(transforms.rotation)); |
|
529 |
|
530 // offset the visualization mesh to center |
|
531 renderer.translate(transforms.offset[0], |
|
532 transforms.offset[1] + transforms.translation[1], 0); |
|
533 |
|
534 renderer.scale(transforms.zoom, transforms.zoom); |
|
535 |
|
536 // draw the visualization mesh |
|
537 renderer.strokeWeight(2); |
|
538 renderer.depthTest(true); |
|
539 this._drawMeshStacks(); |
|
540 this._drawMeshWireframe(); |
|
541 this._drawHighlight(); |
|
542 |
|
543 // make sure the initial transition is drawn until finished |
|
544 if (this._time < INTRO_TRANSITION_DURATION || |
|
545 this._time < OUTRO_TRANSITION_DURATION) { |
|
546 this._redraw = true; |
|
547 } |
|
548 this._time += this._delta; |
|
549 }, |
|
550 |
|
551 /** |
|
552 * Draws the meshStacks object. |
|
553 */ |
|
554 _drawMeshStacks: function TVP__drawMeshStacks() |
|
555 { |
|
556 let renderer = this._renderer; |
|
557 let mesh = this._meshStacks; |
|
558 |
|
559 let visualizationProgram = this._visualizationProgram; |
|
560 let texture = this._texture; |
|
561 let mvMatrix = renderer.mvMatrix; |
|
562 let projMatrix = renderer.projMatrix; |
|
563 |
|
564 // use the necessary shader |
|
565 visualizationProgram.use(); |
|
566 |
|
567 for (let i = 0, len = mesh.length; i < len; i++) { |
|
568 let group = mesh[i]; |
|
569 |
|
570 // bind the attributes and uniforms as necessary |
|
571 visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices); |
|
572 visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord); |
|
573 visualizationProgram.bindVertexBuffer("vertexColor", group.color); |
|
574 |
|
575 visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix); |
|
576 visualizationProgram.bindUniformMatrix("projMatrix", projMatrix); |
|
577 visualizationProgram.bindTexture("sampler", texture); |
|
578 |
|
579 // draw the vertices as TRIANGLES indexed elements |
|
580 renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices); |
|
581 } |
|
582 |
|
583 // save the current model view and projection matrices |
|
584 mesh.mvMatrix = mat4.create(mvMatrix); |
|
585 mesh.projMatrix = mat4.create(projMatrix); |
|
586 }, |
|
587 |
|
588 /** |
|
589 * Draws the meshWireframe object. |
|
590 */ |
|
591 _drawMeshWireframe: function TVP__drawMeshWireframe() |
|
592 { |
|
593 let renderer = this._renderer; |
|
594 let mesh = this._meshWireframe; |
|
595 |
|
596 for (let i = 0, len = mesh.length; i < len; i++) { |
|
597 let group = mesh[i]; |
|
598 |
|
599 // use the necessary shader |
|
600 renderer.useColorShader(group.vertices, WIREFRAME_COLOR); |
|
601 |
|
602 // draw the vertices as LINES indexed elements |
|
603 renderer.drawIndexedVertices(renderer.context.LINES, group.indices); |
|
604 } |
|
605 }, |
|
606 |
|
607 /** |
|
608 * Draws a highlighted quad around a currently selected node. |
|
609 */ |
|
610 _drawHighlight: function TVP__drawHighlight() |
|
611 { |
|
612 // check if there's anything to highlight (i.e any node is selected) |
|
613 if (!this._highlight.disabled) { |
|
614 |
|
615 // set the corresponding state to draw the highlight quad |
|
616 let renderer = this._renderer; |
|
617 let highlight = this._highlight; |
|
618 |
|
619 renderer.depthTest(false); |
|
620 renderer.fill(highlight.fill, 0.5); |
|
621 renderer.stroke(highlight.stroke); |
|
622 renderer.strokeWeight(highlight.strokeWeight); |
|
623 renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3); |
|
624 } |
|
625 }, |
|
626 |
|
627 /** |
|
628 * Creates or refreshes the texture applied to the visualization mesh. |
|
629 */ |
|
630 _setupTexture: function TVP__setupTexture() |
|
631 { |
|
632 let renderer = this._renderer; |
|
633 |
|
634 // destroy any previously created texture |
|
635 TiltUtils.destroyObject(this._texture); this._texture = null; |
|
636 |
|
637 // if the renderer was destroyed, don't continue setup |
|
638 if (!renderer || !renderer.context) { |
|
639 return; |
|
640 } |
|
641 |
|
642 // get the maximum texture size |
|
643 this._maxTextureSize = |
|
644 renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE); |
|
645 |
|
646 // use a simple shim to get the image representation of the document |
|
647 // this will be removed once the MOZ_window_region_texture bug #653656 |
|
648 // is finished; currently just converting the document image to a texture |
|
649 // applied to the mesh |
|
650 this._texture = new renderer.Texture({ |
|
651 source: TiltGL.TextureUtils.createContentImage(this.contentWindow, |
|
652 this._maxTextureSize), |
|
653 format: "RGB" |
|
654 }); |
|
655 |
|
656 if ("function" === typeof this._onSetupTexture) { |
|
657 this._onSetupTexture(); |
|
658 this._onSetupTexture = null; |
|
659 } |
|
660 }, |
|
661 |
|
662 /** |
|
663 * Create the combined mesh representing the document visualization by |
|
664 * traversing the document & adding a stack for each node that is drawable. |
|
665 * |
|
666 * @param {Object} aMeshData |
|
667 * object containing the necessary mesh verts, texcoord etc. |
|
668 */ |
|
669 _setupMesh: function TVP__setupMesh(aMeshData) |
|
670 { |
|
671 let renderer = this._renderer; |
|
672 |
|
673 // destroy any previously created mesh |
|
674 TiltUtils.destroyObject(this._meshStacks); this._meshStacks = []; |
|
675 TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = []; |
|
676 |
|
677 // if the renderer was destroyed, don't continue setup |
|
678 if (!renderer || !renderer.context) { |
|
679 return; |
|
680 } |
|
681 |
|
682 // save the mesh data for future use |
|
683 this._meshData = aMeshData; |
|
684 |
|
685 // create a sub-mesh for each group in the mesh data |
|
686 for (let i = 0, len = aMeshData.groups.length; i < len; i++) { |
|
687 let group = aMeshData.groups[i]; |
|
688 |
|
689 // create the visualization mesh using the vertices, texture coordinates |
|
690 // and indices computed when traversing the document object model |
|
691 this._meshStacks.push({ |
|
692 vertices: new renderer.VertexBuffer(group.vertices, 3), |
|
693 texCoord: new renderer.VertexBuffer(group.texCoord, 2), |
|
694 color: new renderer.VertexBuffer(group.color, 3), |
|
695 indices: new renderer.IndexBuffer(group.stacksIndices) |
|
696 }); |
|
697 |
|
698 // additionally, create a wireframe representation to make the |
|
699 // visualization a bit more pretty |
|
700 this._meshWireframe.push({ |
|
701 vertices: this._meshStacks[i].vertices, |
|
702 indices: new renderer.IndexBuffer(group.wireframeIndices) |
|
703 }); |
|
704 } |
|
705 |
|
706 // configure the required mesh transformations and background only once |
|
707 if (!this._initialMeshConfiguration) { |
|
708 this._initialMeshConfiguration = true; |
|
709 |
|
710 // set the necessary mesh offsets |
|
711 this.transforms.offset[0] = -renderer.width * 0.5; |
|
712 this.transforms.offset[1] = -renderer.height * 0.5; |
|
713 |
|
714 // make sure the canvas is opaque now that the initialization is finished |
|
715 this.canvas.style.background = TiltVisualizerStyle.canvas.background; |
|
716 |
|
717 this._drawVisualization(); |
|
718 this._redraw = true; |
|
719 } |
|
720 |
|
721 if ("function" === typeof this._onSetupMesh) { |
|
722 this._onSetupMesh(); |
|
723 this._onSetupMesh = null; |
|
724 } |
|
725 }, |
|
726 |
|
727 /** |
|
728 * Computes the mesh vertices, texture coordinates etc. by groups of nodes. |
|
729 */ |
|
730 _setupMeshData: function TVP__setupMeshData() |
|
731 { |
|
732 let renderer = this._renderer; |
|
733 |
|
734 // if the renderer was destroyed, don't continue setup |
|
735 if (!renderer || !renderer.context) { |
|
736 return; |
|
737 } |
|
738 |
|
739 // traverse the document and get the depths, coordinates and local names |
|
740 this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, { |
|
741 nodeCallback: this.nodeCallback, |
|
742 invisibleElements: INVISIBLE_ELEMENTS, |
|
743 minSize: ELEMENT_MIN_SIZE, |
|
744 maxX: this._texture.width, |
|
745 maxY: this._texture.height |
|
746 }); |
|
747 |
|
748 let worker = new ChromeWorker(TILT_CRAFTER); |
|
749 |
|
750 worker.addEventListener("message", function TVP_onMessage(event) { |
|
751 this._setupMesh(event.data); |
|
752 }.bind(this), false); |
|
753 |
|
754 // calculate necessary information regarding vertices, texture coordinates |
|
755 // etc. in a separate thread, as this process may take a while |
|
756 worker.postMessage({ |
|
757 maxGroupNodes: MAX_GROUP_NODES, |
|
758 style: TiltVisualizerStyle.nodes, |
|
759 texWidth: this._texture.width, |
|
760 texHeight: this._texture.height, |
|
761 nodesInfo: this._traverseData.info |
|
762 }); |
|
763 }, |
|
764 |
|
765 /** |
|
766 * Sets up event listeners necessary for the presenter. |
|
767 */ |
|
768 _setupEventListeners: function TVP__setupEventListeners() |
|
769 { |
|
770 this.contentWindow.addEventListener("resize", this._onResize, false); |
|
771 }, |
|
772 |
|
773 /** |
|
774 * Called when the content window of the current browser is resized. |
|
775 */ |
|
776 _onResize: function TVP_onResize(e) |
|
777 { |
|
778 let zoom = this._getPageZoom(); |
|
779 let width = e.target.innerWidth * zoom; |
|
780 let height = e.target.innerHeight * zoom; |
|
781 |
|
782 // handle aspect ratio changes to update the projection matrix |
|
783 this._renderer.width = width; |
|
784 this._renderer.height = height; |
|
785 |
|
786 this._redraw = true; |
|
787 }, |
|
788 |
|
789 /** |
|
790 * Highlights a specific node. |
|
791 * |
|
792 * @param {Element} aNode |
|
793 * the html node to be highlighted |
|
794 * @param {String} aFlags |
|
795 * flags specifying highlighting options |
|
796 */ |
|
797 highlightNode: function TVP_highlightNode(aNode, aFlags) |
|
798 { |
|
799 this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags); |
|
800 }, |
|
801 |
|
802 /** |
|
803 * Picks a stacked dom node at the x and y screen coordinates and highlights |
|
804 * the selected node in the mesh. |
|
805 * |
|
806 * @param {Number} x |
|
807 * the current horizontal coordinate of the mouse |
|
808 * @param {Number} y |
|
809 * the current vertical coordinate of the mouse |
|
810 * @param {Object} aProperties |
|
811 * an object containing the following properties: |
|
812 * {Function} onpick: function to be called after picking succeeded |
|
813 * {Function} onfail: function to be called after picking failed |
|
814 */ |
|
815 highlightNodeAt: function TVP_highlightNodeAt(x, y, aProperties) |
|
816 { |
|
817 // make sure the properties parameter is a valid object |
|
818 aProperties = aProperties || {}; |
|
819 |
|
820 // try to pick a mesh node using the current x, y coordinates |
|
821 this.pickNode(x, y, { |
|
822 |
|
823 /** |
|
824 * Mesh picking failed (nothing was found for the picked point). |
|
825 */ |
|
826 onfail: function TVP_onHighlightFail() |
|
827 { |
|
828 this.highlightNodeFor(-1); |
|
829 |
|
830 if ("function" === typeof aProperties.onfail) { |
|
831 aProperties.onfail(); |
|
832 } |
|
833 }.bind(this), |
|
834 |
|
835 /** |
|
836 * Mesh picking succeeded. |
|
837 * |
|
838 * @param {Object} aIntersection |
|
839 * object containing the intersection details |
|
840 */ |
|
841 onpick: function TVP_onHighlightPick(aIntersection) |
|
842 { |
|
843 this.highlightNodeFor(aIntersection.index); |
|
844 |
|
845 if ("function" === typeof aProperties.onpick) { |
|
846 aProperties.onpick(); |
|
847 } |
|
848 }.bind(this) |
|
849 }); |
|
850 }, |
|
851 |
|
852 /** |
|
853 * Sets the corresponding highlight coordinates and color based on the |
|
854 * information supplied. |
|
855 * |
|
856 * @param {Number} aNodeIndex |
|
857 * the index of the node in the this._traverseData array |
|
858 * @param {String} aFlags |
|
859 * flags specifying highlighting options |
|
860 */ |
|
861 highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags) |
|
862 { |
|
863 this._redraw = true; |
|
864 |
|
865 // if the node was already selected, don't do anything |
|
866 if (this._currentSelection === aNodeIndex) { |
|
867 return; |
|
868 } |
|
869 |
|
870 // if an invalid or nonexisted node is specified, disable the highlight |
|
871 if (aNodeIndex < 0) { |
|
872 this._currentSelection = -1; |
|
873 this._highlight.disabled = true; |
|
874 |
|
875 Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.UNHIGHLIGHTING, null); |
|
876 return; |
|
877 } |
|
878 |
|
879 let highlight = this._highlight; |
|
880 let info = this._traverseData.info[aNodeIndex]; |
|
881 let style = TiltVisualizerStyle.nodes; |
|
882 |
|
883 highlight.disabled = false; |
|
884 highlight.fill = style[info.name] || style.highlight.defaultFill; |
|
885 highlight.stroke = style.highlight.defaultStroke; |
|
886 highlight.strokeWeight = style.highlight.defaultStrokeWeight; |
|
887 |
|
888 let x = info.coord.left; |
|
889 let y = info.coord.top; |
|
890 let w = info.coord.width; |
|
891 let h = info.coord.height; |
|
892 let z = info.coord.depth + info.coord.thickness; |
|
893 |
|
894 vec3.set([x, y, z], highlight.v0); |
|
895 vec3.set([x + w, y, z], highlight.v1); |
|
896 vec3.set([x + w, y + h, z], highlight.v2); |
|
897 vec3.set([x, y + h, z], highlight.v3); |
|
898 |
|
899 this._currentSelection = aNodeIndex; |
|
900 |
|
901 // if something is highlighted, make sure it's inside the current viewport; |
|
902 // the point which should be moved into view is considered the center [x, y] |
|
903 // position along the top edge of the currently selected node |
|
904 |
|
905 if (aFlags && aFlags.indexOf("moveIntoView") !== -1) |
|
906 { |
|
907 this.controller.arcball.moveIntoView(vec3.lerp( |
|
908 vec3.scale(this._highlight.v0, this.transforms.zoom, []), |
|
909 vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5)); |
|
910 } |
|
911 |
|
912 Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.HIGHLIGHTING, null); |
|
913 }, |
|
914 |
|
915 /** |
|
916 * Deletes a node from the visualization mesh. |
|
917 * |
|
918 * @param {Number} aNodeIndex |
|
919 * the index of the node in the this._traverseData array; |
|
920 * if not specified, it will default to the current selection |
|
921 */ |
|
922 deleteNode: function TVP_deleteNode(aNodeIndex) |
|
923 { |
|
924 // we probably don't want to delete the html or body node.. just sayin' |
|
925 if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) { |
|
926 return; |
|
927 } |
|
928 |
|
929 let renderer = this._renderer; |
|
930 |
|
931 let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES); |
|
932 let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES); |
|
933 let group = this._meshStacks[groupIndex]; |
|
934 let vertices = group.vertices.components; |
|
935 |
|
936 for (let i = 0, k = 36 * nodeIndex; i < 36; i++) { |
|
937 vertices[i + k] = 0; |
|
938 } |
|
939 |
|
940 group.vertices = new renderer.VertexBuffer(vertices, 3); |
|
941 this._highlight.disabled = true; |
|
942 this._redraw = true; |
|
943 |
|
944 Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.NODE_REMOVED, null); |
|
945 }, |
|
946 |
|
947 /** |
|
948 * Picks a stacked dom node at the x and y screen coordinates and issues |
|
949 * a callback function with the found intersection. |
|
950 * |
|
951 * @param {Number} x |
|
952 * the current horizontal coordinate of the mouse |
|
953 * @param {Number} y |
|
954 * the current vertical coordinate of the mouse |
|
955 * @param {Object} aProperties |
|
956 * an object containing the following properties: |
|
957 * {Function} onpick: function to be called at intersection |
|
958 * {Function} onfail: function to be called if no intersections |
|
959 */ |
|
960 pickNode: function TVP_pickNode(x, y, aProperties) |
|
961 { |
|
962 // make sure the properties parameter is a valid object |
|
963 aProperties = aProperties || {}; |
|
964 |
|
965 // if the mesh wasn't created yet, don't continue picking |
|
966 if (!this._meshStacks || !this._meshWireframe) { |
|
967 return; |
|
968 } |
|
969 |
|
970 let worker = new ChromeWorker(TILT_PICKER); |
|
971 |
|
972 worker.addEventListener("message", function TVP_onMessage(event) { |
|
973 if (event.data) { |
|
974 if ("function" === typeof aProperties.onpick) { |
|
975 aProperties.onpick(event.data); |
|
976 } |
|
977 } else { |
|
978 if ("function" === typeof aProperties.onfail) { |
|
979 aProperties.onfail(); |
|
980 } |
|
981 } |
|
982 }, false); |
|
983 |
|
984 let zoom = this._getPageZoom(); |
|
985 let width = this._renderer.width * zoom; |
|
986 let height = this._renderer.height * zoom; |
|
987 x *= zoom; |
|
988 y *= zoom; |
|
989 |
|
990 // create a ray following the mouse direction from the near clipping plane |
|
991 // to the far clipping plane, to check for intersections with the mesh, |
|
992 // and do all the heavy lifting in a separate thread |
|
993 worker.postMessage({ |
|
994 vertices: this._meshData.allVertices, |
|
995 |
|
996 // create the ray destined for 3D picking |
|
997 ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height], |
|
998 this._meshStacks.mvMatrix, |
|
999 this._meshStacks.projMatrix) |
|
1000 }); |
|
1001 }, |
|
1002 |
|
1003 /** |
|
1004 * Delegate translation method, used by the controller. |
|
1005 * |
|
1006 * @param {Array} aTranslation |
|
1007 * the new translation on the [x, y, z] axis |
|
1008 */ |
|
1009 setTranslation: function TVP_setTranslation(aTranslation) |
|
1010 { |
|
1011 let x = aTranslation[0]; |
|
1012 let y = aTranslation[1]; |
|
1013 let z = aTranslation[2]; |
|
1014 let transforms = this.transforms; |
|
1015 |
|
1016 // only update the translation if it's not already set |
|
1017 if (transforms.translation[0] !== x || |
|
1018 transforms.translation[1] !== y || |
|
1019 transforms.translation[2] !== z) { |
|
1020 |
|
1021 vec3.set(aTranslation, transforms.translation); |
|
1022 this._redraw = true; |
|
1023 } |
|
1024 }, |
|
1025 |
|
1026 /** |
|
1027 * Delegate rotation method, used by the controller. |
|
1028 * |
|
1029 * @param {Array} aQuaternion |
|
1030 * the rotation quaternion, as [x, y, z, w] |
|
1031 */ |
|
1032 setRotation: function TVP_setRotation(aQuaternion) |
|
1033 { |
|
1034 let x = aQuaternion[0]; |
|
1035 let y = aQuaternion[1]; |
|
1036 let z = aQuaternion[2]; |
|
1037 let w = aQuaternion[3]; |
|
1038 let transforms = this.transforms; |
|
1039 |
|
1040 // only update the rotation if it's not already set |
|
1041 if (transforms.rotation[0] !== x || |
|
1042 transforms.rotation[1] !== y || |
|
1043 transforms.rotation[2] !== z || |
|
1044 transforms.rotation[3] !== w) { |
|
1045 |
|
1046 quat4.set(aQuaternion, transforms.rotation); |
|
1047 this._redraw = true; |
|
1048 } |
|
1049 }, |
|
1050 |
|
1051 /** |
|
1052 * Handles notifications at specific frame counts. |
|
1053 */ |
|
1054 _handleKeyframeNotifications: function TV__handleKeyframeNotifications() |
|
1055 { |
|
1056 if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) { |
|
1057 this._time = INTRO_TRANSITION_DURATION; |
|
1058 } |
|
1059 if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) { |
|
1060 this._time = OUTRO_TRANSITION_DURATION; |
|
1061 } |
|
1062 |
|
1063 if (this._time >= INTRO_TRANSITION_DURATION && |
|
1064 !this._isInitializationFinished && |
|
1065 !this._isExecutingDestruction) { |
|
1066 |
|
1067 this._isInitializationFinished = true; |
|
1068 Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.INITIALIZED, null); |
|
1069 |
|
1070 if ("function" === typeof this._onInitializationFinished) { |
|
1071 this._onInitializationFinished(); |
|
1072 } |
|
1073 } |
|
1074 |
|
1075 if (this._time >= OUTRO_TRANSITION_DURATION && |
|
1076 !this._isDestructionFinished && |
|
1077 this._isExecutingDestruction) { |
|
1078 |
|
1079 this._isDestructionFinished = true; |
|
1080 Services.obs.notifyObservers(this.contentWindow, this.NOTIFICATIONS.BEFORE_DESTROYED, null); |
|
1081 |
|
1082 if ("function" === typeof this._onDestructionFinished) { |
|
1083 this._onDestructionFinished(); |
|
1084 } |
|
1085 } |
|
1086 }, |
|
1087 |
|
1088 /** |
|
1089 * Starts executing the destruction sequence and issues a callback function |
|
1090 * when finished. |
|
1091 * |
|
1092 * @param {Function} aCallback |
|
1093 * the destruction finished callback |
|
1094 */ |
|
1095 executeDestruction: function TV_executeDestruction(aCallback) |
|
1096 { |
|
1097 if (!this._isExecutingDestruction) { |
|
1098 this._isExecutingDestruction = true; |
|
1099 this._onDestructionFinished = aCallback; |
|
1100 |
|
1101 // if we execute the destruction after the initialization finishes, |
|
1102 // proceed normally; otherwise, skip everything and immediately issue |
|
1103 // the callback |
|
1104 |
|
1105 if (this._time > OUTRO_TRANSITION_DURATION) { |
|
1106 this._time = 0; |
|
1107 this._redraw = true; |
|
1108 } else { |
|
1109 aCallback(); |
|
1110 } |
|
1111 } |
|
1112 }, |
|
1113 |
|
1114 /** |
|
1115 * Checks if this object was initialized properly. |
|
1116 * |
|
1117 * @return {Boolean} true if the object was initialized properly |
|
1118 */ |
|
1119 isInitialized: function TVP_isInitialized() |
|
1120 { |
|
1121 return this._renderer && this._renderer.context; |
|
1122 }, |
|
1123 |
|
1124 /** |
|
1125 * Function called when this object is destroyed. |
|
1126 */ |
|
1127 _finalize: function TVP__finalize() |
|
1128 { |
|
1129 TiltUtils.destroyObject(this._visualizationProgram); |
|
1130 TiltUtils.destroyObject(this._texture); |
|
1131 |
|
1132 if (this._meshStacks) { |
|
1133 this._meshStacks.forEach(function(group) { |
|
1134 TiltUtils.destroyObject(group.vertices); |
|
1135 TiltUtils.destroyObject(group.texCoord); |
|
1136 TiltUtils.destroyObject(group.color); |
|
1137 TiltUtils.destroyObject(group.indices); |
|
1138 }); |
|
1139 } |
|
1140 if (this._meshWireframe) { |
|
1141 this._meshWireframe.forEach(function(group) { |
|
1142 TiltUtils.destroyObject(group.indices); |
|
1143 }); |
|
1144 } |
|
1145 |
|
1146 TiltUtils.destroyObject(this._renderer); |
|
1147 |
|
1148 // Closing the tab would result in contentWindow being a dead object, |
|
1149 // so operations like removing event listeners won't work anymore. |
|
1150 if (this.contentWindow == this.chromeWindow.content) { |
|
1151 this.contentWindow.removeEventListener("resize", this._onResize, false); |
|
1152 } |
|
1153 } |
|
1154 }; |
|
1155 |
|
1156 /** |
|
1157 * A mouse and keyboard controller implementation. |
|
1158 * |
|
1159 * @param {HTMLCanvasElement} aCanvas |
|
1160 * the visualization canvas element |
|
1161 * @param {TiltVisualizer.Presenter} aPresenter |
|
1162 * the presenter instance to control |
|
1163 */ |
|
1164 TiltVisualizer.Controller = function TV_Controller(aCanvas, aPresenter) |
|
1165 { |
|
1166 /** |
|
1167 * A canvas overlay on which mouse and keyboard event listeners are attached. |
|
1168 */ |
|
1169 this.canvas = aCanvas; |
|
1170 |
|
1171 /** |
|
1172 * Save a reference to the presenter to modify its model-view transforms. |
|
1173 */ |
|
1174 this.presenter = aPresenter; |
|
1175 this.presenter.controller = this; |
|
1176 |
|
1177 /** |
|
1178 * The initial controller dimensions and offset, in pixels. |
|
1179 */ |
|
1180 this._zoom = aPresenter.transforms.zoom; |
|
1181 this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom; |
|
1182 this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom; |
|
1183 this._width = aCanvas.width; |
|
1184 this._height = aCanvas.height; |
|
1185 |
|
1186 /** |
|
1187 * Arcball used to control the visualization using the mouse. |
|
1188 */ |
|
1189 this.arcball = new TiltVisualizer.Arcball( |
|
1190 this.presenter.chromeWindow, this._width, this._height, 0, |
|
1191 [ |
|
1192 this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0, |
|
1193 this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0 |
|
1194 ]); |
|
1195 |
|
1196 /** |
|
1197 * Object containing the rotation quaternion and the translation amount. |
|
1198 */ |
|
1199 this._coordinates = null; |
|
1200 |
|
1201 // bind the owner object to the necessary functions |
|
1202 TiltUtils.bindObjectFunc(this, "_update"); |
|
1203 TiltUtils.bindObjectFunc(this, "^_on"); |
|
1204 |
|
1205 // add the necessary event listeners |
|
1206 this.addEventListeners(); |
|
1207 |
|
1208 // attach this controller's update function to the presenter ondraw event |
|
1209 this.presenter._controllerUpdate = this._update; |
|
1210 }; |
|
1211 |
|
1212 TiltVisualizer.Controller.prototype = { |
|
1213 |
|
1214 /** |
|
1215 * Adds events listeners required by this controller. |
|
1216 */ |
|
1217 addEventListeners: function TVC_addEventListeners() |
|
1218 { |
|
1219 let canvas = this.canvas; |
|
1220 let presenter = this.presenter; |
|
1221 |
|
1222 // bind commonly used mouse and keyboard events with the controller |
|
1223 canvas.addEventListener("mousedown", this._onMouseDown, false); |
|
1224 canvas.addEventListener("mouseup", this._onMouseUp, false); |
|
1225 canvas.addEventListener("mousemove", this._onMouseMove, false); |
|
1226 canvas.addEventListener("mouseover", this._onMouseOver, false); |
|
1227 canvas.addEventListener("mouseout", this._onMouseOut, false); |
|
1228 canvas.addEventListener("MozMousePixelScroll", this._onMozScroll, false); |
|
1229 canvas.addEventListener("keydown", this._onKeyDown, false); |
|
1230 canvas.addEventListener("keyup", this._onKeyUp, false); |
|
1231 canvas.addEventListener("blur", this._onBlur, false); |
|
1232 |
|
1233 // handle resize events to change the arcball dimensions |
|
1234 presenter.contentWindow.addEventListener("resize", this._onResize, false); |
|
1235 }, |
|
1236 |
|
1237 /** |
|
1238 * Removes all added events listeners required by this controller. |
|
1239 */ |
|
1240 removeEventListeners: function TVC_removeEventListeners() |
|
1241 { |
|
1242 let canvas = this.canvas; |
|
1243 let presenter = this.presenter; |
|
1244 |
|
1245 canvas.removeEventListener("mousedown", this._onMouseDown, false); |
|
1246 canvas.removeEventListener("mouseup", this._onMouseUp, false); |
|
1247 canvas.removeEventListener("mousemove", this._onMouseMove, false); |
|
1248 canvas.removeEventListener("mouseover", this._onMouseOver, false); |
|
1249 canvas.removeEventListener("mouseout", this._onMouseOut, false); |
|
1250 canvas.removeEventListener("MozMousePixelScroll", this._onMozScroll, false); |
|
1251 canvas.removeEventListener("keydown", this._onKeyDown, false); |
|
1252 canvas.removeEventListener("keyup", this._onKeyUp, false); |
|
1253 canvas.removeEventListener("blur", this._onBlur, false); |
|
1254 |
|
1255 // Closing the tab would result in contentWindow being a dead object, |
|
1256 // so operations like removing event listeners won't work anymore. |
|
1257 if (presenter.contentWindow == presenter.chromeWindow.content) { |
|
1258 presenter.contentWindow.removeEventListener("resize", this._onResize, false); |
|
1259 } |
|
1260 }, |
|
1261 |
|
1262 /** |
|
1263 * Function called each frame, updating the visualization camera transforms. |
|
1264 * |
|
1265 * @param {Number} aTime |
|
1266 * total time passed since rendering started |
|
1267 * @param {Number} aDelta |
|
1268 * the current animation frame delta |
|
1269 */ |
|
1270 _update: function TVC__update(aTime, aDelta) |
|
1271 { |
|
1272 this._time = aTime; |
|
1273 this._coordinates = this.arcball.update(aDelta); |
|
1274 |
|
1275 this.presenter.setRotation(this._coordinates.rotation); |
|
1276 this.presenter.setTranslation(this._coordinates.translation); |
|
1277 }, |
|
1278 |
|
1279 /** |
|
1280 * Called once after every time a mouse button is pressed. |
|
1281 */ |
|
1282 _onMouseDown: function TVC__onMouseDown(e) |
|
1283 { |
|
1284 e.target.focus(); |
|
1285 e.preventDefault(); |
|
1286 e.stopPropagation(); |
|
1287 |
|
1288 if (this._time < MOUSE_INTRO_DELAY) { |
|
1289 return; |
|
1290 } |
|
1291 |
|
1292 // calculate x and y coordinates using using the client and target offset |
|
1293 let button = e.which; |
|
1294 this._downX = e.clientX - e.target.offsetLeft; |
|
1295 this._downY = e.clientY - e.target.offsetTop; |
|
1296 |
|
1297 this.arcball.mouseDown(this._downX, this._downY, button); |
|
1298 }, |
|
1299 |
|
1300 /** |
|
1301 * Called every time a mouse button is released. |
|
1302 */ |
|
1303 _onMouseUp: function TVC__onMouseUp(e) |
|
1304 { |
|
1305 e.preventDefault(); |
|
1306 e.stopPropagation(); |
|
1307 |
|
1308 if (this._time < MOUSE_INTRO_DELAY) { |
|
1309 return; |
|
1310 } |
|
1311 |
|
1312 // calculate x and y coordinates using using the client and target offset |
|
1313 let button = e.which; |
|
1314 let upX = e.clientX - e.target.offsetLeft; |
|
1315 let upY = e.clientY - e.target.offsetTop; |
|
1316 |
|
1317 // a click in Tilt is issued only when the mouse pointer stays in |
|
1318 // relatively the same position |
|
1319 if (Math.abs(this._downX - upX) < MOUSE_CLICK_THRESHOLD && |
|
1320 Math.abs(this._downY - upY) < MOUSE_CLICK_THRESHOLD) { |
|
1321 |
|
1322 this.presenter.highlightNodeAt(upX, upY); |
|
1323 } |
|
1324 |
|
1325 this.arcball.mouseUp(upX, upY, button); |
|
1326 }, |
|
1327 |
|
1328 /** |
|
1329 * Called every time the mouse moves. |
|
1330 */ |
|
1331 _onMouseMove: function TVC__onMouseMove(e) |
|
1332 { |
|
1333 e.preventDefault(); |
|
1334 e.stopPropagation(); |
|
1335 |
|
1336 if (this._time < MOUSE_INTRO_DELAY) { |
|
1337 return; |
|
1338 } |
|
1339 |
|
1340 // calculate x and y coordinates using using the client and target offset |
|
1341 let moveX = e.clientX - e.target.offsetLeft; |
|
1342 let moveY = e.clientY - e.target.offsetTop; |
|
1343 |
|
1344 this.arcball.mouseMove(moveX, moveY); |
|
1345 }, |
|
1346 |
|
1347 /** |
|
1348 * Called when the mouse leaves the visualization bounds. |
|
1349 */ |
|
1350 _onMouseOver: function TVC__onMouseOver(e) |
|
1351 { |
|
1352 e.preventDefault(); |
|
1353 e.stopPropagation(); |
|
1354 |
|
1355 this.arcball.mouseOver(); |
|
1356 }, |
|
1357 |
|
1358 /** |
|
1359 * Called when the mouse leaves the visualization bounds. |
|
1360 */ |
|
1361 _onMouseOut: function TVC__onMouseOut(e) |
|
1362 { |
|
1363 e.preventDefault(); |
|
1364 e.stopPropagation(); |
|
1365 |
|
1366 this.arcball.mouseOut(); |
|
1367 }, |
|
1368 |
|
1369 /** |
|
1370 * Called when the mouse wheel is used. |
|
1371 */ |
|
1372 _onMozScroll: function TVC__onMozScroll(e) |
|
1373 { |
|
1374 e.preventDefault(); |
|
1375 e.stopPropagation(); |
|
1376 |
|
1377 this.arcball.zoom(e.detail); |
|
1378 }, |
|
1379 |
|
1380 /** |
|
1381 * Called when a key is pressed. |
|
1382 */ |
|
1383 _onKeyDown: function TVC__onKeyDown(e) |
|
1384 { |
|
1385 let code = e.keyCode || e.which; |
|
1386 |
|
1387 if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { |
|
1388 e.preventDefault(); |
|
1389 e.stopPropagation(); |
|
1390 this.arcball.keyDown(code); |
|
1391 } else { |
|
1392 this.arcball.cancelKeyEvents(); |
|
1393 } |
|
1394 |
|
1395 if (e.keyCode === e.DOM_VK_ESCAPE) { |
|
1396 let {TiltManager} = require("devtools/tilt/tilt"); |
|
1397 let tilt = |
|
1398 TiltManager.getTiltForBrowser(this.presenter.chromeWindow); |
|
1399 e.preventDefault(); |
|
1400 e.stopPropagation(); |
|
1401 tilt.destroy(tilt.currentWindowId, true); |
|
1402 } |
|
1403 }, |
|
1404 |
|
1405 /** |
|
1406 * Called when a key is released. |
|
1407 */ |
|
1408 _onKeyUp: function TVC__onKeyUp(e) |
|
1409 { |
|
1410 let code = e.keyCode || e.which; |
|
1411 |
|
1412 if (code === e.DOM_VK_X) { |
|
1413 this.presenter.deleteNode(); |
|
1414 } |
|
1415 if (code === e.DOM_VK_F) { |
|
1416 let highlight = this.presenter._highlight; |
|
1417 let zoom = this.presenter.transforms.zoom; |
|
1418 |
|
1419 this.arcball.moveIntoView(vec3.lerp( |
|
1420 vec3.scale(highlight.v0, zoom, []), |
|
1421 vec3.scale(highlight.v1, zoom, []), 0.5)); |
|
1422 } |
|
1423 if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { |
|
1424 e.preventDefault(); |
|
1425 e.stopPropagation(); |
|
1426 this.arcball.keyUp(code); |
|
1427 } |
|
1428 }, |
|
1429 |
|
1430 /** |
|
1431 * Called when the canvas looses focus. |
|
1432 */ |
|
1433 _onBlur: function TVC__onBlur(e) { |
|
1434 this.arcball.cancelKeyEvents(); |
|
1435 }, |
|
1436 |
|
1437 /** |
|
1438 * Called when the content window of the current browser is resized. |
|
1439 */ |
|
1440 _onResize: function TVC__onResize(e) |
|
1441 { |
|
1442 let zoom = this.presenter._getPageZoom(); |
|
1443 let width = e.target.innerWidth * zoom; |
|
1444 let height = e.target.innerHeight * zoom; |
|
1445 |
|
1446 this.arcball.resize(width, height); |
|
1447 }, |
|
1448 |
|
1449 /** |
|
1450 * Checks if this object was initialized properly. |
|
1451 * |
|
1452 * @return {Boolean} true if the object was initialized properly |
|
1453 */ |
|
1454 isInitialized: function TVC_isInitialized() |
|
1455 { |
|
1456 return this.arcball ? true : false; |
|
1457 }, |
|
1458 |
|
1459 /** |
|
1460 * Function called when this object is destroyed. |
|
1461 */ |
|
1462 _finalize: function TVC__finalize() |
|
1463 { |
|
1464 TiltUtils.destroyObject(this.arcball); |
|
1465 TiltUtils.destroyObject(this._coordinates); |
|
1466 |
|
1467 this.removeEventListeners(); |
|
1468 this.presenter.controller = null; |
|
1469 this.presenter._controllerUpdate = null; |
|
1470 } |
|
1471 }; |
|
1472 |
|
1473 /** |
|
1474 * This is a general purpose 3D rotation controller described by Ken Shoemake |
|
1475 * in the Graphics Interface ’92 Proceedings. It features good behavior |
|
1476 * easy implementation, cheap execution. |
|
1477 * |
|
1478 * @param {Window} aChromeWindow |
|
1479 * a reference to the top-level window |
|
1480 * @param {Number} aWidth |
|
1481 * the width of canvas |
|
1482 * @param {Number} aHeight |
|
1483 * the height of canvas |
|
1484 * @param {Number} aRadius |
|
1485 * optional, the radius of the arcball |
|
1486 * @param {Array} aInitialTrans |
|
1487 * optional, initial vector translation |
|
1488 * @param {Array} aInitialRot |
|
1489 * optional, initial quaternion rotation |
|
1490 */ |
|
1491 TiltVisualizer.Arcball = function TV_Arcball( |
|
1492 aChromeWindow, aWidth, aHeight, aRadius, aInitialTrans, aInitialRot) |
|
1493 { |
|
1494 /** |
|
1495 * Save a reference to the top-level window to set/remove intervals. |
|
1496 */ |
|
1497 this.chromeWindow = aChromeWindow; |
|
1498 |
|
1499 /** |
|
1500 * Values retaining the current horizontal and vertical mouse coordinates. |
|
1501 */ |
|
1502 this._mousePress = vec3.create(); |
|
1503 this._mouseRelease = vec3.create(); |
|
1504 this._mouseMove = vec3.create(); |
|
1505 this._mouseLerp = vec3.create(); |
|
1506 this._mouseButton = -1; |
|
1507 |
|
1508 /** |
|
1509 * Object retaining the current pressed key codes. |
|
1510 */ |
|
1511 this._keyCode = {}; |
|
1512 |
|
1513 /** |
|
1514 * The vectors representing the mouse coordinates mapped on the arcball |
|
1515 * and their perpendicular converted from (x, y) to (x, y, z) at specific |
|
1516 * events like mousePressed and mouseDragged. |
|
1517 */ |
|
1518 this._startVec = vec3.create(); |
|
1519 this._endVec = vec3.create(); |
|
1520 this._pVec = vec3.create(); |
|
1521 |
|
1522 /** |
|
1523 * The corresponding rotation quaternions. |
|
1524 */ |
|
1525 this._lastRot = quat4.create(); |
|
1526 this._deltaRot = quat4.create(); |
|
1527 this._currentRot = quat4.create(aInitialRot); |
|
1528 |
|
1529 /** |
|
1530 * The current camera translation coordinates. |
|
1531 */ |
|
1532 this._lastTrans = vec3.create(); |
|
1533 this._deltaTrans = vec3.create(); |
|
1534 this._currentTrans = vec3.create(aInitialTrans); |
|
1535 this._zoomAmount = 0; |
|
1536 |
|
1537 /** |
|
1538 * Additional rotation and translation vectors. |
|
1539 */ |
|
1540 this._additionalRot = vec3.create(); |
|
1541 this._additionalTrans = vec3.create(); |
|
1542 this._deltaAdditionalRot = quat4.create(); |
|
1543 this._deltaAdditionalTrans = vec3.create(); |
|
1544 |
|
1545 // load the keys controlling the arcball |
|
1546 this._loadKeys(); |
|
1547 |
|
1548 // set the current dimensions of the arcball |
|
1549 this.resize(aWidth, aHeight, aRadius); |
|
1550 }; |
|
1551 |
|
1552 TiltVisualizer.Arcball.prototype = { |
|
1553 |
|
1554 /** |
|
1555 * Call this function whenever you need the updated rotation quaternion |
|
1556 * and the zoom amount. These values will be returned as "rotation" and |
|
1557 * "translation" properties inside an object. |
|
1558 * |
|
1559 * @param {Number} aDelta |
|
1560 * the current animation frame delta |
|
1561 * |
|
1562 * @return {Object} the rotation quaternion and the translation amount |
|
1563 */ |
|
1564 update: function TVA_update(aDelta) |
|
1565 { |
|
1566 let mousePress = this._mousePress; |
|
1567 let mouseRelease = this._mouseRelease; |
|
1568 let mouseMove = this._mouseMove; |
|
1569 let mouseLerp = this._mouseLerp; |
|
1570 let mouseButton = this._mouseButton; |
|
1571 |
|
1572 // smoothly update the mouse coordinates |
|
1573 mouseLerp[0] += (mouseMove[0] - mouseLerp[0]) * ARCBALL_SENSITIVITY; |
|
1574 mouseLerp[1] += (mouseMove[1] - mouseLerp[1]) * ARCBALL_SENSITIVITY; |
|
1575 |
|
1576 // cache the interpolated mouse coordinates |
|
1577 let x = mouseLerp[0]; |
|
1578 let y = mouseLerp[1]; |
|
1579 |
|
1580 // the smoothed arcball rotation may not be finished when the mouse is |
|
1581 // pressed again, so cancel the rotation if other events occur or the |
|
1582 // animation finishes |
|
1583 if (mouseButton === 3 || x === mouseRelease[0] && y === mouseRelease[1]) { |
|
1584 this._rotating = false; |
|
1585 } |
|
1586 |
|
1587 let startVec = this._startVec; |
|
1588 let endVec = this._endVec; |
|
1589 let pVec = this._pVec; |
|
1590 |
|
1591 let lastRot = this._lastRot; |
|
1592 let deltaRot = this._deltaRot; |
|
1593 let currentRot = this._currentRot; |
|
1594 |
|
1595 // left mouse button handles rotation |
|
1596 if (mouseButton === 1 || this._rotating) { |
|
1597 // the rotation doesn't stop immediately after the left mouse button is |
|
1598 // released, so add a flag to smoothly continue it until it ends |
|
1599 this._rotating = true; |
|
1600 |
|
1601 // find the sphere coordinates of the mouse positions |
|
1602 this._pointToSphere(x, y, this.width, this.height, this.radius, endVec); |
|
1603 |
|
1604 // compute the vector perpendicular to the start & end vectors |
|
1605 vec3.cross(startVec, endVec, pVec); |
|
1606 |
|
1607 // if the begin and end vectors don't coincide |
|
1608 if (vec3.length(pVec) > 0) { |
|
1609 deltaRot[0] = pVec[0]; |
|
1610 deltaRot[1] = pVec[1]; |
|
1611 deltaRot[2] = pVec[2]; |
|
1612 |
|
1613 // in the quaternion values, w is cosine (theta / 2), |
|
1614 // where theta is the rotation angle |
|
1615 deltaRot[3] = -vec3.dot(startVec, endVec); |
|
1616 } else { |
|
1617 // return an identity rotation quaternion |
|
1618 deltaRot[0] = 0; |
|
1619 deltaRot[1] = 0; |
|
1620 deltaRot[2] = 0; |
|
1621 deltaRot[3] = 1; |
|
1622 } |
|
1623 |
|
1624 // calculate the current rotation based on the mouse click events |
|
1625 quat4.multiply(lastRot, deltaRot, currentRot); |
|
1626 } else { |
|
1627 // save the current quaternion to stack rotations |
|
1628 quat4.set(currentRot, lastRot); |
|
1629 } |
|
1630 |
|
1631 let lastTrans = this._lastTrans; |
|
1632 let deltaTrans = this._deltaTrans; |
|
1633 let currentTrans = this._currentTrans; |
|
1634 |
|
1635 // right mouse button handles panning |
|
1636 if (mouseButton === 3) { |
|
1637 // calculate a delta translation between the new and old mouse position |
|
1638 // and save it to the current translation |
|
1639 deltaTrans[0] = mouseMove[0] - mousePress[0]; |
|
1640 deltaTrans[1] = mouseMove[1] - mousePress[1]; |
|
1641 |
|
1642 currentTrans[0] = lastTrans[0] + deltaTrans[0]; |
|
1643 currentTrans[1] = lastTrans[1] + deltaTrans[1]; |
|
1644 } else { |
|
1645 // save the current panning to stack translations |
|
1646 lastTrans[0] = currentTrans[0]; |
|
1647 lastTrans[1] = currentTrans[1]; |
|
1648 } |
|
1649 |
|
1650 let zoomAmount = this._zoomAmount; |
|
1651 let keyCode = this._keyCode; |
|
1652 |
|
1653 // mouse wheel handles zooming |
|
1654 deltaTrans[2] = (zoomAmount - currentTrans[2]) * ARCBALL_ZOOM_STEP; |
|
1655 currentTrans[2] += deltaTrans[2]; |
|
1656 |
|
1657 let additionalRot = this._additionalRot; |
|
1658 let additionalTrans = this._additionalTrans; |
|
1659 let deltaAdditionalRot = this._deltaAdditionalRot; |
|
1660 let deltaAdditionalTrans = this._deltaAdditionalTrans; |
|
1661 |
|
1662 let rotateKeys = this.rotateKeys; |
|
1663 let panKeys = this.panKeys; |
|
1664 let zoomKeys = this.zoomKeys; |
|
1665 let resetKey = this.resetKey; |
|
1666 |
|
1667 // handle additional rotation and translation by the keyboard |
|
1668 if (keyCode[rotateKeys.left]) { |
|
1669 additionalRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; |
|
1670 } |
|
1671 if (keyCode[rotateKeys.right]) { |
|
1672 additionalRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; |
|
1673 } |
|
1674 if (keyCode[rotateKeys.up]) { |
|
1675 additionalRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; |
|
1676 } |
|
1677 if (keyCode[rotateKeys.down]) { |
|
1678 additionalRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP; |
|
1679 } |
|
1680 if (keyCode[panKeys.left]) { |
|
1681 additionalTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; |
|
1682 } |
|
1683 if (keyCode[panKeys.right]) { |
|
1684 additionalTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; |
|
1685 } |
|
1686 if (keyCode[panKeys.up]) { |
|
1687 additionalTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; |
|
1688 } |
|
1689 if (keyCode[panKeys.down]) { |
|
1690 additionalTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP; |
|
1691 } |
|
1692 if (keyCode[zoomKeys["in"][0]] || |
|
1693 keyCode[zoomKeys["in"][1]] || |
|
1694 keyCode[zoomKeys["in"][2]]) { |
|
1695 this.zoom(-ARCBALL_TRANSLATION_STEP); |
|
1696 } |
|
1697 if (keyCode[zoomKeys["out"][0]] || |
|
1698 keyCode[zoomKeys["out"][1]]) { |
|
1699 this.zoom(ARCBALL_TRANSLATION_STEP); |
|
1700 } |
|
1701 if (keyCode[zoomKeys["unzoom"]]) { |
|
1702 this._zoomAmount = 0; |
|
1703 } |
|
1704 if (keyCode[resetKey]) { |
|
1705 this.reset(); |
|
1706 } |
|
1707 |
|
1708 // update the delta key rotations and translations |
|
1709 deltaAdditionalRot[0] += |
|
1710 (additionalRot[0] - deltaAdditionalRot[0]) * ARCBALL_SENSITIVITY; |
|
1711 deltaAdditionalRot[1] += |
|
1712 (additionalRot[1] - deltaAdditionalRot[1]) * ARCBALL_SENSITIVITY; |
|
1713 deltaAdditionalRot[2] += |
|
1714 (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY; |
|
1715 |
|
1716 deltaAdditionalTrans[0] += |
|
1717 (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY; |
|
1718 deltaAdditionalTrans[1] += |
|
1719 (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY; |
|
1720 |
|
1721 // create an additional rotation based on the key events |
|
1722 quat4.fromEuler( |
|
1723 deltaAdditionalRot[0], |
|
1724 deltaAdditionalRot[1], |
|
1725 deltaAdditionalRot[2], deltaRot); |
|
1726 |
|
1727 // create an additional translation based on the key events |
|
1728 vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans); |
|
1729 |
|
1730 // handle the reset animation steps if necessary |
|
1731 if (this._resetInProgress) { |
|
1732 this._nextResetStep(aDelta || 1); |
|
1733 } |
|
1734 |
|
1735 // return the current rotation and translation |
|
1736 return { |
|
1737 rotation: quat4.multiply(deltaRot, currentRot), |
|
1738 translation: vec3.add(deltaTrans, currentTrans) |
|
1739 }; |
|
1740 }, |
|
1741 |
|
1742 /** |
|
1743 * Function handling the mouseDown event. |
|
1744 * Call this when the mouse was pressed. |
|
1745 * |
|
1746 * @param {Number} x |
|
1747 * the current horizontal coordinate of the mouse |
|
1748 * @param {Number} y |
|
1749 * the current vertical coordinate of the mouse |
|
1750 * @param {Number} aButton |
|
1751 * which mouse button was pressed |
|
1752 */ |
|
1753 mouseDown: function TVA_mouseDown(x, y, aButton) |
|
1754 { |
|
1755 // save the mouse down state and prepare for rotations or translations |
|
1756 this._mousePress[0] = x; |
|
1757 this._mousePress[1] = y; |
|
1758 this._mouseButton = aButton; |
|
1759 this._cancelReset(); |
|
1760 this._save(); |
|
1761 |
|
1762 // find the sphere coordinates of the mouse positions |
|
1763 this._pointToSphere( |
|
1764 x, y, this.width, this.height, this.radius, this._startVec); |
|
1765 |
|
1766 quat4.set(this._currentRot, this._lastRot); |
|
1767 }, |
|
1768 |
|
1769 /** |
|
1770 * Function handling the mouseUp event. |
|
1771 * Call this when a mouse button was released. |
|
1772 * |
|
1773 * @param {Number} x |
|
1774 * the current horizontal coordinate of the mouse |
|
1775 * @param {Number} y |
|
1776 * the current vertical coordinate of the mouse |
|
1777 */ |
|
1778 mouseUp: function TVA_mouseUp(x, y) |
|
1779 { |
|
1780 // save the mouse up state and prepare for rotations or translations |
|
1781 this._mouseRelease[0] = x; |
|
1782 this._mouseRelease[1] = y; |
|
1783 this._mouseButton = -1; |
|
1784 }, |
|
1785 |
|
1786 /** |
|
1787 * Function handling the mouseMove event. |
|
1788 * Call this when the mouse was moved. |
|
1789 * |
|
1790 * @param {Number} x |
|
1791 * the current horizontal coordinate of the mouse |
|
1792 * @param {Number} y |
|
1793 * the current vertical coordinate of the mouse |
|
1794 */ |
|
1795 mouseMove: function TVA_mouseMove(x, y) |
|
1796 { |
|
1797 // save the mouse move state and prepare for rotations or translations |
|
1798 // only if the mouse is pressed |
|
1799 if (this._mouseButton !== -1) { |
|
1800 this._mouseMove[0] = x; |
|
1801 this._mouseMove[1] = y; |
|
1802 } |
|
1803 }, |
|
1804 |
|
1805 /** |
|
1806 * Function handling the mouseOver event. |
|
1807 * Call this when the mouse enters the context bounds. |
|
1808 */ |
|
1809 mouseOver: function TVA_mouseOver() |
|
1810 { |
|
1811 // if the mouse just entered the parent bounds, stop the animation |
|
1812 this._mouseButton = -1; |
|
1813 }, |
|
1814 |
|
1815 /** |
|
1816 * Function handling the mouseOut event. |
|
1817 * Call this when the mouse leaves the context bounds. |
|
1818 */ |
|
1819 mouseOut: function TVA_mouseOut() |
|
1820 { |
|
1821 // if the mouse leaves the parent bounds, stop the animation |
|
1822 this._mouseButton = -1; |
|
1823 }, |
|
1824 |
|
1825 /** |
|
1826 * Function handling the arcball zoom amount. |
|
1827 * Call this, for example, when the mouse wheel was scrolled or zoom keys |
|
1828 * were pressed. |
|
1829 * |
|
1830 * @param {Number} aZoom |
|
1831 * the zoom direction and speed |
|
1832 */ |
|
1833 zoom: function TVA_zoom(aZoom) |
|
1834 { |
|
1835 this._cancelReset(); |
|
1836 this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom, |
|
1837 ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX); |
|
1838 }, |
|
1839 |
|
1840 /** |
|
1841 * Function handling the keyDown event. |
|
1842 * Call this when a key was pressed. |
|
1843 * |
|
1844 * @param {Number} aCode |
|
1845 * the code corresponding to the key pressed |
|
1846 */ |
|
1847 keyDown: function TVA_keyDown(aCode) |
|
1848 { |
|
1849 this._cancelReset(); |
|
1850 this._keyCode[aCode] = true; |
|
1851 }, |
|
1852 |
|
1853 /** |
|
1854 * Function handling the keyUp event. |
|
1855 * Call this when a key was released. |
|
1856 * |
|
1857 * @param {Number} aCode |
|
1858 * the code corresponding to the key released |
|
1859 */ |
|
1860 keyUp: function TVA_keyUp(aCode) |
|
1861 { |
|
1862 this._keyCode[aCode] = false; |
|
1863 }, |
|
1864 |
|
1865 /** |
|
1866 * Maps the 2d coordinates of the mouse location to a 3d point on a sphere. |
|
1867 * |
|
1868 * @param {Number} x |
|
1869 * the current horizontal coordinate of the mouse |
|
1870 * @param {Number} y |
|
1871 * the current vertical coordinate of the mouse |
|
1872 * @param {Number} aWidth |
|
1873 * the width of canvas |
|
1874 * @param {Number} aHeight |
|
1875 * the height of canvas |
|
1876 * @param {Number} aRadius |
|
1877 * optional, the radius of the arcball |
|
1878 * @param {Array} aSphereVec |
|
1879 * a 3d vector to store the sphere coordinates |
|
1880 */ |
|
1881 _pointToSphere: function TVA__pointToSphere( |
|
1882 x, y, aWidth, aHeight, aRadius, aSphereVec) |
|
1883 { |
|
1884 // adjust point coords and scale down to range of [-1..1] |
|
1885 x = (x - aWidth * 0.5) / aRadius; |
|
1886 y = (y - aHeight * 0.5) / aRadius; |
|
1887 |
|
1888 // compute the square length of the vector to the point from the center |
|
1889 let normal = 0; |
|
1890 let sqlength = x * x + y * y; |
|
1891 |
|
1892 // if the point is mapped outside of the sphere |
|
1893 if (sqlength > 1) { |
|
1894 // calculate the normalization factor |
|
1895 normal = 1 / Math.sqrt(sqlength); |
|
1896 |
|
1897 // set the normalized vector (a point on the sphere) |
|
1898 aSphereVec[0] = x * normal; |
|
1899 aSphereVec[1] = y * normal; |
|
1900 aSphereVec[2] = 0; |
|
1901 } else { |
|
1902 // set the vector to a point mapped inside the sphere |
|
1903 aSphereVec[0] = x; |
|
1904 aSphereVec[1] = y; |
|
1905 aSphereVec[2] = Math.sqrt(1 - sqlength); |
|
1906 } |
|
1907 }, |
|
1908 |
|
1909 /** |
|
1910 * Cancels all pending transformations caused by key events. |
|
1911 */ |
|
1912 cancelKeyEvents: function TVA_cancelKeyEvents() |
|
1913 { |
|
1914 this._keyCode = {}; |
|
1915 }, |
|
1916 |
|
1917 /** |
|
1918 * Cancels all pending transformations caused by mouse events. |
|
1919 */ |
|
1920 cancelMouseEvents: function TVA_cancelMouseEvents() |
|
1921 { |
|
1922 this._rotating = false; |
|
1923 this._mouseButton = -1; |
|
1924 }, |
|
1925 |
|
1926 /** |
|
1927 * Incremental translation method. |
|
1928 * |
|
1929 * @param {Array} aTranslation |
|
1930 * the translation ammount on the [x, y] axis |
|
1931 */ |
|
1932 translate: function TVP_translate(aTranslation) |
|
1933 { |
|
1934 this._additionalTrans[0] += aTranslation[0]; |
|
1935 this._additionalTrans[1] += aTranslation[1]; |
|
1936 }, |
|
1937 |
|
1938 /** |
|
1939 * Incremental rotation method. |
|
1940 * |
|
1941 * @param {Array} aRotation |
|
1942 * the rotation ammount along the [x, y, z] axis |
|
1943 */ |
|
1944 rotate: function TVP_rotate(aRotation) |
|
1945 { |
|
1946 // explicitly rotate along y, x, z values because they're eulerian angles |
|
1947 this._additionalRot[0] += TiltMath.radians(aRotation[1]); |
|
1948 this._additionalRot[1] += TiltMath.radians(aRotation[0]); |
|
1949 this._additionalRot[2] += TiltMath.radians(aRotation[2]); |
|
1950 }, |
|
1951 |
|
1952 /** |
|
1953 * Moves a target point into view only if it's outside the currently visible |
|
1954 * area bounds (in which case it also resets any additional transforms). |
|
1955 * |
|
1956 * @param {Arary} aPoint |
|
1957 * the [x, y] point which should be brought into view |
|
1958 */ |
|
1959 moveIntoView: function TVA_moveIntoView(aPoint) { |
|
1960 let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]); |
|
1961 let visiblePointY = -(this._currentTrans[1] + this._additionalTrans[1]); |
|
1962 |
|
1963 if (aPoint[1] - visiblePointY - MOVE_INTO_VIEW_ACCURACY > this.height || |
|
1964 aPoint[1] - visiblePointY + MOVE_INTO_VIEW_ACCURACY < 0 || |
|
1965 aPoint[0] - visiblePointX > this.width || |
|
1966 aPoint[0] - visiblePointX < 0) { |
|
1967 this.reset([0, -aPoint[1]]); |
|
1968 } |
|
1969 }, |
|
1970 |
|
1971 /** |
|
1972 * Resize this implementation to use different bounds. |
|
1973 * This function is automatically called when the arcball is created. |
|
1974 * |
|
1975 * @param {Number} newWidth |
|
1976 * the new width of canvas |
|
1977 * @param {Number} newHeight |
|
1978 * the new height of canvas |
|
1979 * @param {Number} newRadius |
|
1980 * optional, the new radius of the arcball |
|
1981 */ |
|
1982 resize: function TVA_resize(newWidth, newHeight, newRadius) |
|
1983 { |
|
1984 if (!newWidth || !newHeight) { |
|
1985 return; |
|
1986 } |
|
1987 |
|
1988 // set the new width, height and radius dimensions |
|
1989 this.width = newWidth; |
|
1990 this.height = newHeight; |
|
1991 this.radius = newRadius ? newRadius : Math.min(newWidth, newHeight); |
|
1992 this._save(); |
|
1993 }, |
|
1994 |
|
1995 /** |
|
1996 * Starts an animation resetting the arcball transformations to identity. |
|
1997 * |
|
1998 * @param {Array} aFinalTranslation |
|
1999 * optional, final vector translation |
|
2000 * @param {Array} aFinalRotation |
|
2001 * optional, final quaternion rotation |
|
2002 */ |
|
2003 reset: function TVA_reset(aFinalTranslation, aFinalRotation) |
|
2004 { |
|
2005 if ("function" === typeof this._onResetStart) { |
|
2006 this._onResetStart(); |
|
2007 this._onResetStart = null; |
|
2008 } |
|
2009 |
|
2010 this.cancelMouseEvents(); |
|
2011 this.cancelKeyEvents(); |
|
2012 this._cancelReset(); |
|
2013 |
|
2014 this._save(); |
|
2015 this._resetFinalTranslation = vec3.create(aFinalTranslation); |
|
2016 this._resetFinalRotation = quat4.create(aFinalRotation); |
|
2017 this._resetInProgress = true; |
|
2018 }, |
|
2019 |
|
2020 /** |
|
2021 * Cancels the current arcball reset animation if there is one. |
|
2022 */ |
|
2023 _cancelReset: function TVA__cancelReset() |
|
2024 { |
|
2025 if (this._resetInProgress) { |
|
2026 this._resetInProgress = false; |
|
2027 this._save(); |
|
2028 |
|
2029 if ("function" === typeof this._onResetFinish) { |
|
2030 this._onResetFinish(); |
|
2031 this._onResetFinish = null; |
|
2032 this._onResetStep = null; |
|
2033 } |
|
2034 } |
|
2035 }, |
|
2036 |
|
2037 /** |
|
2038 * Executes the next step in the arcball reset animation. |
|
2039 * |
|
2040 * @param {Number} aDelta |
|
2041 * the current animation frame delta |
|
2042 */ |
|
2043 _nextResetStep: function TVA__nextResetStep(aDelta) |
|
2044 { |
|
2045 // a very large animation frame delta (in case of seriously low framerate) |
|
2046 // would cause all the interpolations to become highly unstable |
|
2047 aDelta = TiltMath.clamp(aDelta, 1, 100); |
|
2048 |
|
2049 let fNearZero = EPSILON * EPSILON; |
|
2050 let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta; |
|
2051 let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR; |
|
2052 let fTran = this._resetFinalTranslation; |
|
2053 let fRot = this._resetFinalRotation; |
|
2054 |
|
2055 let t = vec3.create(fTran); |
|
2056 let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot); |
|
2057 |
|
2058 // reset the rotation quaternion and translation vector |
|
2059 vec3.lerp(this._currentTrans, t, fInterpLin); |
|
2060 quat4.slerp(this._currentRot, r, fInterpSph); |
|
2061 |
|
2062 // also reset any additional transforms by the keyboard or mouse |
|
2063 vec3.scale(this._additionalTrans, fInterpLin); |
|
2064 vec3.scale(this._additionalRot, fInterpLin); |
|
2065 this._zoomAmount *= fInterpLin; |
|
2066 |
|
2067 // clear the loop if the all values are very close to zero |
|
2068 if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero && |
|
2069 vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero && |
|
2070 vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero && |
|
2071 vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero && |
|
2072 vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero && |
|
2073 vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero && |
|
2074 vec3.length(this._additionalRot) < fNearZero && |
|
2075 vec3.length(this._additionalTrans) < fNearZero) { |
|
2076 |
|
2077 this._cancelReset(); |
|
2078 } |
|
2079 |
|
2080 if ("function" === typeof this._onResetStep) { |
|
2081 this._onResetStep(); |
|
2082 } |
|
2083 }, |
|
2084 |
|
2085 /** |
|
2086 * Loads the keys to control this arcball. |
|
2087 */ |
|
2088 _loadKeys: function TVA__loadKeys() |
|
2089 { |
|
2090 this.rotateKeys = { |
|
2091 "up": Ci.nsIDOMKeyEvent["DOM_VK_W"], |
|
2092 "down": Ci.nsIDOMKeyEvent["DOM_VK_S"], |
|
2093 "left": Ci.nsIDOMKeyEvent["DOM_VK_A"], |
|
2094 "right": Ci.nsIDOMKeyEvent["DOM_VK_D"], |
|
2095 }; |
|
2096 this.panKeys = { |
|
2097 "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"], |
|
2098 "down": Ci.nsIDOMKeyEvent["DOM_VK_DOWN"], |
|
2099 "left": Ci.nsIDOMKeyEvent["DOM_VK_LEFT"], |
|
2100 "right": Ci.nsIDOMKeyEvent["DOM_VK_RIGHT"], |
|
2101 }; |
|
2102 this.zoomKeys = { |
|
2103 "in": [ |
|
2104 Ci.nsIDOMKeyEvent["DOM_VK_I"], |
|
2105 Ci.nsIDOMKeyEvent["DOM_VK_ADD"], |
|
2106 Ci.nsIDOMKeyEvent["DOM_VK_EQUALS"], |
|
2107 ], |
|
2108 "out": [ |
|
2109 Ci.nsIDOMKeyEvent["DOM_VK_O"], |
|
2110 Ci.nsIDOMKeyEvent["DOM_VK_SUBTRACT"], |
|
2111 ], |
|
2112 "unzoom": Ci.nsIDOMKeyEvent["DOM_VK_0"] |
|
2113 }; |
|
2114 this.resetKey = Ci.nsIDOMKeyEvent["DOM_VK_R"]; |
|
2115 }, |
|
2116 |
|
2117 /** |
|
2118 * Saves the current arcball state, typically after resize or mouse events. |
|
2119 */ |
|
2120 _save: function TVA__save() |
|
2121 { |
|
2122 if (this._mousePress) { |
|
2123 let x = this._mousePress[0]; |
|
2124 let y = this._mousePress[1]; |
|
2125 |
|
2126 this._mouseMove[0] = x; |
|
2127 this._mouseMove[1] = y; |
|
2128 this._mouseRelease[0] = x; |
|
2129 this._mouseRelease[1] = y; |
|
2130 this._mouseLerp[0] = x; |
|
2131 this._mouseLerp[1] = y; |
|
2132 } |
|
2133 }, |
|
2134 |
|
2135 /** |
|
2136 * Function called when this object is destroyed. |
|
2137 */ |
|
2138 _finalize: function TVA__finalize() |
|
2139 { |
|
2140 this._cancelReset(); |
|
2141 } |
|
2142 }; |
|
2143 |
|
2144 /** |
|
2145 * Tilt configuration preferences. |
|
2146 */ |
|
2147 TiltVisualizer.Prefs = { |
|
2148 |
|
2149 /** |
|
2150 * Specifies if Tilt is enabled or not. |
|
2151 */ |
|
2152 get enabled() |
|
2153 { |
|
2154 return this._enabled; |
|
2155 }, |
|
2156 |
|
2157 set enabled(value) |
|
2158 { |
|
2159 TiltUtils.Preferences.set("enabled", "boolean", value); |
|
2160 this._enabled = value; |
|
2161 }, |
|
2162 |
|
2163 get introTransition() |
|
2164 { |
|
2165 return this._introTransition; |
|
2166 }, |
|
2167 |
|
2168 set introTransition(value) |
|
2169 { |
|
2170 TiltUtils.Preferences.set("intro_transition", "boolean", value); |
|
2171 this._introTransition = value; |
|
2172 }, |
|
2173 |
|
2174 get outroTransition() |
|
2175 { |
|
2176 return this._outroTransition; |
|
2177 }, |
|
2178 |
|
2179 set outroTransition(value) |
|
2180 { |
|
2181 TiltUtils.Preferences.set("outro_transition", "boolean", value); |
|
2182 this._outroTransition = value; |
|
2183 }, |
|
2184 |
|
2185 /** |
|
2186 * Loads the preferences. |
|
2187 */ |
|
2188 load: function TVC_load() |
|
2189 { |
|
2190 let prefs = TiltVisualizer.Prefs; |
|
2191 let get = TiltUtils.Preferences.get; |
|
2192 |
|
2193 prefs._enabled = get("enabled", "boolean"); |
|
2194 prefs._introTransition = get("intro_transition", "boolean"); |
|
2195 prefs._outroTransition = get("outro_transition", "boolean"); |
|
2196 } |
|
2197 }; |
|
2198 |
|
2199 /** |
|
2200 * A custom visualization shader. |
|
2201 * |
|
2202 * @param {Attribute} vertexPosition: the vertex position |
|
2203 * @param {Attribute} vertexTexCoord: texture coordinates used by the sampler |
|
2204 * @param {Attribute} vertexColor: specific [r, g, b] color for each vertex |
|
2205 * @param {Uniform} mvMatrix: the model view matrix |
|
2206 * @param {Uniform} projMatrix: the projection matrix |
|
2207 * @param {Uniform} sampler: the texture sampler to fetch the pixels from |
|
2208 */ |
|
2209 TiltVisualizer.MeshShader = { |
|
2210 |
|
2211 /** |
|
2212 * Vertex shader. |
|
2213 */ |
|
2214 vs: [ |
|
2215 "attribute vec3 vertexPosition;", |
|
2216 "attribute vec2 vertexTexCoord;", |
|
2217 "attribute vec3 vertexColor;", |
|
2218 |
|
2219 "uniform mat4 mvMatrix;", |
|
2220 "uniform mat4 projMatrix;", |
|
2221 |
|
2222 "varying vec2 texCoord;", |
|
2223 "varying vec3 color;", |
|
2224 |
|
2225 "void main() {", |
|
2226 " gl_Position = projMatrix * mvMatrix * vec4(vertexPosition, 1.0);", |
|
2227 " texCoord = vertexTexCoord;", |
|
2228 " color = vertexColor;", |
|
2229 "}" |
|
2230 ].join("\n"), |
|
2231 |
|
2232 /** |
|
2233 * Fragment shader. |
|
2234 */ |
|
2235 fs: [ |
|
2236 "#ifdef GL_ES", |
|
2237 "precision lowp float;", |
|
2238 "#endif", |
|
2239 |
|
2240 "uniform sampler2D sampler;", |
|
2241 |
|
2242 "varying vec2 texCoord;", |
|
2243 "varying vec3 color;", |
|
2244 |
|
2245 "void main() {", |
|
2246 " if (texCoord.x < 0.0) {", |
|
2247 " gl_FragColor = vec4(color, 1.0);", |
|
2248 " } else {", |
|
2249 " gl_FragColor = vec4(texture2D(sampler, texCoord).rgb, 1.0);", |
|
2250 " }", |
|
2251 "}" |
|
2252 ].join("\n") |
|
2253 }; |