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. 4 5 'use strict'; 6 7 /** 8 * @fileoverview Code for the viewport. 9 */ 10 base.require('event_target'); 11 12 base.exportTo('tracing', function() { 13 14 /** 15 * The TimelineViewport manages the transform used for navigating 16 * within the timeline. It is a simple transform: 17 * x' = (x+pan) * scale 18 * 19 * The timeline code tries to avoid directly accessing this transform, 20 * instead using this class to do conversion between world and viewspace, 21 * as well as the math for centering the viewport in various interesting 22 * ways. 23 * 24 * @constructor 25 * @extends {base.EventTarget} 26 */ 27 function TimelineViewport(parentEl) { 28 this.parentEl_ = parentEl; 29 this.scaleX_ = 1; 30 this.panX_ = 0; 31 this.gridTimebase_ = 0; 32 this.gridStep_ = 1000 / 60; 33 this.gridEnabled_ = false; 34 this.hasCalledSetupFunction_ = false; 35 36 this.onResizeBoundToThis_ = this.onResize_.bind(this); 37 38 // The following code uses an interval to detect when the parent element 39 // is attached to the document. That is a trigger to run the setup function 40 // and install a resize listener. 41 this.checkForAttachInterval_ = setInterval( 42 this.checkForAttach_.bind(this), 250); 43 44 this.markers = []; 45 } 46 47 TimelineViewport.prototype = { 48 __proto__: base.EventTarget.prototype, 49 50 drawUnderContent: function(ctx, viewLWorld, viewRWorld, canvasH) { 51 }, 52 53 drawOverContent: function(ctx, viewLWorld, viewRWorld, canvasH) { 54 if (this.gridEnabled) { 55 var x = this.gridTimebase; 56 57 ctx.beginPath(); 58 while (x < viewRWorld) { 59 if (x >= viewLWorld) { 60 // Do conversion to viewspace here rather than on 61 // x to avoid precision issues. 62 var vx = this.xWorldToView(x); 63 ctx.moveTo(vx, 0); 64 ctx.lineTo(vx, canvasH); 65 } 66 x += this.gridStep; 67 } 68 ctx.strokeStyle = 'rgba(255,0,0,0.25)'; 69 ctx.stroke(); 70 } 71 72 for (var i = 0; i < this.markers.length; ++i) { 73 this.markers[i].drawLine(ctx, viewLWorld, viewRWorld, canvasH, this); 74 } 75 }, 76 77 /** 78 * Allows initialization of the viewport when the viewport's parent element 79 * has been attached to the document and given a size. 80 * @param {Function} fn Function to call when the viewport can be safely 81 * initialized. 82 */ 83 setWhenPossible: function(fn) { 84 this.pendingSetFunction_ = fn; 85 }, 86 87 /** 88 * @return {boolean} Whether the current timeline is attached to the 89 * document. 90 */ 91 get isAttachedToDocument_() { 92 var cur = this.parentEl_; 93 while (cur.parentNode) 94 cur = cur.parentNode; 95 return cur == this.parentEl_.ownerDocument; 96 }, 97 98 onResize_: function() { 99 this.dispatchChangeEvent(); 100 }, 101 102 /** 103 * Checks whether the parentNode is attached to the document. 104 * When it is, it installs the iframe-based resize detection hook 105 * and then runs the pendingSetFunction_, if present. 106 */ 107 checkForAttach_: function() { 108 if (!this.isAttachedToDocument_ || this.clientWidth == 0) 109 return; 110 111 if (!this.iframe_) { 112 this.iframe_ = document.createElement('iframe'); 113 this.iframe_.style.cssText = 114 'position:absolute;width:100%;height:0;border:0;visibility:hidden;'; 115 this.parentEl_.appendChild(this.iframe_); 116 117 this.iframe_.contentWindow.addEventListener('resize', 118 this.onResizeBoundToThis_); 119 } 120 121 var curSize = this.clientWidth + 'x' + this.clientHeight; 122 if (this.pendingSetFunction_) { 123 this.lastSize_ = curSize; 124 try { 125 this.pendingSetFunction_(); 126 } catch (ex) { 127 console.log('While running setWhenPossible:', ex); 128 } 129 this.pendingSetFunction_ = undefined; 130 } 131 132 window.clearInterval(this.checkForAttachInterval_); 133 this.checkForAttachInterval_ = undefined; 134 }, 135 136 /** 137 * Fires the change event on this viewport. Used to notify listeners 138 * to redraw when the underlying model has been mutated. 139 */ 140 dispatchChangeEvent: function() { 141 base.dispatchSimpleEvent(this, 'change'); 142 }, 143 144 dispatchMarkersChangeEvent_: function() { 145 base.dispatchSimpleEvent(this, 'markersChange'); 146 }, 147 148 detach: function() { 149 if (this.checkForAttachInterval_) { 150 window.clearInterval(this.checkForAttachInterval_); 151 this.checkForAttachInterval_ = undefined; 152 } 153 if (this.iframe_) { 154 this.iframe_.removeEventListener('resize', this.onResizeBoundToThis_); 155 this.parentEl_.removeChild(this.iframe_); 156 } 157 }, 158 159 get scaleX() { 160 return this.scaleX_; 161 }, 162 set scaleX(s) { 163 var changed = this.scaleX_ != s; 164 if (changed) { 165 this.scaleX_ = s; 166 this.dispatchChangeEvent(); 167 } 168 }, 169 170 get panX() { 171 return this.panX_; 172 }, 173 set panX(p) { 174 var changed = this.panX_ != p; 175 if (changed) { 176 this.panX_ = p; 177 this.dispatchChangeEvent(); 178 } 179 }, 180 181 setPanAndScale: function(p, s) { 182 var changed = this.scaleX_ != s || this.panX_ != p; 183 if (changed) { 184 this.scaleX_ = s; 185 this.panX_ = p; 186 this.dispatchChangeEvent(); 187 } 188 }, 189 190 xWorldToView: function(x) { 191 return (x + this.panX_) * this.scaleX_; 192 }, 193 194 xWorldVectorToView: function(x) { 195 return x * this.scaleX_; 196 }, 197 198 xViewToWorld: function(x) { 199 return (x / this.scaleX_) - this.panX_; 200 }, 201 202 xViewVectorToWorld: function(x) { 203 return x / this.scaleX_; 204 }, 205 206 xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) { 207 if (typeof viewX == 'string') { 208 if (viewX == 'left') { 209 viewX = 0; 210 } else if (viewX == 'center') { 211 viewX = viewWidth / 2; 212 } else if (viewX == 'right') { 213 viewX = viewWidth - 1; 214 } else { 215 throw new Error('unrecognized string for viewPos. left|center|right'); 216 } 217 } 218 this.panX = (viewX / this.scaleX_) - worldX; 219 }, 220 221 xPanWorldBoundsIntoView: function(worldMin, worldMax, viewWidth) { 222 if (this.xWorldToView(worldMin) < 0) 223 this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth); 224 else if (this.xWorldToView(worldMax) > viewWidth) 225 this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth); 226 }, 227 228 xSetWorldBounds: function(worldMin, worldMax, viewWidth) { 229 var worldWidth = worldMax - worldMin; 230 var scaleX = viewWidth / worldWidth; 231 var panX = -worldMin; 232 this.setPanAndScale(panX, scaleX); 233 }, 234 235 get gridEnabled() { 236 return this.gridEnabled_; 237 }, 238 239 set gridEnabled(enabled) { 240 if (this.gridEnabled_ == enabled) 241 return; 242 this.gridEnabled_ = enabled && true; 243 this.dispatchChangeEvent(); 244 }, 245 246 get gridTimebase() { 247 return this.gridTimebase_; 248 }, 249 250 set gridTimebase(timebase) { 251 if (this.gridTimebase_ == timebase) 252 return; 253 this.gridTimebase_ = timebase; 254 base.dispatchSimpleEvent(this, 'change'); 255 }, 256 257 get gridStep() { 258 return this.gridStep_; 259 }, 260 261 applyTransformToCanvas: function(ctx) { 262 ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0); 263 }, 264 265 addMarker: function(positionWorld) { 266 var marker = new ViewportMarker(this, positionWorld); 267 this.markers.push(marker); 268 this.dispatchChangeEvent(); 269 this.dispatchMarkersChangeEvent_(); 270 return marker; 271 }, 272 273 removeMarker: function(marker) { 274 for (var i = 0; i < this.markers.length; ++i) { 275 if (this.markers[i] === marker) { 276 this.markers.splice(i, 1); 277 this.dispatchChangeEvent(); 278 this.dispatchMarkersChangeEvent_(); 279 return true; 280 } 281 } 282 }, 283 284 findMarkerNear: function(positionWorld, nearnessInViewPixels) { 285 // Converts pixels into distance in world. 286 var nearnessThresholdWorld = this.xViewVectorToWorld( 287 nearnessInViewPixels); 288 for (var i = 0; i < this.markers.length; ++i) { 289 if (Math.abs(this.markers[i].positionWorld - positionWorld) <= 290 nearnessThresholdWorld) { 291 var marker = this.markers[i]; 292 return marker; 293 } 294 } 295 return undefined; 296 } 297 }; 298 299 /** 300 * Represents a marked position in the world, at a viewport level. 301 * @constructor 302 */ 303 function ViewportMarker(vp, positionWorld) { 304 this.viewport_ = vp; 305 this.positionWorld_ = positionWorld; 306 this.selected_ = false; 307 } 308 309 ViewportMarker.prototype = { 310 get positionWorld() { 311 return this.positionWorld_; 312 }, 313 314 set positionWorld(positionWorld) { 315 this.positionWorld_ = positionWorld; 316 this.viewport_.dispatchChangeEvent(); 317 }, 318 319 set selected(selected) { 320 this.selected_ = selected; 321 this.viewport_.dispatchChangeEvent(); 322 }, 323 324 get selected() { 325 return this.selected_; 326 }, 327 328 get color() { 329 if (this.selected) 330 return 'rgb(255,0,0)'; 331 return 'rgb(0,0,0)'; 332 }, 333 334 drawTriangle_: function(ctx, viewLWorld, viewRWorld, 335 canvasH, rulerHeight, vp) { 336 ctx.beginPath(); 337 var ts = this.positionWorld_; 338 if (ts >= viewLWorld && ts < viewRWorld) { 339 var viewX = vp.xWorldToView(ts); 340 ctx.moveTo(viewX, rulerHeight); 341 ctx.lineTo(viewX - 3, rulerHeight / 2); 342 ctx.lineTo(viewX + 3, rulerHeight / 2); 343 ctx.lineTo(viewX, rulerHeight); 344 ctx.closePath(); 345 ctx.fillStyle = this.color; 346 ctx.fill(); 347 if (rulerHeight != canvasH) { 348 ctx.beginPath(); 349 ctx.moveTo(viewX, rulerHeight); 350 ctx.lineTo(viewX, canvasH); 351 ctx.closePath(); 352 ctx.strokeStyle = this.color; 353 ctx.stroke(); 354 } 355 } 356 }, 357 358 drawLine: function(ctx, viewLWorld, viewRWorld, canvasH, vp) { 359 ctx.beginPath(); 360 var ts = this.positionWorld_; 361 if (ts >= viewLWorld && ts < viewRWorld) { 362 var viewX = vp.xWorldToView(ts); 363 ctx.moveTo(viewX, 0); 364 ctx.lineTo(viewX, canvasH); 365 } 366 ctx.strokeStyle = this.color; 367 ctx.stroke(); 368 } 369 }; 370 371 return { 372 TimelineViewport: TimelineViewport, 373 ViewportMarker: ViewportMarker 374 }; 375 }); 376