Home | History | Annotate | Download | only in tracks
      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 base.requireStylesheet('tracks.timeline_viewport_track');
      8 
      9 base.require('tracks.timeline_track');
     10 base.require('tracks.timeline_canvas_based_track');
     11 base.require('ui');
     12 
     13 base.exportTo('tracks', function() {
     14 
     15   /**
     16    * A track that displays the viewport size and scale.
     17    * @constructor
     18    * @extends {CanvasBasedTrack}
     19    */
     20 
     21   var TimelineViewportTrack = base.ui.define(tracks.TimelineCanvasBasedTrack);
     22 
     23   var logOf10 = Math.log(10);
     24   function log10(x) {
     25     return Math.log(x) / logOf10;
     26   }
     27 
     28   TimelineViewportTrack.prototype = {
     29 
     30     __proto__: tracks.TimelineCanvasBasedTrack.prototype,
     31 
     32     decorate: function() {
     33       this.classList.add('timeline-viewport-track');
     34       this.strings_secs_ = [];
     35       this.strings_msecs_ = [];
     36       this.addEventListener('mousedown', this.onMouseDown);
     37     },
     38 
     39     onMouseDown: function(e) {
     40       if (e.button != 0)
     41         return;
     42       this.placeAndBeginDraggingMarker(e.clientX);
     43     },
     44 
     45 
     46     placeAndBeginDraggingMarker: function(clientX) {
     47       var viewX = clientX - this.canvasContainer_.offsetLeft;
     48       var worldX = this.viewport_.xViewToWorld(viewX);
     49       var marker = this.viewport_.findMarkerNear(worldX, 6);
     50       var createdMarker = false;
     51       var movedMarker = false;
     52       if (!marker) {
     53         marker = this.viewport_.addMarker(worldX);
     54         createdMarker = true;
     55       }
     56       marker.selected = true;
     57 
     58       var that = this;
     59       var onMouseMove = function(e) {
     60         var viewX = e.clientX - that.canvasContainer_.offsetLeft;
     61         var worldX = that.viewport_.xViewToWorld(viewX);
     62         marker.positionWorld = worldX;
     63         movedMarker = true;
     64       };
     65 
     66       var onMouseUp = function(e) {
     67         marker.selected = false;
     68         if (!movedMarker && !createdMarker)
     69           that.viewport_.removeMarker(marker);
     70         document.removeEventListener('mouseup', onMouseUp);
     71         document.removeEventListener('mousemove', onMouseMove);
     72       };
     73 
     74       document.addEventListener('mouseup', onMouseUp);
     75       document.addEventListener('mousemove', onMouseMove);
     76     },
     77 
     78     drawLine_: function(ctx, x1, y1, x2, y2, color) {
     79       ctx.beginPath();
     80       ctx.moveTo(x1, y1);
     81       ctx.lineTo(x2, y2);
     82       ctx.closePath();
     83       ctx.strokeStyle = color;
     84       ctx.stroke();
     85     },
     86 
     87     drawArrow_: function(ctx, x1, y1, x2, y2, arrowWidth, color) {
     88 
     89       this.drawLine_(ctx, x1, y1, x2, y2, color);
     90 
     91       var dx = x2 - x1;
     92       var dy = y2 - y1;
     93       var len = Math.sqrt(dx * dx + dy * dy);
     94       var perc = (len - 10) / len;
     95       var bx = x1 + perc * dx;
     96       var by = y1 + perc * dy;
     97       var ux = dx / len;
     98       var uy = dy / len;
     99       var ax = uy * arrowWidth;
    100       var ay = -ux * arrowWidth;
    101 
    102       ctx.beginPath();
    103       ctx.fillStyle = color;
    104       ctx.moveTo(bx + ax, by + ay);
    105       ctx.lineTo(x2, y2);
    106       ctx.lineTo(bx - ax, by - ay);
    107       ctx.lineTo(bx + ax, by + ay);
    108       ctx.closePath();
    109       ctx.fill();
    110     },
    111 
    112     redraw: function() {
    113       var ctx = this.ctx_;
    114       var canvasW = this.canvas_.width;
    115       var canvasH = this.canvas_.height;
    116 
    117       ctx.clearRect(0, 0, canvasW, canvasH);
    118 
    119       // Culling parametrs.
    120       var vp = this.viewport_;
    121       var pixWidth = vp.xViewVectorToWorld(1);
    122       var viewLWorld = vp.xViewToWorld(0);
    123       var viewRWorld = vp.xViewToWorld(canvasW);
    124 
    125       var measurements = this.classList.contains('timeline-viewport' +
    126           '-track-with-distance-measurements');
    127 
    128       var rulerHeight = measurements ? canvasH / 2 : canvasH;
    129 
    130       for (var i = 0; i < vp.markers.length; ++i) {
    131         vp.markers[i].drawTriangle_(ctx, viewLWorld, viewRWorld,
    132                                     canvasH, rulerHeight, vp);
    133       }
    134 
    135       var idealMajorMarkDistancePix = 150;
    136       var idealMajorMarkDistanceWorld =
    137           vp.xViewVectorToWorld(idealMajorMarkDistancePix);
    138 
    139       var majorMarkDistanceWorld;
    140       var unit;
    141       var unitDivisor;
    142       var tickLabels;
    143 
    144       // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc.
    145       var conservativeGuess =
    146           Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld)));
    147 
    148       // Once we have a conservative guess, consider things that evenly add up
    149       // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still
    150       // exceeds the ideal mark distance.
    151       var divisors = [10, 5, 2, 1];
    152       for (var i = 0; i < divisors.length; ++i) {
    153         var tightenedGuess = conservativeGuess / divisors[i];
    154         if (vp.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix)
    155           continue;
    156         majorMarkDistanceWorld = conservativeGuess / divisors[i - 1];
    157         break;
    158       }
    159       var tickLabels = undefined;
    160       if (majorMarkDistanceWorld < 100) {
    161         unit = 'ms';
    162         unitDivisor = 1;
    163         tickLabels = this.strings_msecs_;
    164       } else {
    165         unit = 's';
    166         unitDivisor = 1000;
    167         tickLabels = this.strings_secs_;
    168       }
    169 
    170       var numTicksPerMajor = 5;
    171       var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
    172       var minorMarkDistancePx = vp.xWorldVectorToView(minorMarkDistanceWorld);
    173 
    174       var firstMajorMark =
    175           Math.floor(viewLWorld / majorMarkDistanceWorld) *
    176               majorMarkDistanceWorld;
    177 
    178       var minorTickH = Math.floor(canvasH * 0.25);
    179 
    180       ctx.fillStyle = 'rgb(0, 0, 0)';
    181       ctx.strokeStyle = 'rgb(0, 0, 0)';
    182       ctx.textAlign = 'left';
    183       ctx.textBaseline = 'top';
    184 
    185       var pixelRatio = window.devicePixelRatio || 1;
    186       ctx.font = (9 * pixelRatio) + 'px sans-serif';
    187 
    188       // Each iteration of this loop draws one major mark
    189       // and numTicksPerMajor minor ticks.
    190       //
    191       // Rendering can't be done in world space because canvas transforms
    192       // affect line width. So, do the conversions manually.
    193       for (var curX = firstMajorMark;
    194            curX < viewRWorld;
    195            curX += majorMarkDistanceWorld) {
    196 
    197         var curXView = Math.floor(vp.xWorldToView(curX));
    198 
    199         var unitValue = curX / unitDivisor;
    200         var roundedUnitValue = Math.floor(unitValue * 100000) / 100000;
    201 
    202         if (!tickLabels[roundedUnitValue])
    203           tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit;
    204         ctx.fillText(tickLabels[roundedUnitValue],
    205                      curXView + 2 * pixelRatio, 0);
    206         ctx.beginPath();
    207 
    208         // Major mark
    209         ctx.moveTo(curXView, 0);
    210         ctx.lineTo(curXView, rulerHeight);
    211 
    212         // Minor marks
    213         for (var i = 1; i < numTicksPerMajor; ++i) {
    214           var xView = Math.floor(curXView + minorMarkDistancePx * i);
    215           ctx.moveTo(xView, rulerHeight - minorTickH);
    216           ctx.lineTo(xView, rulerHeight);
    217         }
    218 
    219         ctx.stroke();
    220       }
    221 
    222       // Give distance between directly adjacent markers.
    223       if (measurements) {
    224 
    225         // Divide canvas horizontally between ruler and measurements.
    226         ctx.moveTo(0, rulerHeight);
    227         ctx.lineTo(canvasW, rulerHeight);
    228         ctx.stroke();
    229 
    230         // Obtain a sorted array of markers
    231         var sortedMarkers = vp.markers.slice();
    232         sortedMarkers.sort(function(a, b) {
    233           return a.positionWorld_ - b.positionWorld_;
    234         });
    235 
    236         // Distance Variables.
    237         var displayDistance;
    238         var unitDivisor;
    239         var displayTextColor = 'rgb(0,0,0)';
    240         var measurementsPosY = rulerHeight + 2;
    241 
    242         // Arrow Variables.
    243         var arrowSpacing = 10;
    244         var arrowColor = 'rgb(128,121,121)';
    245         var arrowPosY = measurementsPosY + 4;
    246         var arrowWidthView = 3;
    247         var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
    248 
    249         for (i = 0; i < sortedMarkers.length - 1; i++) {
    250           var rightMarker = sortedMarkers[i + 1];
    251           var leftMarker = sortedMarkers[i];
    252           var distanceBetweenMarkers =
    253               rightMarker.positionWorld - leftMarker.positionWorld;
    254           var distanceBetweenMarkersView =
    255               vp.xWorldVectorToView(distanceBetweenMarkers);
    256 
    257           var positionInMiddleOfMarkers = leftMarker.positionWorld +
    258                                               distanceBetweenMarkers / 2;
    259           var positionInMiddleOfMarkersView =
    260               vp.xWorldToView(positionInMiddleOfMarkers);
    261 
    262           // Determine units.
    263           if (distanceBetweenMarkers < 100) {
    264             unit = 'ms';
    265             unitDivisor = 1;
    266           } else {
    267             unit = 's';
    268             unitDivisor = 1000;
    269           }
    270           // Calculate display value to print.
    271           displayDistance = distanceBetweenMarkers / unitDivisor;
    272           var roundedDisplayDistance =
    273               Math.abs((Math.floor(displayDistance * 1000) / 1000));
    274           var textToDraw = roundedDisplayDistance + ' ' + unit;
    275           var textWidthView = ctx.measureText(textToDraw).width;
    276           var textWidthWorld = vp.xViewVectorToWorld(textWidthView);
    277           var spaceForArrowsAndTextView = textWidthView +
    278                                           spaceForArrowsView + arrowSpacing;
    279 
    280           // Set text positions.
    281           var textLeft = leftMarker.positionWorld +
    282               (distanceBetweenMarkers / 2) - (textWidthWorld / 2);
    283           var textRight = textLeft + textWidthWorld;
    284           var textPosY = measurementsPosY;
    285           var textLeftView = vp.xWorldToView(textLeft);
    286           var textRightView = vp.xWorldToView(textRight);
    287           var leftMarkerView = vp.xWorldToView(leftMarker.positionWorld);
    288           var rightMarkerView = vp.xWorldToView(rightMarker.positionWorld);
    289           var textDrawn = false;
    290 
    291           if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
    292             // Print the display distance text.
    293             ctx.fillStyle = displayTextColor;
    294             ctx.fillText(textToDraw, textLeftView, textPosY);
    295             textDrawn = true;
    296           }
    297 
    298           if (spaceForArrowsView <= distanceBetweenMarkersView) {
    299             var leftArrowStart;
    300             var rightArrowStart;
    301             if (textDrawn) {
    302               leftArrowStart = textLeftView - arrowSpacing;
    303               rightArrowStart = textRightView + arrowSpacing;
    304             } else {
    305               leftArrowStart = positionInMiddleOfMarkersView;
    306               rightArrowStart = positionInMiddleOfMarkersView;
    307             }
    308             // Draw left arrow.
    309             this.drawArrow_(ctx, leftArrowStart, arrowPosY,
    310                 leftMarkerView, arrowPosY, arrowWidthView, arrowColor);
    311             // Draw right arrow.
    312             this.drawArrow_(ctx, rightArrowStart, arrowPosY,
    313                 rightMarkerView, arrowPosY, arrowWidthView, arrowColor);
    314           }
    315         }
    316       }
    317     },
    318 
    319     /**
    320      * Adds items intersecting a point to a selection.
    321      * @param {number} vX X location to search at, in viewspace.
    322      * @param {number} vY Y location to search at, in viewspace.
    323      * @param {TimelineSelection} selection Selection to which to add hits.
    324      * @return {boolean} true if a slice was found, otherwise false.
    325      */
    326     addIntersectingItemsToSelection: function(vX, vY, selection) {
    327       // Does nothing. There's nothing interesting to pick on the viewport
    328       // track.
    329     },
    330 
    331     /**
    332      * Adds items intersecting the given range to a selection.
    333      * @param {number} loVX Lower X bound of the interval to search, in
    334      *     viewspace.
    335      * @param {number} hiVX Upper X bound of the interval to search, in
    336      *     viewspace.
    337      * @param {number} loVY Lower Y bound of the interval to search, in
    338      *     viewspace.
    339      * @param {number} hiVY Upper Y bound of the interval to search, in
    340      *     viewspace.
    341      * @param {TimelineSelection} selection Selection to which to add hits.
    342      */
    343     addIntersectingItemsInRangeToSelection: function(
    344         loVX, hiVX, loY, hiY, selection) {
    345       // Does nothing. There's nothing interesting to pick on the viewport
    346       // track.
    347     },
    348 
    349     addAllObjectsMatchingFilterToSelection: function(filter, selection) {
    350     }
    351   };
    352 
    353   return {
    354     TimelineViewportTrack: TimelineViewportTrack
    355   };
    356 });
    357