UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

394 lines (329 loc) 10.4 kB
/** * @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; });