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