Home | History | Annotate | Download | only in gpu_internals
      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  */
     19 cr.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