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