blob: 410dbcc5aaeac5ad286760ff32d666144736990f [file] [log] [blame]
Chris Craikb122baf2015-03-05 13:58:42 -08001<!DOCTYPE html>
2<!--
3Copyright (c) 2013 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found 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
19tv.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>