@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
394 lines (329 loc) • 10.4 kB
JavaScript
/**
* @file
*
* Defines the {@link PlayheadLayer} class.
*
* @module playhead-layer
*/
define([
'../utils',
'./invoker',
'konva'
], function(Utils, Invoker, Konva) {
'use strict';
var HANDLE_RADIUS = 10;
/**
* Creates a Konva.Layer that displays a playhead marker.
*
* @class
* @alias PlayheadLayer
*
* @param {Peaks} peaks
* @param {WaveformOverview|WaveformZoomView} view
* @param {Boolean} showTime If <code>true</code> The playback time position
* is shown next to the playhead.
*/
function PlayheadLayer(peaks, view, lines, showTime) {
this._peaks = peaks;
this._view = view;
this._lines = lines;
this._playheadPixel = 0;
this._playheadVisible = false;
this._playheadColor = peaks.options.playheadColor;
this._playheadTextColor = peaks.options.playheadTextColor;
this._time = null;
this._playheadLayer = new Konva.Layer();
this._invoker = new Invoker();
this._throttledBatchDraw = this._invoker.throttleTrailing(
this._playheadLayer.batchDraw.bind(this._playheadLayer)
);
this._activeSegments = {};
this._activeSources = {};
this._createPlayhead(this._playheadColor);
if (showTime) {
this._createPlayheadText(this._playheadTextColor);
}
this.fitToView();
this._peaks.on('handler.segments.remove_all', this._onSegmentsRemoveAll.bind(this));
this._peaks.on('handler.segments.remove', this._onSegmentsRemove.bind(this));
}
PlayheadLayer.prototype._onSegmentsRemoveAll = function() {
this._activeSegments = {};
};
PlayheadLayer.prototype._onSegmentsRemove = function(segments) {
if (this._activeSegments) {
for (var id in segments) {
if (Utils.objectHasProperty(segments, id)) {
var activeSegmentId = this._activeSegments[segments[id].line] ?
this._activeSegments[segments[id].line].id :
null;
if (segments[id].id === activeSegmentId) {
delete this._activeSegments[segments[id].line];
}
}
}
}
};
/**
* Adds the layer to the given {Konva.Stage}.
*
* @param {Konva.Stage} stage
*/
PlayheadLayer.prototype.addToStage = function(stage) {
stage.add(this._playheadLayer);
};
PlayheadLayer.prototype.listening = function(bool) {
this._playheadLayer.listening(bool);
};
/**
* Resizes the playhead UI objects to fit the available space in the
* view.
*/
PlayheadLayer.prototype.fitToView = function() {
var height = this._view.getHeight();
this._playheadLine.points([0.5, 0, 0.5, height]);
// if (this._playheadText) {
// this._playheadText.y(30);
// }
};
/**
* Creates the playhead UI objects.
*
* @private
* @param {String} color
*/
PlayheadLayer.prototype._createPlayhead = function(color) {
// Create with default points, the real values are set in fitToView().
this._playheadLine = new Konva.Line({
stroke: color,
strokeWidth: 3
});
this._playheadHandle = new Konva.RegularPolygon({
x: 0.5,
y: HANDLE_RADIUS / 2,
sides: 3,
fill: color,
strokeWidth: 0,
radius: HANDLE_RADIUS,
rotation: 180
});
var self = this;
this._playheadGroup = new Konva.Group({
x: 0,
y: 0,
draggable: true,
dragBoundFunc: function(pos) {
var time = Math.max(
0,
self._view.pixelsToTime(
pos.x + self._view.getFrameOffset()
)
);
self._view.updateWithAutoScroll(
function() {
time = Math.max(
0,
self._view.pixelsToTime(
self._view.getPointerPosition().x + self._view.getFrameOffset()
)
);
self._onPlayheadDrag(time);
},
function() {
self._onPlayheadDrag(time);
}
);
return {
x: Math.max(pos.x, 0),
y: 0
};
}
});
this._playheadGroup.on('dragstart', this._onPlayheadDragStart.bind(this));
this._playheadGroup.on('dragend', this._onPlayheadDragEnd.bind(this));
this._playheadGroup.add(this._playheadHandle);
this._playheadGroup.add(this._playheadLine);
this._playheadLayer.add(this._playheadGroup);
};
PlayheadLayer.prototype._onPlayheadDrag = function(time) {
if (this._peaks.player._seek(time)) {
this._peaks.emit('playhead.drag', this._peaks.player.getCurrentTime());
}
};
PlayheadLayer.prototype._onPlayheadDragStart = function() {
this._view.enableAutoScroll(false);
this._dragging = true;
};
PlayheadLayer.prototype._onPlayheadDragEnd = function() {
this._view.enableAutoScroll(true);
this._dragging = false;
if (this._playheadGroup._scrollingInterval) {
clearInterval(this._playheadGroup._scrollingInterval);
this._playheadGroup._scrollingInterval = null;
}
this._peaks.emit('playhead.dragend', this._peaks.player.getCurrentTime());
};
PlayheadLayer.prototype._createPlayheadText = function(color) {
// Create with default y, the real value is set in fitToView().
this._playheadText = new Konva.Text({
x: 13,
y: 10,
text: '00:00:00',
fontSize: 11,
fontFamily: 'sans-serif',
fill: color,
align: 'right',
listening: false
});
this._playheadGroup.add(this._playheadText);
};
/**
* Updates the playhead position.
*
* @param {Number} time Current playhead position, in seconds.
*/
PlayheadLayer.prototype.updatePlayheadTime = function(time) {
this._syncPlayhead(time);
};
/**
* Updates the playhead position.
*
* @private
* @param {Number} time Current playhead position, in seconds.
*/
PlayheadLayer.prototype._syncPlayhead = function(time) {
const pixelHasChanged = this._updatePlayheadPixel(time);
const timeHasChanged = this._time !== time;
if (timeHasChanged) {
this._updateActiveSegmentsAndSources(time);
this._updatePlayheadText(time);
}
this._time = time;
if (pixelHasChanged || timeHasChanged) {
this.batchDraw();
}
};
PlayheadLayer.prototype.batchDraw = function() {
this._throttledBatchDraw();
};
/**
* Update cached pixel values and position of the playhead group.
* @private
* @param {Number} time
* @returns {Number} playheadX localized to frame (without frameOffset)
*/
PlayheadLayer.prototype._updatePlayheadPixel = function(time) {
const pixelIndex = this._view.timeToPixels(time);
const frameOffset = this._view.timeToPixels(this._view.getTimeOffset());
this._playheadPixel = pixelIndex;
const playheadX = this._playheadPixel - frameOffset;
const oldX = this._playheadGroup.getAttr('x');
const shouldUpdateX = oldX !== playheadX;
if (shouldUpdateX) {
this._playheadGroup.setAttr('x', playheadX);
}
return shouldUpdateX;
};
/**
* Compute and emit sources/segments enter/exit events.
* @private
* @param {Number} time
*/
PlayheadLayer.prototype._updateActiveSegmentsAndSources = function(time) {
const lineGroups = this._lines.getLineGroupsById();
for (var lineId in lineGroups) {
if (Utils.objectHasProperty(lineGroups, lineId)) {
const lineGroup = lineGroups[lineId];
if (lineGroup.isSegmentsLine()) {
this._updateActiveSegment(lineGroup.getSegmentsGroup(), lineId, time);
}
else {
this._updateActiveSource(lineGroup, lineId, time);
}
}
}
};
/**
* Compute and emit segment enter/exit events for a specific segment group.
* @private
* @param {SegmentsGroup} segmentsGroup
* @param {Number} lineId
* @param {Number} time
*/
PlayheadLayer.prototype._updateActiveSegment = function(segmentsGroup, lineId, time) {
const newActiveSegment = segmentsGroup.getActiveSegment(time);
if (newActiveSegment !== this._activeSegments[lineId]) {
if (this._activeSegments[lineId]) {
this._peaks.emit('segments.exit', this._activeSegments[lineId]);
delete this._activeSegments[lineId];
}
if (newActiveSegment) {
this._peaks.emit('segments.enter', newActiveSegment);
this._activeSegments[lineId] = newActiveSegment;
}
}
};
/**
* Compute and emit source enter/exit events for a specific source line group.
* @private
* @param {LineGroup} sourcesLineGroup
* @param {Number} lineId
* @param {Number} time
*/
PlayheadLayer.prototype._updateActiveSource = function(sourcesLineGroup, lineId, time) {
const newActiveSource = sourcesLineGroup.getActiveSource(time);
if (newActiveSource !== this._activeSources[lineId]) {
if (this._activeSources[lineId]) {
this._peaks.emit('sources.exit', this._activeSources[lineId]);
delete this._activeSources[lineId];
}
if (newActiveSource) {
this._peaks.emit('sources.enter', newActiveSource);
this._activeSources[lineId] = newActiveSource;
}
}
};
/**
* Update the playhead time label and emit playhead.moved event.
* @private
* @param {Number} time
*/
PlayheadLayer.prototype._updatePlayheadText = function(time) {
var text = Utils.formatTime(time, false);
this._playheadText.setText(text);
};
/**
* Returns the position of the playhead marker, in pixels relative to the
* left hand side of the waveform view.
*
* @return {Number}
*/
PlayheadLayer.prototype.getPlayheadOffset = function() {
return this._playheadPixel - this._view.getFrameOffset();
};
PlayheadLayer.prototype.getPlayheadPixel = function() {
return this._playheadPixel;
};
PlayheadLayer.prototype.showPlayheadTime = function(show) {
var updated = false;
if (show) {
if (!this._playheadText) {
// Create it
this._createPlayheadText(this._playheadTextColor);
updated = true;
}
}
else {
if (this._playheadText) {
this._playheadText.remove();
this._playheadText.destroy();
this._playheadText = null;
updated = true;
}
}
if (updated) {
this.batchDraw();
}
};
return PlayheadLayer;
});