1// Copyright (c) 2011 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. 4 5 6/** 7 * @fileoverview Interactive visualizaiton of TimelineModel objects 8 * based loosely on gantt charts. Each thread in the TimelineModel is given a 9 * set of TimelineTracks, one per subrow in the thread. The Timeline class 10 * acts as a controller, creating the individual tracks, while TimelineTracks 11 * do actual drawing. 12 * 13 * Visually, the Timeline produces (prettier) visualizations like the following: 14 * Thread1: AAAAAAAAAA AAAAA 15 * BBBB BB 16 * Thread2: CCCCCC CCCCC 17 * 18 */ 19cr.define('gpu', function() { 20 21 /** 22 * The TimelineViewport manages the transform used for navigating 23 * within the timeline. It is a simple transform: 24 * x' = (x+pan) * scale 25 * 26 * The timeline code tries to avoid directly accessing this transform, 27 * instead using this class to do conversion between world and view space, 28 * as well as the math for centering the viewport in various interesting 29 * ways. 30 * 31 * @constructor 32 * @extends {cr.EventTarget} 33 */ 34 function TimelineViewport() { 35 this.scaleX_ = 1; 36 this.panX_ = 0; 37 } 38 39 TimelineViewport.prototype = { 40 __proto__: cr.EventTarget.prototype, 41 42 get scaleX() { 43 return this.scaleX_; 44 }, 45 set scaleX(s) { 46 var changed = this.scaleX_ != s; 47 if (changed) { 48 this.scaleX_ = s; 49 cr.dispatchSimpleEvent(this, 'change'); 50 } 51 }, 52 53 get panX() { 54 return this.panX_; 55 }, 56 set panX(p) { 57 var changed = this.panX_ != p; 58 if (changed) { 59 this.panX_ = p; 60 cr.dispatchSimpleEvent(this, 'change'); 61 } 62 }, 63 64 setPanAndScale: function(p, s) { 65 var changed = this.scaleX_ != s || this.panX_ != p; 66 if (changed) { 67 this.scaleX_ = s; 68 this.panX_ = p; 69 cr.dispatchSimpleEvent(this, 'change'); 70 } 71 }, 72 73 xWorldToView: function(x) { 74 return (x + this.panX_) * this.scaleX_; 75 }, 76 77 xWorldVectorToView: function(x) { 78 return x * this.scaleX_; 79 }, 80 81 xViewToWorld: function(x) { 82 return (x / this.scaleX_) - this.panX_; 83 }, 84 85 xViewVectorToWorld: function(x) { 86 return x / this.scaleX_; 87 }, 88 89 xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) { 90 if (typeof viewX == 'string') { 91 if (viewX == 'left') { 92 viewX = 0; 93 } else if (viewX == 'center') { 94 viewX = viewWidth / 2; 95 } else if (viewX == 'right') { 96 viewX = viewWidth - 1; 97 } else { 98 throw Error('unrecognized string for viewPos. left|center|right'); 99 } 100 } 101 this.panX = (viewX / this.scaleX_) - worldX; 102 }, 103 104 applyTransformToCanavs: function(ctx) { 105 ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0); 106 } 107 }; 108 109 /** 110 * Renders a TimelineModel into a div element, making one 111 * TimelineTrack for each subrow in each thread of the model, managing 112 * overall track layout, and handling user interaction with the 113 * viewport. 114 * 115 * @constructor 116 * @extends {HTMLDivElement} 117 */ 118 Timeline = cr.ui.define('div'); 119 120 Timeline.prototype = { 121 __proto__: HTMLDivElement.prototype, 122 123 model_: null, 124 125 decorate: function() { 126 this.classList.add('timeline'); 127 this.needsViewportReset_ = false; 128 129 this.viewport_ = new TimelineViewport(); 130 this.viewport_.addEventListener('change', this.invalidate.bind(this)); 131 132 this.invalidatePending_ = false; 133 134 this.tracks_ = this.ownerDocument.createElement('div'); 135 this.tracks_.invalidate = this.invalidate.bind(this); 136 this.appendChild(this.tracks_); 137 138 this.dragBox_ = this.ownerDocument.createElement('div'); 139 this.dragBox_.className = 'timeline-drag-box'; 140 this.appendChild(this.dragBox_); 141 142 // The following code uses a setInterval to monitor the timeline control 143 // for size changes. This is so that we can keep the canvas' bitmap size 144 // correctly synchronized with its presentation size. 145 // TODO(nduca): detect this in a more efficient way, e.g. iframe hack. 146 this.lastSize_ = this.clientWidth + 'x' + this.clientHeight; 147 this.ownerDocument.defaultView.setInterval(function() { 148 var curSize = this.clientWidth + 'x' + this.clientHeight; 149 if (this.clientWidth && curSize != this.lastSize_) { 150 this.lastSize_ = curSize; 151 this.onResize(); 152 } 153 }.bind(this), 250); 154 155 document.addEventListener('keypress', this.onKeypress_.bind(this)); 156 this.addEventListener('mousedown', this.onMouseDown_.bind(this)); 157 this.addEventListener('mousemove', this.onMouseMove_.bind(this)); 158 this.addEventListener('mouseup', this.onMouseUp_.bind(this)); 159 this.lastMouseViewPos_ = {x: 0, y: 0}; 160 161 this.selection_ = []; 162 }, 163 164 get model() { 165 return this.model_; 166 }, 167 168 set model(model) { 169 if (!model) 170 throw Error('Model cannot be null'); 171 if (this.model) { 172 throw Error('Cannot set model twice.'); 173 } 174 this.model_ = model; 175 176 // Create tracks. 177 this.tracks_.textContent = ''; 178 var threads = model.getAllThreads(); 179 for (var tI = 0; tI < threads.length; tI++) { 180 var thread = threads[tI]; 181 var track = new TimelineThreadTrack(); 182 track.thread = thread; 183 track.viewport = this.viewport_; 184 this.tracks_.appendChild(track); 185 186 } 187 188 this.needsViewportReset_ = true; 189 }, 190 191 invalidate: function() { 192 if (this.invalidatePending_) 193 return; 194 this.invalidatePending_ = true; 195 window.setTimeout(function() { 196 this.invalidatePending_ = false; 197 this.redrawAllTracks_(); 198 }.bind(this), 0); 199 }, 200 201 onResize: function() { 202 for (var i = 0; i < this.tracks_.children.length; ++i) { 203 var track = this.tracks_.children[i]; 204 track.onResize(); 205 } 206 }, 207 208 redrawAllTracks_: function() { 209 if (this.needsViewportReset_ && this.clientWidth != 0) { 210 this.needsViewportReset_ = false; 211 /* update viewport */ 212 var rangeTimestamp = this.model_.maxTimestamp - 213 this.model_.minTimestamp; 214 var w = this.firstCanvas.width; 215 console.log('viewport was reset with w=', w); 216 var scaleX = w / rangeTimestamp; 217 var panX = -this.model_.minTimestamp; 218 this.viewport_.setPanAndScale(panX, scaleX); 219 } 220 for (var i = 0; i < this.tracks_.children.length; ++i) { 221 this.tracks_.children[i].redraw(); 222 } 223 }, 224 225 updateChildViewports_: function() { 226 for (var cI = 0; cI < this.tracks_.children.length; ++cI) { 227 var child = this.tracks_.children[cI]; 228 child.setViewport(this.panX, this.scaleX); 229 } 230 }, 231 232 onKeypress_: function(e) { 233 var vp = this.viewport_; 234 if (this.firstCanvas) { 235 var viewWidth = this.firstCanvas.clientWidth; 236 var curMouseV, curCenterW; 237 switch (event.keyCode) { 238 case 101: // e 239 var vX = this.lastMouseViewPos_.x; 240 var wX = vp.xViewToWorld(this.lastMouseViewPos_.x); 241 var distFromCenter = vX - (viewWidth / 2); 242 var percFromCenter = distFromCenter / viewWidth; 243 var percFromCenterSq = percFromCenter * percFromCenter; 244 vp.xPanWorldPosToViewPos(wX, 'center', viewWidth); 245 break; 246 case 119: // w 247 curMouseV = this.lastMouseViewPos_.x; 248 curCenterW = vp.xViewToWorld(curMouseV); 249 vp.scaleX = vp.scaleX * 1.5; 250 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); 251 break; 252 case 115: // s 253 curMouseV = this.lastMouseViewPos_.x; 254 curCenterW = vp.xViewToWorld(curMouseV); 255 vp.scaleX = vp.scaleX / 1.5; 256 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); 257 break; 258 case 87: // W 259 curMouseV = this.lastMouseViewPos_.x; 260 curCenterW = vp.xViewToWorld(curMouseV); 261 vp.scaleX = vp.scaleX * 10; 262 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); 263 break; 264 case 83: // S 265 curMouseV = this.lastMouseViewPos_.x; 266 curCenterW = vp.xViewToWorld(curMouseV); 267 vp.scaleX = vp.scaleX / 10; 268 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); 269 break; 270 case 97: // a 271 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); 272 break; 273 case 100: // d 274 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); 275 break; 276 case 65: // A 277 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); 278 break; 279 case 68: // D 280 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); 281 break; 282 } 283 } 284 }, 285 286 get keyHelp() { 287 return 'Keyboard shortcuts:\n' + 288 ' w/s : Zoom in/out\n' + 289 ' a/d : Pan left/right\n' + 290 ' e : Center on mouse'; 291 }, 292 293 get selection() { 294 return this.selection_; 295 }, 296 297 get firstCanvas() { 298 return this.tracks_.firstChild ? 299 this.tracks_.firstChild.firstCanvas : undefined; 300 }, 301 302 showDragBox_: function() { 303 this.dragBox_.hidden = false; 304 }, 305 306 hideDragBox_: function() { 307 this.dragBox_.hidden = true; 308 }, 309 310 setDragBoxPosition_: function(eDown, eCur) { 311 var loX = Math.min(eDown.clientX, eCur.clientX); 312 var hiX = Math.max(eDown.clientX, eCur.clientX); 313 var loY = Math.min(eDown.clientY, eCur.clientY); 314 var hiY = Math.max(eDown.clientY, eCur.clientY); 315 316 this.dragBox_.style.left = loX + 'px'; 317 this.dragBox_.style.top = loY + 'px'; 318 this.dragBox_.style.width = hiX - loX + 'px'; 319 this.dragBox_.style.height = hiY - loY + 'px'; 320 321 var canv = this.firstCanvas; 322 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); 323 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); 324 325 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; 326 this.dragBox_.textContent = roundedDuration + 'ms'; 327 328 var e = new cr.Event('selectionChanging'); 329 e.loWX = loWX; 330 e.hiWX = hiWX; 331 this.dispatchEvent(e); 332 }, 333 334 onMouseDown_: function(e) { 335 var canv = this.firstCanvas; 336 var pos = { 337 x: e.clientX - canv.offsetLeft, 338 y: e.clientY - canv.offsetTop 339 }; 340 var wX = this.viewport_.xViewToWorld(pos.x); 341 342 // Update the drag box position 343 this.showDragBox_(); 344 this.setDragBoxPosition_(e, e); 345 this.dragBeginEvent_ = e; 346 e.preventDefault(); 347 }, 348 349 onMouseMove_: function(e) { 350 if (!this.firstCanvas) 351 return; 352 var canv = this.firstCanvas; 353 var pos = { 354 x: e.clientX - canv.offsetLeft, 355 y: e.clientY - canv.offsetTop 356 }; 357 358 // Remember position. Used during keyboard zooming. 359 this.lastMouseViewPos_ = pos; 360 361 // Update the drag box 362 if (this.dragBeginEvent_) { 363 this.setDragBoxPosition_(this.dragBeginEvent_, e); 364 } 365 }, 366 367 onMouseUp_: function(e) { 368 var i; 369 if (this.dragBeginEvent_) { 370 // Stop the dragging. 371 this.hideDragBox_(); 372 var eDown = this.dragBeginEvent_; 373 this.dragBeginEvent_ = null; 374 375 // Figure out extents of the drag. 376 var loX = Math.min(eDown.clientX, e.clientX); 377 var hiX = Math.max(eDown.clientX, e.clientX); 378 var loY = Math.min(eDown.clientY, e.clientY); 379 var hiY = Math.max(eDown.clientY, e.clientY); 380 381 // Convert to worldspace. 382 var canv = this.firstCanvas; 383 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); 384 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); 385 386 // Clear old selection. 387 for (i = 0; i < this.selection_.length; ++i) { 388 this.selection_[i].slice.selected = false; 389 } 390 // Figure out what has been hit. 391 var selection = []; 392 function addHit(type, track, slice) { 393 selection.push({track: track, slice: slice}); 394 } 395 for (i = 0; i < this.tracks_.children.length; ++i) { 396 var track = this.tracks_.children[i]; 397 398 // Only check tracks that insersect the rect. 399 var a = Math.max(loY, track.offsetTop); 400 var b = Math.min(hiY, track.offsetTop + track.offsetHeight); 401 if (a <= b) { 402 track.pickRange(loWX, hiWX, loY, hiY, addHit); 403 } 404 } 405 // Activate the new selection. 406 this.selection_ = selection; 407 cr.dispatchSimpleEvent(this, 'selectionChange'); 408 for (i = 0; i < this.selection_.length; ++i) { 409 this.selection_[i].slice.selected = true; 410 } 411 this.invalidate(); // Cause tracks to redraw. 412 } 413 } 414 }; 415 416 /** 417 * The TimelineModel being viewed by the timeline 418 * @type {TimelineModel} 419 */ 420 cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS); 421 422 return { 423 Timeline: Timeline 424 }; 425}); 426