@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
245 lines (194 loc) • 7.02 kB
JavaScript
/**
* @file
*
* Defines the {@link TimelineAxis} class.
*
* @module timeline-axis
*/
define([
'./utils',
'konva'
], function(Utils, Konva) {
'use strict';
var LEFT_PADDING = 4;
/**
* Creates the timeline axis shapes and adds them to the given view layer.
*
* @class
* @alias TimelineAxis
*
* @param {WaveformOverview|TimelineZoomView} view
* @param {Object} options
* @param {String} options.axisGridlineColor
* @param {String} options.axisLabelColor
*/
function TimelineAxis(peaks, view, options) {
this._view = view;
var self = this;
peaks.on('playhead.moved', this._onPlayheadMoved.bind(this));
peaks.on('playhead.hidden', this._onPlayheadHidden.bind(this));
self._axisGridlineColor = options.axisGridlineColor;
self._axisLabelColor = options.axisLabelColor;
self._backLayer = new Konva.Layer({
listening: false
});
self._frontLayer = new Konva.Layer({
listening: false
});
self._axisShape = new Konva.Shape({
sceneFunc: function(context) {
self.drawAxis(context, view);
}
});
self._backLayer.add(self._axisShape);
self._timesShape = new Konva.Shape({
sceneFunc: function(context) {
self.drawTimes(context, view);
}
});
self._frontLayer.add(self._timesShape);
}
TimelineAxis.prototype._onPlayheadMoved = function(playheadX, playheadWidth) {
this._maskStart = playheadX + this._view.getFrameOffset();
this._maskEnd = playheadX + this._view.getFrameOffset() + playheadWidth;
this._frontLayer.draw();
};
TimelineAxis.prototype._onPlayheadHidden = function() {
this._maskStart = null;
this._maskEnd = null;
this._frontLayer.draw();
};
TimelineAxis.prototype.draw = function() {
this._backLayer.draw();
this._frontLayer.draw();
};
TimelineAxis.prototype.addBackToStage = function(stage) {
stage.add(this._backLayer);
};
TimelineAxis.prototype.addFrontToStage = function(stage) {
stage.add(this._frontLayer);
};
/**
* Returns number of seconds for each x-axis marker, appropriate for the
* current zoom level, ensuring that markers are not too close together
* and that markers are placed at intuitive time intervals (i.e., every 1,
* 2, 5, 10, 20, 30 seconds, then every 1, 2, 5, 10, 20, 30 minutes, then
* every 1, 2, 5, 10, 20, 30 hours).
*
* @param {WaveformOverview|WaveformZoomView} view
* @returns {Number}
*/
TimelineAxis.prototype.getAxisLabelScale = function(view) {
var baseSecs = 1; // seconds
var steps = [0.1, 0.5, 1, 2, 5, 10, 20, 30];
var minSpacing = 60;
var index = 0;
var secs;
for (;;) {
secs = baseSecs * steps[index];
var pixels = view.timeToPixels(secs);
if (pixels < minSpacing) {
if (++index === steps.length) {
baseSecs *= 60; // seconds -> minutes -> hours
index = 0;
}
}
else {
break;
}
}
return secs;
};
/**
* Draws the time axis and labels onto a view.
*
* @param {Konva.Context} context The context to draw on.
* @param {WaveformOverview|WaveformZoomView} view
*/
TimelineAxis.prototype.drawAxis = function(context, view) {
var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset());
// Time interval between axis markers (seconds)
var axisLabelIntervalSecs = this.getAxisLabelScale(view);
// Time of first axis marker (seconds)
var firstAxisLabelSecs = Utils.roundUpToNearest(currentFrameStartTime, axisLabelIntervalSecs);
// Distance between waveform start time and first axis marker (seconds)
var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime;
// Distance between waveform start time and first axis marker (pixels)
var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs);
context.setAttr('strokeStyle', this._axisGridlineColor);
context.setAttr('lineWidth', 1);
// Set text style
context.setAttr('font', '11px sans-serif');
context.setAttr('fillStyle', this._axisLabelColor);
context.setAttr('textAlign', 'left');
context.setAttr('textBaseline', 'bottom');
var secs = firstAxisLabelSecs;
var x;
var width = view.getWidth();
var height = view.getHeight();
for (;;) {
// Position of axis marker (pixels)
x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs);
if (x >= width) {
break;
}
context.beginPath();
context.moveTo(x + 0.5, 0);
context.lineTo(x + 0.5, height);
context.stroke();
secs += axisLabelIntervalSecs;
}
};
TimelineAxis.prototype.drawTimes = function(context, view) {
var currentFrameStartTime = view.pixelsToTime(view.getFrameOffset());
// Time interval between axis markers (seconds)
var axisLabelIntervalSecs = this.getAxisLabelScale(view);
// Time of first axis marker (seconds)
var firstAxisLabelSecs = Utils.roundUpToNearestPositive(
currentFrameStartTime - 1,
axisLabelIntervalSecs
);
// Distance between waveform start time and first axis marker (seconds)
var axisLabelOffsetSecs = firstAxisLabelSecs - currentFrameStartTime;
// Distance between waveform start time and first axis marker (pixels)
var axisLabelOffsetPixels = view.timeToPixels(axisLabelOffsetSecs);
// Set text style
context.setAttr('font', '11px sans-serif');
context.setAttr('fillStyle', this._axisLabelColor);
context.setAttr('textAlign', 'left');
context.setAttr('textBaseline', 'bottom');
var secs = firstAxisLabelSecs;
var x;
var width = view.getWidth();
for (;;) {
// Position of axis marker (pixels)
x = axisLabelOffsetPixels + view.timeToPixels(secs - firstAxisLabelSecs);
if (x >= width) {
break;
}
var label = Utils.formatTime(secs, false);
var measures = context.measureText(label);
var labelHeight = measures.actualBoundingBoxAscent
- measures.actualBoundingBoxDescent;
var labelWidth = measures.width;
var labelX = x + LEFT_PADDING;
var labelY = labelHeight;
if (!this._labelIsMasked(
labelX + view.getFrameOffset(),
labelX + view.getFrameOffset() + labelWidth
)) {
context.fillText(label, labelX, labelY);
}
secs += axisLabelIntervalSecs;
}
};
TimelineAxis.prototype._labelIsMasked = function(labelStart, labelEnd) {
if (this._maskStart && this._maskEnd) {
if (labelEnd > this._maskStart && labelStart < this._maskEnd) {
return true;
}
}
return false;
};
return TimelineAxis;
});