Chris Craik | b122baf | 2015-03-05 13:58:42 -0800 | [diff] [blame] | 1 | <!DOCTYPE html> |
| 2 | <!-- |
| 3 | Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 4 | Use of this source code is governed by a BSD-style license that can be |
| 5 | found in the LICENSE file. |
| 6 | --> |
| 7 | |
| 8 | <link rel="stylesheet" href="/core/tracks/ruler_track.css"> |
| 9 | |
| 10 | <link rel="import" href="/core/constants.html"> |
| 11 | <link rel="import" href="/core/tracks/track.html"> |
| 12 | <link rel="import" href="/core/tracks/heading_track.html"> |
| 13 | <link rel="import" href="/core/draw_helpers.html"> |
| 14 | <link rel="import" href="/base/ui.html"> |
| 15 | |
| 16 | <script> |
| 17 | 'use strict'; |
| 18 | |
| 19 | tv.exportTo('tv.c.tracks', function() { |
| 20 | /** |
| 21 | * A track that displays the ruler. |
| 22 | * @constructor |
| 23 | * @extends {HeadingTrack} |
| 24 | */ |
| 25 | var RulerTrack = tv.b.ui.define('ruler-track', tv.c.tracks.HeadingTrack); |
| 26 | |
| 27 | var logOf10 = Math.log(10); |
| 28 | function log10(x) { |
| 29 | return Math.log(x) / logOf10; |
| 30 | } |
| 31 | |
| 32 | RulerTrack.prototype = { |
| 33 | __proto__: tv.c.tracks.HeadingTrack.prototype, |
| 34 | |
| 35 | decorate: function(viewport) { |
| 36 | tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport); |
| 37 | this.classList.add('ruler-track'); |
| 38 | this.strings_secs_ = []; |
| 39 | this.strings_msecs_ = []; |
| 40 | |
| 41 | this.viewportChange_ = this.viewportChange_.bind(this); |
| 42 | viewport.addEventListener('change', this.viewportChange_); |
| 43 | |
| 44 | }, |
| 45 | |
| 46 | detach: function() { |
| 47 | tv.c.tracks.HeadingTrack.prototype.detach.call(this); |
| 48 | this.viewport.removeEventListener('change', |
| 49 | this.viewportChange_); |
| 50 | }, |
| 51 | |
| 52 | viewportChange_: function() { |
| 53 | if (this.viewport.interestRange.isEmpty) |
| 54 | this.classList.remove('tall-mode'); |
| 55 | else |
| 56 | this.classList.add('tall-mode'); |
| 57 | }, |
| 58 | |
| 59 | draw: function(type, viewLWorld, viewRWorld) { |
| 60 | switch (type) { |
| 61 | case tv.c.tracks.DrawType.GRID: |
| 62 | this.drawGrid_(viewLWorld, viewRWorld); |
| 63 | break; |
| 64 | case tv.c.tracks.DrawType.MARKERS: |
| 65 | if (!this.viewport.interestRange.isEmpty) |
| 66 | this.viewport.interestRange.draw(this.context(), |
| 67 | viewLWorld, viewRWorld); |
| 68 | break; |
| 69 | } |
| 70 | }, |
| 71 | |
| 72 | drawGrid_: function(viewLWorld, viewRWorld) { |
| 73 | var ctx = this.context(); |
| 74 | var pixelRatio = window.devicePixelRatio || 1; |
| 75 | |
| 76 | var canvasBounds = ctx.canvas.getBoundingClientRect(); |
| 77 | var trackBounds = this.getBoundingClientRect(); |
| 78 | var width = canvasBounds.width * pixelRatio; |
| 79 | var height = trackBounds.height * pixelRatio; |
| 80 | |
| 81 | var hasInterestRange = !this.viewport.interestRange.isEmpty; |
| 82 | |
| 83 | var rulerHeight = hasInterestRange ? (height * 2) / 5 : height; |
| 84 | |
| 85 | var vp = this.viewport; |
| 86 | var dt = vp.currentDisplayTransform; |
| 87 | |
| 88 | var idealMajorMarkDistancePix = 150 * pixelRatio; |
| 89 | var idealMajorMarkDistanceWorld = |
| 90 | dt.xViewVectorToWorld(idealMajorMarkDistancePix); |
| 91 | |
| 92 | var majorMarkDistanceWorld; |
| 93 | |
| 94 | // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc. |
| 95 | var conservativeGuess = |
| 96 | Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld))); |
| 97 | |
| 98 | // Once we have a conservative guess, consider things that evenly add up |
| 99 | // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still |
| 100 | // exceeds the ideal mark distance. |
| 101 | var divisors = [10, 5, 2, 1]; |
| 102 | for (var i = 0; i < divisors.length; ++i) { |
| 103 | var tightenedGuess = conservativeGuess / divisors[i]; |
| 104 | if (dt.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix) |
| 105 | continue; |
| 106 | majorMarkDistanceWorld = conservativeGuess / divisors[i - 1]; |
| 107 | break; |
| 108 | } |
| 109 | |
| 110 | var unit; |
| 111 | var unitDivisor; |
| 112 | var tickLabels = undefined; |
| 113 | if (majorMarkDistanceWorld < 100) { |
| 114 | unit = 'ms'; |
| 115 | unitDivisor = 1; |
| 116 | tickLabels = this.strings_msecs_; |
| 117 | } else { |
| 118 | unit = 's'; |
| 119 | unitDivisor = 1000; |
| 120 | tickLabels = this.strings_secs_; |
| 121 | } |
| 122 | |
| 123 | var numTicksPerMajor = 5; |
| 124 | var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor; |
| 125 | var minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld); |
| 126 | |
| 127 | var firstMajorMark = |
| 128 | Math.floor(viewLWorld / majorMarkDistanceWorld) * |
| 129 | majorMarkDistanceWorld; |
| 130 | |
| 131 | var minorTickH = Math.floor(rulerHeight * 0.25); |
| 132 | |
| 133 | ctx.save(); |
| 134 | |
| 135 | var pixelRatio = window.devicePixelRatio || 1; |
| 136 | ctx.lineWidth = Math.round(pixelRatio); |
| 137 | |
| 138 | // Apply subpixel translate to get crisp lines. |
| 139 | // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ |
| 140 | var crispLineCorrection = (ctx.lineWidth % 2) / 2; |
| 141 | ctx.translate(crispLineCorrection, -crispLineCorrection); |
| 142 | |
| 143 | ctx.fillStyle = 'rgb(0, 0, 0)'; |
| 144 | ctx.strokeStyle = 'rgb(0, 0, 0)'; |
| 145 | ctx.textAlign = 'left'; |
| 146 | ctx.textBaseline = 'top'; |
| 147 | |
| 148 | ctx.font = (9 * pixelRatio) + 'px sans-serif'; |
| 149 | |
| 150 | vp.majorMarkPositions = []; |
| 151 | |
| 152 | // Each iteration of this loop draws one major mark |
| 153 | // and numTicksPerMajor minor ticks. |
| 154 | // |
| 155 | // Rendering can't be done in world space because canvas transforms |
| 156 | // affect line width. So, do the conversions manually. |
| 157 | ctx.beginPath(); |
| 158 | for (var curX = firstMajorMark; |
| 159 | curX < viewRWorld; |
| 160 | curX += majorMarkDistanceWorld) { |
| 161 | |
| 162 | var curXView = Math.floor(dt.xWorldToView(curX)); |
| 163 | |
| 164 | var unitValue = curX / unitDivisor; |
| 165 | var roundedUnitValue = Math.floor(unitValue * 100000) / 100000; |
| 166 | |
| 167 | if (!tickLabels[roundedUnitValue]) |
| 168 | tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit; |
| 169 | ctx.fillText(tickLabels[roundedUnitValue], |
| 170 | curXView + (2 * pixelRatio), 0); |
| 171 | |
| 172 | vp.majorMarkPositions.push(curXView); |
| 173 | |
| 174 | // Major mark |
| 175 | tv.c.drawLine(ctx, curXView, 0, curXView, rulerHeight); |
| 176 | |
| 177 | // Minor marks |
| 178 | for (var i = 1; i < numTicksPerMajor; ++i) { |
| 179 | var xView = Math.floor(curXView + minorMarkDistancePx * i); |
| 180 | tv.c.drawLine(ctx, |
| 181 | xView, rulerHeight - minorTickH, |
| 182 | xView, rulerHeight); |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | // Draw bottom bar. |
| 187 | ctx.strokeStyle = 'rgb(0, 0, 0)'; |
| 188 | tv.c.drawLine(ctx, 0, height, width, height); |
| 189 | ctx.stroke(); |
| 190 | |
| 191 | // Give distance between directly adjacent markers. |
| 192 | if (!hasInterestRange) |
| 193 | return; |
| 194 | |
| 195 | // Draw middle bar. |
| 196 | tv.c.drawLine(ctx, 0, rulerHeight, width, rulerHeight); |
| 197 | ctx.stroke(); |
| 198 | |
| 199 | // Distance Variables. |
| 200 | var displayDistance; |
| 201 | var displayTextColor = 'rgb(0,0,0)'; |
| 202 | |
| 203 | // Arrow Variables. |
| 204 | var arrowSpacing = 10 * pixelRatio; |
| 205 | var arrowColor = 'rgb(128,121,121)'; |
| 206 | var arrowPosY = rulerHeight * 1.75; |
| 207 | var arrowWidthView = 3 * pixelRatio; |
| 208 | var arrowLengthView = 10 * pixelRatio; |
| 209 | var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing); |
| 210 | |
| 211 | ctx.textBaseline = 'middle'; |
| 212 | ctx.font = (14 * pixelRatio) + 'px sans-serif'; |
| 213 | var textPosY = arrowPosY; |
| 214 | |
| 215 | var interestRange = vp.interestRange; |
| 216 | |
| 217 | // If the range is zero, draw it's min timestamp next to the line. |
| 218 | if (interestRange.range === 0) { |
| 219 | var markerWorld = interestRange.min; |
| 220 | var markerView = dt.xWorldToView(markerWorld); |
| 221 | var displayValue = markerWorld / unitDivisor; |
| 222 | displayValue = Math.abs((Math.floor(displayValue * 1000) / 1000)); |
| 223 | |
| 224 | var textToDraw = displayValue + ' ' + unit; |
| 225 | var textLeftView = markerView + 4 * pixelRatio; |
| 226 | var textWidthView = ctx.measureText(textToDraw).width; |
| 227 | |
| 228 | // Put text to the left in case it gets cut off. |
| 229 | if (textLeftView + textWidthView > width) |
| 230 | textLeftView = markerView - 4 * pixelRatio - textWidthView; |
| 231 | |
| 232 | ctx.fillStyle = displayTextColor; |
| 233 | ctx.fillText(textToDraw, textLeftView, textPosY); |
| 234 | return; |
| 235 | } |
| 236 | |
| 237 | var leftMarker = interestRange.min; |
| 238 | var rightMarker = interestRange.max; |
| 239 | |
| 240 | var leftMarkerView = dt.xWorldToView(leftMarker); |
| 241 | var rightMarkerView = dt.xWorldToView(rightMarker); |
| 242 | |
| 243 | var distanceBetweenMarkers = interestRange.range; |
| 244 | var distanceBetweenMarkersView = |
| 245 | dt.xWorldVectorToView(distanceBetweenMarkers); |
| 246 | var positionInMiddleOfMarkersView = |
| 247 | leftMarkerView + (distanceBetweenMarkersView / 2); |
| 248 | |
| 249 | // Determine units. |
| 250 | if (distanceBetweenMarkers < 100) { |
| 251 | unit = 'ms'; |
| 252 | unitDivisor = 1; |
| 253 | } else { |
| 254 | unit = 's'; |
| 255 | unitDivisor = 1000; |
| 256 | } |
| 257 | |
| 258 | // Calculate display value to print. |
| 259 | displayDistance = distanceBetweenMarkers / unitDivisor; |
| 260 | var roundedDisplayDistance = |
| 261 | Math.abs((Math.floor(displayDistance * 1000) / 1000)); |
| 262 | var textToDraw = roundedDisplayDistance + ' ' + unit; |
| 263 | var textWidthView = ctx.measureText(textToDraw).width; |
| 264 | var spaceForArrowsAndTextView = |
| 265 | textWidthView + spaceForArrowsView + arrowSpacing; |
| 266 | |
| 267 | // Set text positions. |
| 268 | var textLeftView = positionInMiddleOfMarkersView - textWidthView / 2; |
| 269 | var textRightView = textLeftView + textWidthView; |
| 270 | |
| 271 | if (spaceForArrowsAndTextView > distanceBetweenMarkersView) { |
| 272 | // Print the display distance text right of the 2 markers. |
| 273 | textLeftView = rightMarkerView + 2 * arrowSpacing; |
| 274 | |
| 275 | // Put text to the left in case it gets cut off. |
| 276 | if (textLeftView + textWidthView > width) |
| 277 | textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView; |
| 278 | |
| 279 | ctx.fillStyle = displayTextColor; |
| 280 | ctx.fillText(textToDraw, textLeftView, textPosY); |
| 281 | |
| 282 | // Draw the arrows pointing from outside in and a line in between. |
| 283 | ctx.strokeStyle = arrowColor; |
| 284 | ctx.beginPath(); |
| 285 | tv.c.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView, |
| 286 | arrowPosY); |
| 287 | ctx.stroke(); |
| 288 | |
| 289 | ctx.fillStyle = arrowColor; |
| 290 | tv.c.drawArrow(ctx, |
| 291 | leftMarkerView - 1.5 * arrowSpacing, arrowPosY, |
| 292 | leftMarkerView, arrowPosY, |
| 293 | arrowLengthView, arrowWidthView); |
| 294 | tv.c.drawArrow(ctx, |
| 295 | rightMarkerView + 1.5 * arrowSpacing, arrowPosY, |
| 296 | rightMarkerView, arrowPosY, |
| 297 | arrowLengthView, arrowWidthView); |
| 298 | |
| 299 | } else if (spaceForArrowsView <= distanceBetweenMarkersView) { |
| 300 | var leftArrowStart; |
| 301 | var rightArrowStart; |
| 302 | if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) { |
| 303 | // Print the display distance text. |
| 304 | ctx.fillStyle = displayTextColor; |
| 305 | ctx.fillText(textToDraw, textLeftView, textPosY); |
| 306 | |
| 307 | leftArrowStart = textLeftView - arrowSpacing; |
| 308 | rightArrowStart = textRightView + arrowSpacing; |
| 309 | } else { |
| 310 | leftArrowStart = positionInMiddleOfMarkersView; |
| 311 | rightArrowStart = positionInMiddleOfMarkersView; |
| 312 | } |
| 313 | |
| 314 | // Draw the arrows pointing inside out. |
| 315 | ctx.strokeStyle = arrowColor; |
| 316 | ctx.fillStyle = arrowColor; |
| 317 | tv.c.drawArrow(ctx, |
| 318 | leftArrowStart, arrowPosY, |
| 319 | leftMarkerView, arrowPosY, |
| 320 | arrowLengthView, arrowWidthView); |
| 321 | tv.c.drawArrow(ctx, |
| 322 | rightArrowStart, arrowPosY, |
| 323 | rightMarkerView, arrowPosY, |
| 324 | arrowLengthView, arrowWidthView); |
| 325 | } |
| 326 | |
| 327 | ctx.restore(); |
| 328 | }, |
| 329 | |
| 330 | /** |
| 331 | * Adds items intersecting the given range to a selection. |
| 332 | * @param {number} loVX Lower X bound of the interval to search, in |
| 333 | * viewspace. |
| 334 | * @param {number} hiVX Upper X bound of the interval to search, in |
| 335 | * viewspace. |
| 336 | * @param {number} loVY Lower Y bound of the interval to search, in |
| 337 | * viewspace. |
| 338 | * @param {number} hiVY Upper Y bound of the interval to search, in |
| 339 | * viewspace. |
| 340 | * @param {Selection} selection Selection to which to add results. |
| 341 | */ |
| 342 | addIntersectingItemsInRangeToSelection: function( |
| 343 | loVX, hiVX, loY, hiY, selection) { |
| 344 | // Does nothing. There's nothing interesting to pick on the ruler |
| 345 | // track. |
| 346 | }, |
| 347 | |
| 348 | addAllObjectsMatchingFilterToSelection: function(filter, selection) { |
| 349 | } |
| 350 | }; |
| 351 | |
| 352 | return { |
| 353 | RulerTrack: RulerTrack |
| 354 | }; |
| 355 | }); |
| 356 | </script> |