blob: d843b1dd991984de6e98c2d36dcc6b559a740fbc [file] [log] [blame]
Chris Craikb122baf2015-03-05 13:58:42 -08001<!DOCTYPE html>
2<!--
3Copyright (c) 2012 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/core/draw_helpers.html">
9<link rel="import" href="/core/timeline_interest_range.html">
10<link rel="import" href="/core/timeline_display_transform.html">
11<link rel="import" href="/base/events.html">
12<link rel="import" href="/base/ui/animation.html">
13<link rel="import" href="/base/ui/animation_controller.html">
14<link rel="import" href="/base/ui/dom_helpers.html">
15
16<script>
17'use strict';
18
19/**
20 * @fileoverview Code for the viewport.
21 */
22tv.exportTo('tv.c', function() {
23 var TimelineDisplayTransform = tv.c.TimelineDisplayTransform;
24 var TimelineInterestRange = tv.c.TimelineInterestRange;
25
26 /**
27 * ContainerToTrackObj is a class to handle building and accessing a map
28 * between an EventContainer's stableId and its handling track.
29 *
30 * @constructor
31 */
32 function ContainerToTrackObj() {
33 this.stableIdToTrackMap_ = {};
34 }
35
36 ContainerToTrackObj.prototype = {
37 addContainer: function(container, track) {
38 if (!track)
39 throw new Error('Must provide a track.');
40 this.stableIdToTrackMap_[container.stableId] = track;
41 },
42
43 clearMap: function() {
44 this.stableIdToTrackMap_ = {};
45 },
46
47 getTrackByStableId: function(stableId) {
48 return this.stableIdToTrackMap_[stableId];
49 }
50 };
51
52 /**
53 * The TimelineViewport manages the transform used for navigating
54 * within the timeline. It is a simple transform:
55 * x' = (x+pan) * scale
56 *
57 * The timeline code tries to avoid directly accessing this transform,
58 * instead using this class to do conversion between world and viewspace,
59 * as well as the math for centering the viewport in various interesting
60 * ways.
61 *
62 * @constructor
63 * @extends {tv.b.EventTarget}
64 */
65 function TimelineViewport(parentEl) {
66 this.parentEl_ = parentEl;
67 this.modelTrackContainer_ = undefined;
68 this.currentDisplayTransform_ = new TimelineDisplayTransform();
69 this.initAnimationController_();
70
71 // Flow events
72 this.showFlowEvents_ = false;
73
74 // Highlights.
75 this.highlightVSync_ = false;
76
77 // High details.
78 this.highDetails_ = false;
79
80 // Grid system.
81 this.gridTimebase_ = 0;
82 this.gridStep_ = 1000 / 60;
83 this.gridEnabled_ = false;
84
85 // Init logic.
86 this.hasCalledSetupFunction_ = false;
87
88 this.onResize_ = this.onResize_.bind(this);
89 this.onModelTrackControllerScroll_ =
90 this.onModelTrackControllerScroll_.bind(this);
91
92 // The following code uses an interval to detect when the parent element
93 // is attached to the document. That is a trigger to run the setup function
94 // and install a resize listener.
95 this.checkForAttachInterval_ = setInterval(
96 this.checkForAttach_.bind(this), 250);
97
98 this.majorMarkPositions = [];
99 this.interestRange_ = new TimelineInterestRange(this);
100
101 this.eventToTrackMap_ = {};
102 this.containerToTrackObj = new ContainerToTrackObj();
103 }
104
105 TimelineViewport.prototype = {
106 __proto__: tv.b.EventTarget.prototype,
107
108 /**
109 * Allows initialization of the viewport when the viewport's parent element
110 * has been attached to the document and given a size.
111 * @param {Function} fn Function to call when the viewport can be safely
112 * initialized.
113 */
114 setWhenPossible: function(fn) {
115 this.pendingSetFunction_ = fn;
116 },
117
118 /**
119 * @return {boolean} Whether the current timeline is attached to the
120 * document.
121 */
122 get isAttachedToDocumentOrInTestMode() {
123 // Allow not providing a parent element, used by tests.
124 if (this.parentEl_ === undefined)
125 return;
126 return tv.b.ui.isElementAttachedToDocument(this.parentEl_);
127 },
128
129 onResize_: function() {
130 this.dispatchChangeEvent();
131 },
132
133 /**
134 * Checks whether the parentNode is attached to the document.
135 * When it is, it installs the iframe-based resize detection hook
136 * and then runs the pendingSetFunction_, if present.
137 */
138 checkForAttach_: function() {
139 if (!this.isAttachedToDocumentOrInTestMode || this.clientWidth == 0)
140 return;
141
142 if (!this.iframe_) {
143 this.iframe_ = document.createElement('iframe');
144 this.iframe_.style.cssText =
145 'position:absolute;width:100%;height:0;border:0;visibility:hidden;';
146 this.parentEl_.appendChild(this.iframe_);
147
148 this.iframe_.contentWindow.addEventListener('resize', this.onResize_);
149 }
150
151 var curSize = this.parentEl_.clientWidth + 'x' +
152 this.parentEl_.clientHeight;
153 if (this.pendingSetFunction_) {
154 this.lastSize_ = curSize;
155 try {
156 this.pendingSetFunction_();
157 } catch (ex) {
158 console.log('While running setWhenPossible:',
159 ex.message ? ex.message + '\n' + ex.stack : ex.stack);
160 }
161 this.pendingSetFunction_ = undefined;
162 }
163
164 window.clearInterval(this.checkForAttachInterval_);
165 this.checkForAttachInterval_ = undefined;
166 },
167
168 /**
169 * Fires the change event on this viewport. Used to notify listeners
170 * to redraw when the underlying model has been mutated.
171 */
172 dispatchChangeEvent: function() {
173 tv.b.dispatchSimpleEvent(this, 'change');
174 },
175
176 detach: function() {
177 if (this.checkForAttachInterval_) {
178 window.clearInterval(this.checkForAttachInterval_);
179 this.checkForAttachInterval_ = undefined;
180 }
181 if (this.iframe_) {
182 this.iframe_.removeEventListener('resize', this.onResize_);
183 this.parentEl_.removeChild(this.iframe_);
184 }
185 },
186
187 initAnimationController_: function() {
188 this.dtAnimationController_ = new tv.b.ui.AnimationController();
189 this.dtAnimationController_.addEventListener(
190 'didtick', function(e) {
191 this.onCurentDisplayTransformChange_(e.oldTargetState);
192 }.bind(this));
193
194 var that = this;
195 this.dtAnimationController_.target = {
196 get panX() {
197 return that.currentDisplayTransform_.panX;
198 },
199
200 set panX(panX) {
201 that.currentDisplayTransform_.panX = panX;
202 },
203
204 get panY() {
205 return that.currentDisplayTransform_.panY;
206 },
207
208 set panY(panY) {
209 that.currentDisplayTransform_.panY = panY;
210 },
211
212 get scaleX() {
213 return that.currentDisplayTransform_.scaleX;
214 },
215
216 set scaleX(scaleX) {
217 that.currentDisplayTransform_.scaleX = scaleX;
218 },
219
220 cloneAnimationState: function() {
221 return that.currentDisplayTransform_.clone();
222 },
223
224 xPanWorldPosToViewPos: function(xWorld, xView) {
225 that.currentDisplayTransform_.xPanWorldPosToViewPos(
226 xWorld, xView, that.modelTrackContainer_.canvas.clientWidth);
227 }
228 };
229 },
230
231 get currentDisplayTransform() {
232 return this.currentDisplayTransform_;
233 },
234
235 setDisplayTransformImmediately: function(displayTransform) {
236 this.dtAnimationController_.cancelActiveAnimation();
237
238 var oldDisplayTransform =
239 this.dtAnimationController_.target.cloneAnimationState();
240 this.currentDisplayTransform_.set(displayTransform);
241 this.onCurentDisplayTransformChange_(oldDisplayTransform);
242 },
243
244 queueDisplayTransformAnimation: function(animation) {
245 if (!(animation instanceof tv.b.ui.Animation))
246 throw new Error('animation must be instanceof tv.b.ui.Animation');
247 this.dtAnimationController_.queueAnimation(animation);
248 },
249
250 onCurentDisplayTransformChange_: function(oldDisplayTransform) {
251 // Ensure panY stays clamped in the track container's scroll range.
252 if (this.modelTrackContainer_) {
253 this.currentDisplayTransform.panY = tv.b.clamp(
254 this.currentDisplayTransform.panY,
255 0,
256 this.modelTrackContainer_.scrollHeight -
257 this.modelTrackContainer_.clientHeight);
258 }
259
260 var changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
261 var yChanged = this.currentDisplayTransform.panY !==
262 oldDisplayTransform.panY;
263 if (yChanged)
264 this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
265 if (changed)
266 this.dispatchChangeEvent();
267 },
268
269 onModelTrackControllerScroll_: function(e) {
270 if (this.dtAnimationController_.activeAnimation &&
271 this.dtAnimationController_.activeAnimation.affectsPanY)
272 this.dtAnimationController_.cancelActiveAnimation();
273 var panY = this.modelTrackContainer_.scrollTop;
274 this.currentDisplayTransform_.panY = panY;
275 },
276
277 get modelTrackContainer() {
278 return this.modelTrackContainer_;
279 },
280
281 set modelTrackContainer(m) {
282 if (this.modelTrackContainer_)
283 this.modelTrackContainer_.removeEventListener('scroll',
284 this.onModelTrackControllerScroll_);
285
286 this.modelTrackContainer_ = m;
287 this.modelTrackContainer_.addEventListener('scroll',
288 this.onModelTrackControllerScroll_);
289 },
290
291 get showFlowEvents() {
292 return this.showFlowEvents_;
293 },
294
295 set showFlowEvents(showFlowEvents) {
296 this.showFlowEvents_ = showFlowEvents;
297 this.dispatchChangeEvent();
298 },
299
300 get highlightVSync() {
301 return this.highlightVSync_;
302 },
303
304 set highlightVSync(highlightVSync) {
305 this.highlightVSync_ = highlightVSync;
306 this.dispatchChangeEvent();
307 },
308
309 get highDetails() {
310 return this.highDetails_;
311 },
312
313 set highDetails(highDetails) {
314 this.highDetails_ = highDetails;
315 this.dispatchChangeEvent();
316 },
317
318 get gridEnabled() {
319 return this.gridEnabled_;
320 },
321
322 set gridEnabled(enabled) {
323 if (this.gridEnabled_ == enabled)
324 return;
325
326 this.gridEnabled_ = enabled && true;
327 this.dispatchChangeEvent();
328 },
329
330 get gridTimebase() {
331 return this.gridTimebase_;
332 },
333
334 set gridTimebase(timebase) {
335 if (this.gridTimebase_ == timebase)
336 return;
337 this.gridTimebase_ = timebase;
338 this.dispatchChangeEvent();
339 },
340
341 get gridStep() {
342 return this.gridStep_;
343 },
344
345 get interestRange() {
346 return this.interestRange_;
347 },
348
349 drawMajorMarkLines: function(ctx) {
350 // Apply subpixel translate to get crisp lines.
351 // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
352 ctx.save();
353 ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
354
355 ctx.beginPath();
356 for (var idx in this.majorMarkPositions) {
357 var x = Math.floor(this.majorMarkPositions[idx]);
358 tv.c.drawLine(ctx, x, 0, x, ctx.canvas.height);
359 }
360 ctx.strokeStyle = '#ddd';
361 ctx.stroke();
362
363 ctx.restore();
364 },
365
366 drawGridLines: function(ctx, viewLWorld, viewRWorld) {
367 if (!this.gridEnabled)
368 return;
369
370 var dt = this.currentDisplayTransform;
371 var x = this.gridTimebase;
372
373 // Apply subpixel translate to get crisp lines.
374 // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
375 ctx.save();
376 ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
377
378 ctx.beginPath();
379 while (x < viewRWorld) {
380 if (x >= viewLWorld) {
381 // Do conversion to viewspace here rather than on
382 // x to avoid precision issues.
383 var vx = Math.floor(dt.xWorldToView(x));
384 tv.c.drawLine(ctx, vx, 0, vx, ctx.canvas.height);
385 }
386
387 x += this.gridStep;
388 }
389 ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
390 ctx.stroke();
391
392 ctx.restore();
393 },
394
395 rebuildEventToTrackMap: function() {
396 this.eventToTrackMap_ = undefined;
397
398 var eventToTrackMap = {};
399 eventToTrackMap.addEvent = function(event, track) {
400 if (!track)
401 throw new Error('Must provide a track.');
402 this[event.guid] = track;
403 };
404 this.modelTrackContainer_.addEventsToTrackMap(eventToTrackMap);
405 this.eventToTrackMap_ = eventToTrackMap;
406 },
407
408 rebuildContainerToTrackMap: function() {
409 this.containerToTrackObj.clearMap();
410 this.modelTrackContainer_.addContainersToTrackMap(
411 this.containerToTrackObj);
412 },
413
414 trackForEvent: function(event) {
415 return this.eventToTrackMap_[event.guid];
416 }
417 };
418
419 return {
420 ContainerToTrackObj: ContainerToTrackObj,
421 TimelineViewport: TimelineViewport
422 };
423});
424</script>