Home | History | Annotate | Download | only in src
      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