blob: 5396b2f3bc18c6aec579e3329b5fb7ff9f000234 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
base.requireStylesheet('tracks.timeline_viewport_track');
base.require('tracks.timeline_track');
base.require('tracks.timeline_canvas_based_track');
base.require('ui');
base.exportTo('tracks', function() {
/**
* A track that displays the viewport size and scale.
* @constructor
* @extends {CanvasBasedTrack}
*/
var TimelineViewportTrack = base.ui.define(tracks.TimelineCanvasBasedTrack);
var logOf10 = Math.log(10);
function log10(x) {
return Math.log(x) / logOf10;
}
TimelineViewportTrack.prototype = {
__proto__: tracks.TimelineCanvasBasedTrack.prototype,
decorate: function() {
this.classList.add('timeline-viewport-track');
this.strings_secs_ = [];
this.strings_msecs_ = [];
this.addEventListener('mousedown', this.onMouseDown);
},
onMouseDown: function(e) {
if (e.button != 0)
return;
this.placeAndBeginDraggingMarker(e.clientX);
},
placeAndBeginDraggingMarker: function(clientX) {
var viewX = clientX - this.canvasContainer_.offsetLeft;
var worldX = this.viewport_.xViewToWorld(viewX);
var marker = this.viewport_.findMarkerNear(worldX, 6);
var createdMarker = false;
var movedMarker = false;
if (!marker) {
marker = this.viewport_.addMarker(worldX);
createdMarker = true;
}
marker.selected = true;
var that = this;
var onMouseMove = function(e) {
var viewX = e.clientX - that.canvasContainer_.offsetLeft;
var worldX = that.viewport_.xViewToWorld(viewX);
marker.positionWorld = worldX;
movedMarker = true;
};
var onMouseUp = function(e) {
marker.selected = false;
if (!movedMarker && !createdMarker)
that.viewport_.removeMarker(marker);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousemove', onMouseMove);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousemove', onMouseMove);
},
drawLine_: function(ctx, x1, y1, x2, y2, color) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.strokeStyle = color;
ctx.stroke();
},
drawArrow_: function(ctx, x1, y1, x2, y2, arrowWidth, color) {
this.drawLine_(ctx, x1, y1, x2, y2, color);
var dx = x2 - x1;
var dy = y2 - y1;
var len = Math.sqrt(dx * dx + dy * dy);
var perc = (len - 10) / len;
var bx = x1 + perc * dx;
var by = y1 + perc * dy;
var ux = dx / len;
var uy = dy / len;
var ax = uy * arrowWidth;
var ay = -ux * arrowWidth;
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(bx + ax, by + ay);
ctx.lineTo(x2, y2);
ctx.lineTo(bx - ax, by - ay);
ctx.lineTo(bx + ax, by + ay);
ctx.closePath();
ctx.fill();
},
redraw: function() {
var ctx = this.ctx_;
var canvasW = this.canvas_.width;
var canvasH = this.canvas_.height;
ctx.clearRect(0, 0, canvasW, canvasH);
// Culling parametrs.
var vp = this.viewport_;
var pixWidth = vp.xViewVectorToWorld(1);
var viewLWorld = vp.xViewToWorld(0);
var viewRWorld = vp.xViewToWorld(canvasW);
var measurements = this.classList.contains('timeline-viewport' +
'-track-with-distance-measurements');
var rulerHeight = measurements ? canvasH / 2 : canvasH;
for (var i = 0; i < vp.markers.length; ++i) {
vp.markers[i].drawTriangle_(ctx, viewLWorld, viewRWorld,
canvasH, rulerHeight, vp);
}
var idealMajorMarkDistancePix = 150;
var idealMajorMarkDistanceWorld =
vp.xViewVectorToWorld(idealMajorMarkDistancePix);
var majorMarkDistanceWorld;
var unit;
var unitDivisor;
var tickLabels;
// The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc.
var conservativeGuess =
Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld)));
// Once we have a conservative guess, consider things that evenly add up
// to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still
// exceeds the ideal mark distance.
var divisors = [10, 5, 2, 1];
for (var i = 0; i < divisors.length; ++i) {
var tightenedGuess = conservativeGuess / divisors[i];
if (vp.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix)
continue;
majorMarkDistanceWorld = conservativeGuess / divisors[i - 1];
break;
}
var tickLabels = undefined;
if (majorMarkDistanceWorld < 100) {
unit = 'ms';
unitDivisor = 1;
tickLabels = this.strings_msecs_;
} else {
unit = 's';
unitDivisor = 1000;
tickLabels = this.strings_secs_;
}
var numTicksPerMajor = 5;
var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
var minorMarkDistancePx = vp.xWorldVectorToView(minorMarkDistanceWorld);
var firstMajorMark =
Math.floor(viewLWorld / majorMarkDistanceWorld) *
majorMarkDistanceWorld;
var minorTickH = Math.floor(canvasH * 0.25);
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.strokeStyle = 'rgb(0, 0, 0)';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
var pixelRatio = window.devicePixelRatio || 1;
ctx.font = (9 * pixelRatio) + 'px sans-serif';
// Each iteration of this loop draws one major mark
// and numTicksPerMajor minor ticks.
//
// Rendering can't be done in world space because canvas transforms
// affect line width. So, do the conversions manually.
for (var curX = firstMajorMark;
curX < viewRWorld;
curX += majorMarkDistanceWorld) {
var curXView = Math.floor(vp.xWorldToView(curX));
var unitValue = curX / unitDivisor;
var roundedUnitValue = Math.floor(unitValue * 100000) / 100000;
if (!tickLabels[roundedUnitValue])
tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit;
ctx.fillText(tickLabels[roundedUnitValue],
curXView + 2 * pixelRatio, 0);
ctx.beginPath();
// Major mark
ctx.moveTo(curXView, 0);
ctx.lineTo(curXView, rulerHeight);
// Minor marks
for (var i = 1; i < numTicksPerMajor; ++i) {
var xView = Math.floor(curXView + minorMarkDistancePx * i);
ctx.moveTo(xView, rulerHeight - minorTickH);
ctx.lineTo(xView, rulerHeight);
}
ctx.stroke();
}
// Give distance between directly adjacent markers.
if (measurements) {
// Divide canvas horizontally between ruler and measurements.
ctx.moveTo(0, rulerHeight);
ctx.lineTo(canvasW, rulerHeight);
ctx.stroke();
// Obtain a sorted array of markers
var sortedMarkers = vp.markers.slice();
sortedMarkers.sort(function(a, b) {
return a.positionWorld_ - b.positionWorld_;
});
// Distance Variables.
var displayDistance;
var unitDivisor;
var displayTextColor = 'rgb(0,0,0)';
var measurementsPosY = rulerHeight + 2;
// Arrow Variables.
var arrowSpacing = 10;
var arrowColor = 'rgb(128,121,121)';
var arrowPosY = measurementsPosY + 4;
var arrowWidthView = 3;
var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
for (i = 0; i < sortedMarkers.length - 1; i++) {
var rightMarker = sortedMarkers[i + 1];
var leftMarker = sortedMarkers[i];
var distanceBetweenMarkers =
rightMarker.positionWorld - leftMarker.positionWorld;
var distanceBetweenMarkersView =
vp.xWorldVectorToView(distanceBetweenMarkers);
var positionInMiddleOfMarkers = leftMarker.positionWorld +
distanceBetweenMarkers / 2;
var positionInMiddleOfMarkersView =
vp.xWorldToView(positionInMiddleOfMarkers);
// Determine units.
if (distanceBetweenMarkers < 100) {
unit = 'ms';
unitDivisor = 1;
} else {
unit = 's';
unitDivisor = 1000;
}
// Calculate display value to print.
displayDistance = distanceBetweenMarkers / unitDivisor;
var roundedDisplayDistance =
Math.abs((Math.floor(displayDistance * 1000) / 1000));
var textToDraw = roundedDisplayDistance + ' ' + unit;
var textWidthView = ctx.measureText(textToDraw).width;
var textWidthWorld = vp.xViewVectorToWorld(textWidthView);
var spaceForArrowsAndTextView = textWidthView +
spaceForArrowsView + arrowSpacing;
// Set text positions.
var textLeft = leftMarker.positionWorld +
(distanceBetweenMarkers / 2) - (textWidthWorld / 2);
var textRight = textLeft + textWidthWorld;
var textPosY = measurementsPosY;
var textLeftView = vp.xWorldToView(textLeft);
var textRightView = vp.xWorldToView(textRight);
var leftMarkerView = vp.xWorldToView(leftMarker.positionWorld);
var rightMarkerView = vp.xWorldToView(rightMarker.positionWorld);
var textDrawn = false;
if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
// Print the display distance text.
ctx.fillStyle = displayTextColor;
ctx.fillText(textToDraw, textLeftView, textPosY);
textDrawn = true;
}
if (spaceForArrowsView <= distanceBetweenMarkersView) {
var leftArrowStart;
var rightArrowStart;
if (textDrawn) {
leftArrowStart = textLeftView - arrowSpacing;
rightArrowStart = textRightView + arrowSpacing;
} else {
leftArrowStart = positionInMiddleOfMarkersView;
rightArrowStart = positionInMiddleOfMarkersView;
}
// Draw left arrow.
this.drawArrow_(ctx, leftArrowStart, arrowPosY,
leftMarkerView, arrowPosY, arrowWidthView, arrowColor);
// Draw right arrow.
this.drawArrow_(ctx, rightArrowStart, arrowPosY,
rightMarkerView, arrowPosY, arrowWidthView, arrowColor);
}
}
}
},
/**
* Adds items intersecting a point to a selection.
* @param {number} vX X location to search at, in viewspace.
* @param {number} vY Y location to search at, in viewspace.
* @param {TimelineSelection} selection Selection to which to add hits.
* @return {boolean} true if a slice was found, otherwise false.
*/
addIntersectingItemsToSelection: function(vX, vY, selection) {
// Does nothing. There's nothing interesting to pick on the viewport
// track.
},
/**
* Adds items intersecting the given range to a selection.
* @param {number} loVX Lower X bound of the interval to search, in
* viewspace.
* @param {number} hiVX Upper X bound of the interval to search, in
* viewspace.
* @param {number} loVY Lower Y bound of the interval to search, in
* viewspace.
* @param {number} hiVY Upper Y bound of the interval to search, in
* viewspace.
* @param {TimelineSelection} selection Selection to which to add hits.
*/
addIntersectingItemsInRangeToSelection: function(
loVX, hiVX, loY, hiY, selection) {
// Does nothing. There's nothing interesting to pick on the viewport
// track.
},
addAllObjectsMatchingFilterToSelection: function(filter, selection) {
}
};
return {
TimelineViewportTrack: TimelineViewportTrack
};
});