1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * @fileoverview Code for the viewport.
10 tvcm.require('tvcm.events');
11 tvcm.require('tracing.draw_helpers');
12 tvcm.require('tracing.timeline_interest_range');
13 tvcm.require('tracing.timeline_display_transform');
14 tvcm.require('tvcm.ui.animation');
15 tvcm.require('tvcm.ui.animation_controller');
17 tvcm.exportTo('tracing', function() {
19 var TimelineDisplayTransform = tracing.TimelineDisplayTransform;
20 var TimelineInterestRange = tracing.TimelineInterestRange;
23 * The TimelineViewport manages the transform used for navigating
24 * within the timeline. It is a simple transform:
25 * x' = (x+pan) * scale
27 * The timeline code tries to avoid directly accessing this transform,
28 * instead using this class to do conversion between world and viewspace,
29 * as well as the math for centering the viewport in various interesting
33 * @extends {tvcm.EventTarget}
35 function TimelineViewport(parentEl) {
36 this.parentEl_ = parentEl;
37 this.modelTrackContainer_ = undefined;
38 this.currentDisplayTransform_ = new TimelineDisplayTransform();
39 this.initAnimationController_();
42 this.gridTimebase_ = 0;
43 this.gridStep_ = 1000 / 60;
44 this.gridEnabled_ = false;
47 this.hasCalledSetupFunction_ = false;
49 this.onResize_ = this.onResize_.bind(this);
50 this.onModelTrackControllerScroll_ =
51 this.onModelTrackControllerScroll_.bind(this);
53 // The following code uses an interval to detect when the parent element
54 // is attached to the document. That is a trigger to run the setup function
55 // and install a resize listener.
56 this.checkForAttachInterval_ = setInterval(
57 this.checkForAttach_.bind(this), 250);
59 this.majorMarkPositions = [];
60 this.interestRange_ = new TimelineInterestRange(this);
62 this.eventToTrackMap_ = {};
65 TimelineViewport.prototype = {
66 __proto__: tvcm.EventTarget.prototype,
69 * Allows initialization of the viewport when the viewport's parent element
70 * has been attached to the document and given a size.
71 * @param {Function} fn Function to call when the viewport can be safely
74 setWhenPossible: function(fn) {
75 this.pendingSetFunction_ = fn;
79 * @return {boolean} Whether the current timeline is attached to the
82 get isAttachedToDocumentOrInTestMode() {
83 // Allow not providing a parent element, used by tests.
84 if (this.parentEl_ === undefined)
86 return tvcm.ui.isElementAttachedToDocument(this.parentEl_);
89 onResize_: function() {
90 this.dispatchChangeEvent();
94 * Checks whether the parentNode is attached to the document.
95 * When it is, it installs the iframe-based resize detection hook
96 * and then runs the pendingSetFunction_, if present.
98 checkForAttach_: function() {
99 if (!this.isAttachedToDocumentOrInTestMode || this.clientWidth == 0)
103 this.iframe_ = document.createElement('iframe');
104 this.iframe_.style.cssText =
105 'position:absolute;width:100%;height:0;border:0;visibility:hidden;';
106 this.parentEl_.appendChild(this.iframe_);
108 this.iframe_.contentWindow.addEventListener('resize', this.onResize_);
111 var curSize = this.parentEl_.clientWidth + 'x' +
112 this.parentEl_.clientHeight;
113 if (this.pendingSetFunction_) {
114 this.lastSize_ = curSize;
116 this.pendingSetFunction_();
118 console.log('While running setWhenPossible:',
119 ex.message ? ex.message + '\n' + ex.stack : ex.stack);
121 this.pendingSetFunction_ = undefined;
124 window.clearInterval(this.checkForAttachInterval_);
125 this.checkForAttachInterval_ = undefined;
129 * Fires the change event on this viewport. Used to notify listeners
130 * to redraw when the underlying model has been mutated.
132 dispatchChangeEvent: function() {
133 tvcm.dispatchSimpleEvent(this, 'change');
137 if (this.checkForAttachInterval_) {
138 window.clearInterval(this.checkForAttachInterval_);
139 this.checkForAttachInterval_ = undefined;
142 this.iframe_.removeEventListener('resize', this.onResize_);
143 this.parentEl_.removeChild(this.iframe_);
147 initAnimationController_: function() {
148 this.dtAnimationController_ = new tvcm.ui.AnimationController();
149 this.dtAnimationController_.addEventListener(
150 'didtick', function(e) {
151 this.onCurentDisplayTransformChange_(e.oldTargetState);
155 this.dtAnimationController_.target = {
157 return that.currentDisplayTransform_.panX;
161 that.currentDisplayTransform_.panX = panX;
165 return that.currentDisplayTransform_.panY;
169 that.currentDisplayTransform_.panY = panY;
173 return that.currentDisplayTransform_.scaleX;
177 that.currentDisplayTransform_.scaleX = scaleX;
180 cloneAnimationState: function() {
181 return that.currentDisplayTransform_.clone();
184 xPanWorldPosToViewPos: function(xWorld, xView) {
185 that.currentDisplayTransform_.xPanWorldPosToViewPos(
186 xWorld, xView, that.modelTrackContainer_.canvas.clientWidth);
191 get currentDisplayTransform() {
192 return this.currentDisplayTransform_;
195 setDisplayTransformImmediately: function(displayTransform) {
196 this.dtAnimationController_.cancelActiveAnimation();
198 var oldDisplayTransform =
199 this.dtAnimationController_.target.cloneAnimationState();
200 this.currentDisplayTransform_.set(displayTransform);
201 this.onCurentDisplayTransformChange_(oldDisplayTransform);
204 queueDisplayTransformAnimation: function(animation) {
205 if (!(animation instanceof tvcm.ui.Animation))
206 throw new Error('animation must be instanceof tvcm.ui.Animation');
207 this.dtAnimationController_.queueAnimation(animation);
210 onCurentDisplayTransformChange_: function(oldDisplayTransform) {
211 // Ensure panY stays clamped in the track container's scroll range.
212 if (this.modelTrackContainer_) {
213 this.currentDisplayTransform.panY = tvcm.clamp(
214 this.currentDisplayTransform.panY,
216 this.modelTrackContainer_.scrollHeight -
217 this.modelTrackContainer_.clientHeight);
220 var changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
221 var yChanged = this.currentDisplayTransform.panY !==
222 oldDisplayTransform.panY;
224 this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
226 this.dispatchChangeEvent();
229 onModelTrackControllerScroll_: function(e) {
230 if (this.dtAnimationController_.activeAnimation &&
231 this.dtAnimationController_.activeAnimation.affectsPanY)
232 this.dtAnimationController_.cancelActiveAnimation();
233 var panY = this.modelTrackContainer_.scrollTop;
234 this.currentDisplayTransform_.panY = panY;
237 get modelTrackContainer() {
238 return this.modelTrackContainer_;
241 set modelTrackContainer(m) {
242 if (this.modelTrackContainer_)
243 this.modelTrackContainer_.removeEventListener('scroll',
244 this.onModelTrackControllerScroll_);
246 this.modelTrackContainer_ = m;
247 this.modelTrackContainer_.addEventListener('scroll',
248 this.onModelTrackControllerScroll_);
252 return this.gridEnabled_;
255 set gridEnabled(enabled) {
256 if (this.gridEnabled_ == enabled)
259 this.gridEnabled_ = enabled && true;
260 this.dispatchChangeEvent();
264 return this.gridTimebase_;
267 set gridTimebase(timebase) {
268 if (this.gridTimebase_ == timebase)
270 this.gridTimebase_ = timebase;
271 this.dispatchChangeEvent();
275 return this.gridStep_;
278 get interestRange() {
279 return this.interestRange_;
282 drawMajorMarkLines: function(ctx) {
283 // Apply subpixel translate to get crisp lines.
284 // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
286 ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
289 for (var idx in this.majorMarkPositions) {
290 var x = Math.floor(this.majorMarkPositions[idx]);
291 tracing.drawLine(ctx, x, 0, x, ctx.canvas.height);
293 ctx.strokeStyle = '#ddd';
299 drawGridLines: function(ctx, viewLWorld, viewRWorld) {
300 if (!this.gridEnabled)
303 var dt = this.currentDisplayTransform;
304 var x = this.gridTimebase;
306 // Apply subpixel translate to get crisp lines.
307 // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
309 ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
312 while (x < viewRWorld) {
313 if (x >= viewLWorld) {
314 // Do conversion to viewspace here rather than on
315 // x to avoid precision issues.
316 var vx = Math.floor(dt.xWorldToView(x));
317 tracing.drawLine(ctx, vx, 0, vx, ctx.canvas.height);
322 ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
328 rebuildEventToTrackMap: function() {
329 this.eventToTrackMap_ = undefined;
331 var eventToTrackMap = {};
332 eventToTrackMap.addEvent = function(event, track) {
334 throw new Error('Must provide a track.');
335 this[event.guid] = track;
337 this.modelTrackContainer_.addEventsToTrackMap(eventToTrackMap);
338 this.eventToTrackMap_ = eventToTrackMap;
341 trackForEvent: function(event) {
342 return this.eventToTrackMap_[event.guid];
347 TimelineViewport: TimelineViewport