| <!DOCTYPE html> |
| <!-- |
| 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. |
| --> |
| |
| <link rel="import" href="/core/draw_helpers.html"> |
| <link rel="import" href="/core/timeline_interest_range.html"> |
| <link rel="import" href="/core/timeline_display_transform.html"> |
| <link rel="import" href="/base/events.html"> |
| <link rel="import" href="/base/ui/animation.html"> |
| <link rel="import" href="/base/ui/animation_controller.html"> |
| <link rel="import" href="/base/ui/dom_helpers.html"> |
| |
| <script> |
| 'use strict'; |
| |
| /** |
| * @fileoverview Code for the viewport. |
| */ |
| tv.exportTo('tv.c', function() { |
| var TimelineDisplayTransform = tv.c.TimelineDisplayTransform; |
| var TimelineInterestRange = tv.c.TimelineInterestRange; |
| |
| /** |
| * ContainerToTrackObj is a class to handle building and accessing a map |
| * between an EventContainer's stableId and its handling track. |
| * |
| * @constructor |
| */ |
| function ContainerToTrackObj() { |
| this.stableIdToTrackMap_ = {}; |
| } |
| |
| ContainerToTrackObj.prototype = { |
| addContainer: function(container, track) { |
| if (!track) |
| throw new Error('Must provide a track.'); |
| this.stableIdToTrackMap_[container.stableId] = track; |
| }, |
| |
| clearMap: function() { |
| this.stableIdToTrackMap_ = {}; |
| }, |
| |
| getTrackByStableId: function(stableId) { |
| return this.stableIdToTrackMap_[stableId]; |
| } |
| }; |
| |
| /** |
| * 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 {tv.b.EventTarget} |
| */ |
| function TimelineViewport(parentEl) { |
| this.parentEl_ = parentEl; |
| this.modelTrackContainer_ = undefined; |
| this.currentDisplayTransform_ = new TimelineDisplayTransform(); |
| this.initAnimationController_(); |
| |
| // Flow events |
| this.showFlowEvents_ = false; |
| |
| // Highlights. |
| this.highlightVSync_ = false; |
| |
| // High details. |
| this.highDetails_ = false; |
| |
| // Grid system. |
| this.gridTimebase_ = 0; |
| this.gridStep_ = 1000 / 60; |
| this.gridEnabled_ = false; |
| |
| // Init logic. |
| this.hasCalledSetupFunction_ = false; |
| |
| this.onResize_ = this.onResize_.bind(this); |
| this.onModelTrackControllerScroll_ = |
| this.onModelTrackControllerScroll_.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.majorMarkPositions = []; |
| this.interestRange_ = new TimelineInterestRange(this); |
| |
| this.eventToTrackMap_ = {}; |
| this.containerToTrackObj = new ContainerToTrackObj(); |
| } |
| |
| TimelineViewport.prototype = { |
| __proto__: tv.b.EventTarget.prototype, |
| |
| /** |
| * 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 isAttachedToDocumentOrInTestMode() { |
| // Allow not providing a parent element, used by tests. |
| if (this.parentEl_ === undefined) |
| return; |
| return tv.b.ui.isElementAttachedToDocument(this.parentEl_); |
| }, |
| |
| 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.isAttachedToDocumentOrInTestMode || 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.onResize_); |
| } |
| |
| var curSize = this.parentEl_.clientWidth + 'x' + |
| this.parentEl_.clientHeight; |
| if (this.pendingSetFunction_) { |
| this.lastSize_ = curSize; |
| try { |
| this.pendingSetFunction_(); |
| } catch (ex) { |
| console.log('While running setWhenPossible:', |
| ex.message ? ex.message + '\n' + ex.stack : ex.stack); |
| } |
| 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() { |
| tv.b.dispatchSimpleEvent(this, 'change'); |
| }, |
| |
| detach: function() { |
| if (this.checkForAttachInterval_) { |
| window.clearInterval(this.checkForAttachInterval_); |
| this.checkForAttachInterval_ = undefined; |
| } |
| if (this.iframe_) { |
| this.iframe_.removeEventListener('resize', this.onResize_); |
| this.parentEl_.removeChild(this.iframe_); |
| } |
| }, |
| |
| initAnimationController_: function() { |
| this.dtAnimationController_ = new tv.b.ui.AnimationController(); |
| this.dtAnimationController_.addEventListener( |
| 'didtick', function(e) { |
| this.onCurentDisplayTransformChange_(e.oldTargetState); |
| }.bind(this)); |
| |
| var that = this; |
| this.dtAnimationController_.target = { |
| get panX() { |
| return that.currentDisplayTransform_.panX; |
| }, |
| |
| set panX(panX) { |
| that.currentDisplayTransform_.panX = panX; |
| }, |
| |
| get panY() { |
| return that.currentDisplayTransform_.panY; |
| }, |
| |
| set panY(panY) { |
| that.currentDisplayTransform_.panY = panY; |
| }, |
| |
| get scaleX() { |
| return that.currentDisplayTransform_.scaleX; |
| }, |
| |
| set scaleX(scaleX) { |
| that.currentDisplayTransform_.scaleX = scaleX; |
| }, |
| |
| cloneAnimationState: function() { |
| return that.currentDisplayTransform_.clone(); |
| }, |
| |
| xPanWorldPosToViewPos: function(xWorld, xView) { |
| that.currentDisplayTransform_.xPanWorldPosToViewPos( |
| xWorld, xView, that.modelTrackContainer_.canvas.clientWidth); |
| } |
| }; |
| }, |
| |
| get currentDisplayTransform() { |
| return this.currentDisplayTransform_; |
| }, |
| |
| setDisplayTransformImmediately: function(displayTransform) { |
| this.dtAnimationController_.cancelActiveAnimation(); |
| |
| var oldDisplayTransform = |
| this.dtAnimationController_.target.cloneAnimationState(); |
| this.currentDisplayTransform_.set(displayTransform); |
| this.onCurentDisplayTransformChange_(oldDisplayTransform); |
| }, |
| |
| queueDisplayTransformAnimation: function(animation) { |
| if (!(animation instanceof tv.b.ui.Animation)) |
| throw new Error('animation must be instanceof tv.b.ui.Animation'); |
| this.dtAnimationController_.queueAnimation(animation); |
| }, |
| |
| onCurentDisplayTransformChange_: function(oldDisplayTransform) { |
| // Ensure panY stays clamped in the track container's scroll range. |
| if (this.modelTrackContainer_) { |
| this.currentDisplayTransform.panY = tv.b.clamp( |
| this.currentDisplayTransform.panY, |
| 0, |
| this.modelTrackContainer_.scrollHeight - |
| this.modelTrackContainer_.clientHeight); |
| } |
| |
| var changed = !this.currentDisplayTransform.equals(oldDisplayTransform); |
| var yChanged = this.currentDisplayTransform.panY !== |
| oldDisplayTransform.panY; |
| if (yChanged) |
| this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY; |
| if (changed) |
| this.dispatchChangeEvent(); |
| }, |
| |
| onModelTrackControllerScroll_: function(e) { |
| if (this.dtAnimationController_.activeAnimation && |
| this.dtAnimationController_.activeAnimation.affectsPanY) |
| this.dtAnimationController_.cancelActiveAnimation(); |
| var panY = this.modelTrackContainer_.scrollTop; |
| this.currentDisplayTransform_.panY = panY; |
| }, |
| |
| get modelTrackContainer() { |
| return this.modelTrackContainer_; |
| }, |
| |
| set modelTrackContainer(m) { |
| if (this.modelTrackContainer_) |
| this.modelTrackContainer_.removeEventListener('scroll', |
| this.onModelTrackControllerScroll_); |
| |
| this.modelTrackContainer_ = m; |
| this.modelTrackContainer_.addEventListener('scroll', |
| this.onModelTrackControllerScroll_); |
| }, |
| |
| get showFlowEvents() { |
| return this.showFlowEvents_; |
| }, |
| |
| set showFlowEvents(showFlowEvents) { |
| this.showFlowEvents_ = showFlowEvents; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get highlightVSync() { |
| return this.highlightVSync_; |
| }, |
| |
| set highlightVSync(highlightVSync) { |
| this.highlightVSync_ = highlightVSync; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get highDetails() { |
| return this.highDetails_; |
| }, |
| |
| set highDetails(highDetails) { |
| this.highDetails_ = highDetails; |
| this.dispatchChangeEvent(); |
| }, |
| |
| 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; |
| this.dispatchChangeEvent(); |
| }, |
| |
| get gridStep() { |
| return this.gridStep_; |
| }, |
| |
| get interestRange() { |
| return this.interestRange_; |
| }, |
| |
| drawMajorMarkLines: function(ctx) { |
| // Apply subpixel translate to get crisp lines. |
| // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| ctx.save(); |
| ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); |
| |
| ctx.beginPath(); |
| for (var idx in this.majorMarkPositions) { |
| var x = Math.floor(this.majorMarkPositions[idx]); |
| tv.c.drawLine(ctx, x, 0, x, ctx.canvas.height); |
| } |
| ctx.strokeStyle = '#ddd'; |
| ctx.stroke(); |
| |
| ctx.restore(); |
| }, |
| |
| drawGridLines: function(ctx, viewLWorld, viewRWorld) { |
| if (!this.gridEnabled) |
| return; |
| |
| var dt = this.currentDisplayTransform; |
| var x = this.gridTimebase; |
| |
| // Apply subpixel translate to get crisp lines. |
| // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| ctx.save(); |
| ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); |
| |
| ctx.beginPath(); |
| while (x < viewRWorld) { |
| if (x >= viewLWorld) { |
| // Do conversion to viewspace here rather than on |
| // x to avoid precision issues. |
| var vx = Math.floor(dt.xWorldToView(x)); |
| tv.c.drawLine(ctx, vx, 0, vx, ctx.canvas.height); |
| } |
| |
| x += this.gridStep; |
| } |
| ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)'; |
| ctx.stroke(); |
| |
| ctx.restore(); |
| }, |
| |
| rebuildEventToTrackMap: function() { |
| this.eventToTrackMap_ = undefined; |
| |
| var eventToTrackMap = {}; |
| eventToTrackMap.addEvent = function(event, track) { |
| if (!track) |
| throw new Error('Must provide a track.'); |
| this[event.guid] = track; |
| }; |
| this.modelTrackContainer_.addEventsToTrackMap(eventToTrackMap); |
| this.eventToTrackMap_ = eventToTrackMap; |
| }, |
| |
| rebuildContainerToTrackMap: function() { |
| this.containerToTrackObj.clearMap(); |
| this.modelTrackContainer_.addContainersToTrackMap( |
| this.containerToTrackObj); |
| }, |
| |
| trackForEvent: function(event) { |
| return this.eventToTrackMap_[event.guid]; |
| } |
| }; |
| |
| return { |
| ContainerToTrackObj: ContainerToTrackObj, |
| TimelineViewport: TimelineViewport |
| }; |
| }); |
| </script> |