// 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 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; }, xPanWorldBoundsIntoView: 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); }, xSetWorldBounds: function(worldMin, worldMax, viewWidth) { var worldWidth = worldMax - worldMin; var scaleX = viewWidth / worldWidth; 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 ViewportMarker(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 ViewportMarker(vp, positionWorld) { this.viewport_ = vp; this.positionWorld_ = positionWorld; this.selected_ = false; } ViewportMarker.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, ViewportMarker: ViewportMarker }; });