| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * @fileoverview Code for the timeline viewport. |
| */ |
| base.require('event_target'); |
| |
| base.exportTo('tracing', function() { |
| |
| /** |
| * The TimelineViewport manages the transform used for navigating |
| * within the timeline. It is a simple transform: |
| * x' = (x+pan) * scale |
| * |
| * The timeline code tries to avoid directly accessing this transform, |
| * instead using this class to do conversion between world and viewspace, |
| * as well as the math for centering the viewport in various interesting |
| * ways. |
| * |
| * @constructor |
| * @extends {base.EventTarget} |
| */ |
| function TimelineViewport(parentEl) { |
| this.parentEl_ = parentEl; |
| this.scaleX_ = 1; |
| this.panX_ = 0; |
| this.gridTimebase_ = 0; |
| this.gridStep_ = 1000 / 60; |
| this.gridEnabled_ = false; |
| this.hasCalledSetupFunction_ = false; |
| |
| this.onResizeBoundToThis_ = this.onResize_.bind(this); |
| |
| // The following code uses an interval to detect when the parent element |
| // is attached to the document. That is a trigger to run the setup function |
| // and install a resize listener. |
| this.checkForAttachInterval_ = setInterval( |
| this.checkForAttach_.bind(this), 250); |
| |
| this.markers = []; |
| } |
| |
| TimelineViewport.prototype = { |
| __proto__: base.EventTarget.prototype, |
| |
| drawUnderContent: function(ctx, viewLWorld, viewRWorld, canvasH) { |
| }, |
| |
| drawOverContent: function(ctx, viewLWorld, viewRWorld, canvasH) { |
| if (this.gridEnabled) { |
| var x = this.gridTimebase; |
| |
| ctx.beginPath(); |
| while (x < viewRWorld) { |
| if (x >= viewLWorld) { |
| // Do conversion to viewspace here rather than on |
| // x to avoid precision issues. |
| var vx = this.xWorldToView(x); |
| ctx.moveTo(vx, 0); |
| ctx.lineTo(vx, canvasH); |
| } |
| x += this.gridStep; |
| } |
| ctx.strokeStyle = 'rgba(255,0,0,0.25)'; |
| ctx.stroke(); |
| } |
| |
| for (var i = 0; i < this.markers.length; ++i) { |
| this.markers[i].drawLine(ctx, viewLWorld, viewRWorld, canvasH, this); |
| } |
| }, |
| |
| /** |
| * Allows initialization of the viewport when the viewport's parent element |
| * has been attached to the document and given a size. |
| * @param {Function} fn Function to call when the viewport can be safely |
| * initialized. |
| */ |
| setWhenPossible: function(fn) { |
| this.pendingSetFunction_ = fn; |
| }, |
| |
| /** |
| * @return {boolean} Whether the current timeline is attached to the |
| * document. |
| */ |
| get isAttachedToDocument_() { |
| var cur = this.parentEl_; |
| while (cur.parentNode) |
| cur = cur.parentNode; |
| return cur == this.parentEl_.ownerDocument; |
| }, |
| |
| onResize_: function() { |
| this.dispatchChangeEvent(); |
| }, |
| |
| /** |
| * Checks whether the parentNode is attached to the document. |
| * When it is, it installs the iframe-based resize detection hook |
| * and then runs the pendingSetFunction_, if present. |
| */ |
| checkForAttach_: function() { |
| if (!this.isAttachedToDocument_ || this.clientWidth == 0) |
| return; |
| |
| if (!this.iframe_) { |
| this.iframe_ = document.createElement('iframe'); |
| this.iframe_.style.cssText = |
| 'position:absolute;width:100%;height:0;border:0;visibility:hidden;'; |
| this.parentEl_.appendChild(this.iframe_); |
| |
| this.iframe_.contentWindow.addEventListener('resize', |
| this.onResizeBoundToThis_); |
| } |
| |
| var curSize = this.clientWidth + 'x' + this.clientHeight; |
| if (this.pendingSetFunction_) { |
| this.lastSize_ = curSize; |
| try { |
| this.pendingSetFunction_(); |
| } catch (ex) { |
| console.log('While running setWhenPossible:', ex); |
| } |
| this.pendingSetFunction_ = undefined; |
| } |
| |
| window.clearInterval(this.checkForAttachInterval_); |
| this.checkForAttachInterval_ = undefined; |
| }, |
| |
| /** |
| * Fires the change event on this viewport. Used to notify listeners |
| * to redraw when the underlying model has been mutated. |
| */ |
| dispatchChangeEvent: function() { |
| base.dispatchSimpleEvent(this, 'change'); |
| }, |
| |
| dispatchMarkersChangeEvent_: function() { |
| base.dispatchSimpleEvent(this, 'markersChange'); |
| }, |
| |
| detach: function() { |
| if (this.checkForAttachInterval_) { |
| window.clearInterval(this.checkForAttachInterval_); |
| this.checkForAttachInterval_ = undefined; |
| } |
| if (this.iframe_) { |
| this.iframe_.removeEventListener('resize', this.onResizeBoundToThis_); |
| this.parentEl_.removeChild(this.iframe_); |
| } |
| }, |
| |
| get scaleX() { |
| return this.scaleX_; |
| }, |
| set scaleX(s) { |
| var changed = this.scaleX_ != s; |
| if (changed) { |
| this.scaleX_ = s; |
| this.dispatchChangeEvent(); |
| } |
| }, |
| |
| get panX() { |
| return this.panX_; |
| }, |
| set panX(p) { |
| var changed = this.panX_ != p; |
| if (changed) { |
| this.panX_ = p; |
| this.dispatchChangeEvent(); |
| } |
| }, |
| |
| setPanAndScale: function(p, s) { |
| var changed = this.scaleX_ != s || this.panX_ != p; |
| if (changed) { |
| this.scaleX_ = s; |
| this.panX_ = p; |
| this.dispatchChangeEvent(); |
| } |
| }, |
| |
| xWorldToView: function(x) { |
| return (x + this.panX_) * this.scaleX_; |
| }, |
| |
| xWorldVectorToView: function(x) { |
| return x * this.scaleX_; |
| }, |
| |
| xViewToWorld: function(x) { |
| return (x / this.scaleX_) - this.panX_; |
| }, |
| |
| xViewVectorToWorld: function(x) { |
| return x / this.scaleX_; |
| }, |
| |
| xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) { |
| if (typeof viewX == 'string') { |
| if (viewX == 'left') { |
| viewX = 0; |
| } else if (viewX == 'center') { |
| viewX = viewWidth / 2; |
| } else if (viewX == 'right') { |
| viewX = viewWidth - 1; |
| } else { |
| throw new Error('unrecognized string for viewPos. left|center|right'); |
| } |
| } |
| this.panX = (viewX / this.scaleX_) - worldX; |
| }, |
| |
| xPanWorldRangeIntoView: function(worldMin, worldMax, viewWidth) { |
| if (this.xWorldToView(worldMin) < 0) |
| this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth); |
| else if (this.xWorldToView(worldMax) > viewWidth) |
| this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth); |
| }, |
| |
| xSetWorldRange: function(worldMin, worldMax, viewWidth) { |
| var worldRange = worldMax - worldMin; |
| var scaleX = viewWidth / worldRange; |
| var panX = -worldMin; |
| this.setPanAndScale(panX, scaleX); |
| }, |
| |
| get gridEnabled() { |
| return this.gridEnabled_; |
| }, |
| |
| set gridEnabled(enabled) { |
| if (this.gridEnabled_ == enabled) |
| return; |
| this.gridEnabled_ = enabled && true; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get gridTimebase() { |
| return this.gridTimebase_; |
| }, |
| |
| set gridTimebase(timebase) { |
| if (this.gridTimebase_ == timebase) |
| return; |
| this.gridTimebase_ = timebase; |
| base.dispatchSimpleEvent(this, 'change'); |
| }, |
| |
| get gridStep() { |
| return this.gridStep_; |
| }, |
| |
| applyTransformToCanvas: function(ctx) { |
| ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0); |
| }, |
| |
| addMarker: function(positionWorld) { |
| var marker = new TimelineViewportMarker(this, positionWorld); |
| this.markers.push(marker); |
| this.dispatchChangeEvent(); |
| this.dispatchMarkersChangeEvent_(); |
| return marker; |
| }, |
| |
| removeMarker: function(marker) { |
| for (var i = 0; i < this.markers.length; ++i) { |
| if (this.markers[i] === marker) { |
| this.markers.splice(i, 1); |
| this.dispatchChangeEvent(); |
| this.dispatchMarkersChangeEvent_(); |
| return true; |
| } |
| } |
| }, |
| |
| findMarkerNear: function(positionWorld, nearnessInViewPixels) { |
| // Converts pixels into distance in world. |
| var nearnessThresholdWorld = this.xViewVectorToWorld( |
| nearnessInViewPixels); |
| for (var i = 0; i < this.markers.length; ++i) { |
| if (Math.abs(this.markers[i].positionWorld - positionWorld) <= |
| nearnessThresholdWorld) { |
| var marker = this.markers[i]; |
| return marker; |
| } |
| } |
| return undefined; |
| } |
| }; |
| |
| /** |
| * Represents a marked position in the world, at a viewport level. |
| * @constructor |
| */ |
| function TimelineViewportMarker(vp, positionWorld) { |
| this.viewport_ = vp; |
| this.positionWorld_ = positionWorld; |
| this.selected_ = false; |
| } |
| |
| TimelineViewportMarker.prototype = { |
| get positionWorld() { |
| return this.positionWorld_; |
| }, |
| |
| set positionWorld(positionWorld) { |
| this.positionWorld_ = positionWorld; |
| this.viewport_.dispatchChangeEvent(); |
| }, |
| |
| set selected(selected) { |
| this.selected_ = selected; |
| this.viewport_.dispatchChangeEvent(); |
| }, |
| |
| get selected() { |
| return this.selected_; |
| }, |
| |
| get color() { |
| if (this.selected) |
| return 'rgb(255,0,0)'; |
| return 'rgb(0,0,0)'; |
| }, |
| |
| drawTriangle_: function(ctx, viewLWorld, viewRWorld, |
| canvasH, rulerHeight, vp) { |
| ctx.beginPath(); |
| var ts = this.positionWorld_; |
| if (ts >= viewLWorld && ts < viewRWorld) { |
| var viewX = vp.xWorldToView(ts); |
| ctx.moveTo(viewX, rulerHeight); |
| ctx.lineTo(viewX - 3, rulerHeight / 2); |
| ctx.lineTo(viewX + 3, rulerHeight / 2); |
| ctx.lineTo(viewX, rulerHeight); |
| ctx.closePath(); |
| ctx.fillStyle = this.color; |
| ctx.fill(); |
| if (rulerHeight != canvasH) { |
| ctx.beginPath(); |
| ctx.moveTo(viewX, rulerHeight); |
| ctx.lineTo(viewX, canvasH); |
| ctx.closePath(); |
| ctx.strokeStyle = this.color; |
| ctx.stroke(); |
| } |
| } |
| }, |
| |
| drawLine: function(ctx, viewLWorld, viewRWorld, canvasH, vp) { |
| ctx.beginPath(); |
| var ts = this.positionWorld_; |
| if (ts >= viewLWorld && ts < viewRWorld) { |
| var viewX = vp.xWorldToView(ts); |
| ctx.moveTo(viewX, 0); |
| ctx.lineTo(viewX, canvasH); |
| } |
| ctx.strokeStyle = this.color; |
| ctx.stroke(); |
| } |
| }; |
| |
| return { |
| TimelineViewport: TimelineViewport, |
| TimelineViewportMarker: TimelineViewportMarker |
| }; |
| }); |