UNPKG

@checksub_team/peaks_timeline

Version:

JavaScript UI component for displaying audio waveforms

907 lines (710 loc) 26.3 kB
/** * @file * * Defines the {@link TimelineZoomView} class. * * @module timeline-zoomview */ define([ './mouse-drag-handler', './playhead-layer', './sources-layer', './mode-layer', './timeline-axis', './utils', 'konva' ], function( MouseDragHandler, PlayheadLayer, SourcesLayer, ModeLayer, TimelineAxis, Utils, Konva) { 'use strict'; /** * Creates a zoomable timeline view. * * @class * @alias TimelineZoomView * * @param {HTMLElement} container * @param {Peaks} peaks */ function TimelineZoomView(container, peaks) { var self = this; self._container = container; self._peaks = peaks; // Bind event handlers self._onTimeUpdate = self._onTimeUpdate.bind(self); self._onSeek = self._onSeek.bind(self); self._onPlay = self._onPlay.bind(self); self._onPause = self._onPause.bind(self); self._onWindowResize = self._onWindowResize.bind(self); self._onKeyboardLeft = self._onKeyboardLeft.bind(self); self._onKeyboardRight = self._onKeyboardRight.bind(self); self._onKeyboardShiftLeft = self._onKeyboardShiftLeft.bind(self); self._onKeyboardShiftRight = self._onKeyboardShiftRight.bind(self); self._onDefaultMode = self._onDefaultMode.bind(self); self._onCutMode = self._onCutMode.bind(self); // Register event handlers self._peaks.on('timeline.update', self._onTimeUpdate); self._peaks.on('timeline.seek', self._onTimeUpdate); self._peaks.on('playhead.drag', self._onTimeUpdate); self._peaks.on('playhead.dragend', self._onTimeUpdate); self._peaks.on('user_seek', self._onSeek); self._peaks.on('timeline.play', self._onPlay); self._peaks.on('timeline.pause', self._onPause); self._peaks.on('window_resize', self._onWindowResize); self._peaks.on('keyboard.left', self._onKeyboardLeft); self._peaks.on('keyboard.right', self._onKeyboardRight); self._peaks.on('keyboard.shift_left', self._onKeyboardShiftLeft); self._peaks.on('keyboard.shift_right', self._onKeyboardShiftRight); self._peaks.on('default_mode', self._onDefaultMode); self._peaks.on('cut_mode', self._onCutMode); self._enableAutoScroll = true; self._amplitudeScale = 1.0; self._options = peaks.options; self._sources = peaks.sources; self._timelineLength = 0; self._timeToPixelsScale = self._options.initialZoomLevel; self._timeToPixelsMinScale = self._options.minScale; self._resizeTimeoutId = null; self._isFocused = false; self._isClickable = true; self._width = container.clientWidth; self._height = container.clientHeight || self._options.height; self._originalWidth = self._width; // The pixel offset of the current frame being displayed self.setFrameOffset(0); self._frameOffsetY = 0; self._preventWrappingChange = false; container.oncontextmenu = function() { return false; }; container.style.whiteSpace = 'nowrap'; var lineIndicatorContainer = document.createElement('div'); var stageContainer = document.createElement('div'); lineIndicatorContainer.id = 'line-indicator-container'; stageContainer.id = 'stage-container'; stageContainer.style.display = 'inline-block'; stageContainer.style.zIndex = 0; lineIndicatorContainer.style.display = 'inline-block'; lineIndicatorContainer.style.zIndex = 1; container.appendChild(lineIndicatorContainer); container.appendChild(stageContainer); self._stage = new Konva.Stage({ container: stageContainer, width: self._width - self._peaks.options.lineIndicatorWidth, height: self._height }); self._width -= self._peaks.options.lineIndicatorWidth; self._axis = new TimelineAxis(self._peaks, self, { axisGridlineColor: this._options.axisGridlineColor, axisLabelColor: this._options.axisLabelColor }); self._axis.addBackToStage(self._stage); self._sourcesLayer = new SourcesLayer(peaks, self, true); self._sourcesLayer.addToStage(self._stage); self._axis.addFrontToStage(self._stage); self._playheadLayer = new PlayheadLayer( peaks, self, self._sourcesLayer, self._options.showPlayheadTime ); self._playheadLayer.addToStage(self._stage); var time = self._peaks.player.getCurrentTime(); self._syncPlayhead(time); self._hoveredElement = null; self._modeLayer = new ModeLayer(peaks, self, self._playheadLayer, self._stage, 'default'); self._modeLayer.addToStage(self._stage); self._mouseDragHandler = new MouseDragHandler(self._stage, { onMouseDown: function(mousePosX, mousePosY) { this.initialFrameOffset = self._frameOffset; this.initialFrameOffsetY = self._frameOffsetY; this.mouseDownX = mousePosX; this.mouseDownY = mousePosY; self.enableAutoScroll(false); }, onMouseMove: function(mousePosX, mousePosY) { // Moving the mouse to the left increases the time position of the // left-hand edge of the visible waveform. var diff = this.mouseDownX - mousePosX; var diffY = this.mouseDownY - mousePosY; var newFrameOffset = null; if (self.isListening()) { newFrameOffset = Math.max( this.initialFrameOffset + diff, 0 ); } var height = self._sourcesLayer._lines.height() - self._height; var newFrameOffsetY = 0; if (self._peaks.options.enableVerticalScrolling && height > 0) { newFrameOffsetY = Utils.clamp( this.initialFrameOffsetY + diffY, 0, height ); } else { self._frameOffsetY = 0; } if ((newFrameOffset !== this.initialFrameOffset) || (newFrameOffsetY !== this.initialFrameOffsetY)) { self.updateTimeline( newFrameOffset, newFrameOffsetY ); } }, onMouseUp: function() { self.enableAutoScroll(true); } }); this._stage.on('mouseover', function() { self._isFocused = true; }); this._stage.on('mouseout', function() { self._isFocused = false; }); this._stage.on('click', function(event) { self._isFocused = true; if (!self._isClickable) { return; } self._peaks.emit('zoomview.click', event); // Set playhead position only on click release, when not dragging. if (self._modeLayer.getCurrentMode() === 'default' && !self._mouseDragHandler.isDragging()) { var mouseDownX = Math.floor(self._stage.getPointerPosition().x); var pixelIndex = self._frameOffset + mouseDownX; var time = self.pixelsToTime(pixelIndex); if (!(self._options.blockUpdatingOnMouseClickWithMetaKey && event.metaKey) && !(self._options.blockUpdatingOnMouseClickWithCtrlKey && event.ctrlKey)) { self.updateTimeline(pixelIndex - mouseDownX); self._peaks.player.seek(time); } self._peaks.emit('zoomview.updateTime', event, time); } }); this._stage.on('dblclick', function(event) { var mousePosX = event.evt.layerX; var pixelIndex = self._frameOffset + mousePosX; var time = self.pixelsToTime(pixelIndex); self._peaks.emit('zoomview.dblclick', time); }); this._stage.on('wheel', function(e) { // prevent parent scrolling e.evt.preventDefault(); if (self._mouseDragHandler.isDragging()) { return; } if (self._peaks.keyboardHandler.isCtrlCmdPressed()) { if (e.evt.deltaY > 0) { self.setZoom( self.getTimeToPixelsScale() + Math.floor(self.getTimeToPixelsScale() / 10) + 1 ); } else if ((e.evt.deltaY < 0)) { self.setZoom( self.getTimeToPixelsScale() - Math.floor(self.getTimeToPixelsScale() / 10) + 1 ); } self.updateTimelineLength(); } else { var diff = e.evt.deltaX * 2; var diffY = e.evt.deltaY / 2; var newFrameOffset = null; if (self.isListening()) { newFrameOffset = Utils.clamp( self.getFrameOffset() + diff, 0, self._timelineLength - self._width ); } var height = self._sourcesLayer._lines.height() - self._height; var newFrameOffsetY = 0; if (self._peaks.options.enableVerticalScrolling && height > 0) { newFrameOffsetY = Utils.clamp( self.getFrameOffsetY() + diffY, 0, height ); } else { self._frameOffsetY = 0; } if ((newFrameOffset !== self.getFrameOffset()) || (newFrameOffsetY !== self.getFrameOffsetY())) { self.updateTimeline( newFrameOffset, newFrameOffsetY ); } } }); window.addEventListener('mouseup', this._mouseUp.bind(this), false); window.addEventListener('touchend', this._mouseUp.bind(this), false); window.addEventListener('blur', this._mouseUp.bind(this), false); } TimelineZoomView.prototype._mouseUp = function() { this.clearScrollingInterval(); }; TimelineZoomView.prototype.setClickable = function(clickable) { this._isClickable = clickable; }; TimelineZoomView.prototype.getSelectedElements = function() { return Object.values(this._modeLayer.getSelectedElements()); }; TimelineZoomView.prototype.updateWithAutoScroll = function(updateInInterval, updateOutInterval) { var self = this; var posX = this.getPointerPosition().x; var threshold = Math.round(this._peaks.options.autoScrollThreshold * this.getWidth()); this._limited = 0; if (posX < threshold) { this._limited = Math.round(-30 * Math.min(1, (threshold - posX) / threshold)); } else if (posX > this.getWidth() - threshold) { this._limited = Math.round( 30 * Math.min(1, (posX - (this.getWidth() - threshold)) / threshold) ); } if (this._limited && self.getFrameOffset() > 0 || this._limited > 0) { if (!this._scrollingInterval) { this._scrollingInterval = setInterval( function() { var newOffset = self.getFrameOffset() + self._limited; if (newOffset < 0) { self.updateTimeline(0, null, false); clearInterval(self._scrollingInterval); self._scrollingInterval = null; } else { self.updateTimeline(self.getFrameOffset() + self._limited, null, false); } updateInInterval(); }, 10 ); } } else { this.clearScrollingInterval(); if (updateOutInterval) { updateOutInterval(); } else { updateInInterval(); } } }; TimelineZoomView.prototype.clearScrollingInterval = function() { if (this._scrollingInterval) { clearInterval(this._scrollingInterval); this._scrollingInterval = null; } }; TimelineZoomView.prototype.getCurrentMode = function() { return this._modeLayer.getCurrentMode(); }; TimelineZoomView.prototype.overrideInteractions = function(bool, areInteractionsAllowed) { this._sourcesLayer._lines.overrideInteractions(bool, areInteractionsAllowed); this._playheadLayer.listening(areInteractionsAllowed); this._sourcesLayer.stopDrag(); this._sourcesLayer.draw(); }; TimelineZoomView.prototype.allowInteractions = function(forSources, forSegments) { this._sourcesLayer._lines.allowInteractions(forSources, forSegments); this._sourcesLayer.stopDrag(); this._sourcesLayer.draw(); }; TimelineZoomView.prototype.getSelectedElements = function() { return this._modeLayer.getSelectedElements(); }; TimelineZoomView.prototype.getSourceGroupById = function(sourceId) { return this._sourcesLayer.getSourceGroupById(sourceId); }; TimelineZoomView.prototype.selectSourceById = function(sourceId) { const sourceGroup = this._sourcesLayer.getSourceGroupById(sourceId); if (sourceGroup) { this._modeLayer.selectElements([sourceGroup.getSource()], false); } }; TimelineZoomView.prototype.selectSourcesOnLineAfter = function(lineId, time) { const sources = this._sourcesLayer.getSourcesOnLineAfter(lineId, time); if (sources) { this._modeLayer.selectElements(sources, false); } }; TimelineZoomView.prototype.deselectAll = function(notify) { this._modeLayer.deselectDifference([], notify); }; TimelineZoomView.prototype.isListening = function() { return this._stage.listening(); }; TimelineZoomView.prototype.isFocused = function() { return this._isFocused; }; TimelineZoomView.prototype.drawSourcesLayer = function() { this._sourcesLayer.draw(); }; TimelineZoomView.prototype.getSegmentsGroup = function() { return this._sourcesLayer.getSegmentsGroup(); }; TimelineZoomView.prototype.getTimeToPixelsScale = function() { return this._timeToPixelsScale; }; TimelineZoomView.prototype.getTimeToPixelsMaxZoom = function() { return this._options.zoomRange[1]; }; TimelineZoomView.prototype.setTimeToPixelsMaxZoom = function(value) { this._options.zoomRange[1] = value; if (value < this._timeToPixelsScale) { this.setZoom(value); } }; TimelineZoomView.prototype.getTimeToPixelsMinScale = function() { return this._timeToPixelsMinScale; }; TimelineZoomView.prototype.getName = function() { return 'zoomview'; }; TimelineZoomView.prototype._onTimeUpdate = function(time) { if (this._mouseDragHandler.isDragging()) { return; } this._syncPlayhead(time); }; TimelineZoomView.prototype._onSeek = function(time) { var frameIndex = this.timeToPixels(time); this.updateTimeline(frameIndex - Math.floor(this._width / 2)); this._playheadLayer.updatePlayheadTime(time); }; TimelineZoomView.prototype._onPlay = function(time) { this._playheadLayer.updatePlayheadTime(time); this.enableAutoScroll(true); }; TimelineZoomView.prototype._onPause = function(time) { this._playheadLayer.stop(time); this.enableAutoScroll(true); }; TimelineZoomView.prototype._onWindowResize = function() { var self = this; var width = self._container.clientWidth; self._width = width; self._stage.width(width); self.updateTimeline(self._frameOffset, self._frameOffsetY); }; TimelineZoomView.prototype._onKeyboardLeft = function() { if (this.isFocused()) { this._keyboardScroll(-1, false); } }; TimelineZoomView.prototype._onKeyboardRight = function() { if (this.isFocused()) { this._keyboardScroll(1, false); } }; TimelineZoomView.prototype._onKeyboardShiftLeft = function() { if (this.isFocused()) { this._keyboardScroll(-1, true); } }; TimelineZoomView.prototype._onKeyboardShiftRight = function() { if (this.isFocused()) { this._keyboardScroll(1, true); } }; TimelineZoomView.prototype._onDefaultMode = function() { this.preventWrappingChange(false); this._modeLayer.setMode('default'); }; TimelineZoomView.prototype._onCutMode = function() { this.preventWrappingChange(true); this._modeLayer.setMode('cut'); }; TimelineZoomView.prototype.preventWrappingChange = function(bool) { this._preventWrappingChange = bool; }; TimelineZoomView.prototype.isPreventingWrappingChange = function() { return this._preventWrappingChange; }; TimelineZoomView.prototype.getHoveredElement = function() { return this._hoveredElement; }; TimelineZoomView.prototype.setHoveredElement = function(element) { this._hoveredElement = element; }; TimelineZoomView.prototype._keyboardScroll = function(direction, large) { var increment; if (large) { increment = direction * this._width; } else { increment = direction * this.timeToPixels(this._options.nudgeIncrement); } this.updateTimeline(this._frameOffset + increment); }; TimelineZoomView.prototype._syncPlayhead = function(time) { this._playheadLayer.updatePlayheadTime(time); if (this._enableAutoScroll) { // Check for the playhead reaching the right-hand side of the window. var pixelIndex = this.timeToPixels(time); var threshold = Math.round(0.1 * this._width); var endThreshold = this._frameOffset + this._width - threshold; if (pixelIndex >= endThreshold || pixelIndex < this._frameOffset) { // Put the playhead at 100 pixels from the left edge this.setFrameOffset(pixelIndex - threshold); if (this._frameOffset < 0) { this.setFrameOffset(0); } this.updateTimeline(this._frameOffset); } } }; TimelineZoomView.prototype.showPlayhead = function() { var newFrameOffset = this._playheadLayer.getPlayheadPixel() - Math.round(0.1 * this._width); this.setFrameOffset(newFrameOffset); this.updateTimeline(this._frameOffset); this.enableAutoScroll(true); }; /** * Changes the zoom level. * * @param {Number} scale The new zoom level, in samples per pixel. */ TimelineZoomView.prototype._getScale = function(duration) { return duration * this._data.sample_rate / this._width; }; TimelineZoomView.prototype.setZoom = function(newScale) { newScale = Math.min(Math.max(this._options.zoomRange[0], newScale), this._options.zoomRange[1]); var currentTime = this._peaks.player.getCurrentTime(); var apexTime; var playheadOffsetPixels = this._playheadLayer.getPlayheadOffset(); if (playheadOffsetPixels >= 0 && playheadOffsetPixels < this._width) { // Playhead is visible. Change the zoom level while keeping the // playhead at the same position in the window. apexTime = currentTime; } else { // Playhead is not visible. Change the zoom level while keeping the // centre of the window at the same position in the waveform. playheadOffsetPixels = this._width / 2; apexTime = this.pixelsToTime(this._frameOffset + playheadOffsetPixels); } var prevScale = this._timeToPixelsScale; this._timeToPixelsScale = newScale; var apexPixel = this.timeToPixels(apexTime); this.setFrameOffset(apexPixel - playheadOffsetPixels); this.updateTimeline(this._frameOffset, undefined, undefined, true); this._sourcesLayer.rescale(true); // Update the playhead position after zooming. this._playheadLayer.updatePlayheadTime(currentTime); this._peaks.emit('zoom.update', newScale, prevScale); return true; }; TimelineZoomView.prototype.getTimelineLength = function() { return this._timelineLength; }; TimelineZoomView.prototype.updateTimelineLength = function() { this._timelineLength = this._sourcesLayer.getLength() + this._peaks.options.horizontalPadding; }; TimelineZoomView.prototype.setTimelineLength = function(length) { this._timelineLength = length; }; TimelineZoomView.prototype.getPointerPosition = function() { return this._stage.getPointerPosition(); }; TimelineZoomView.prototype.getStartTime = function() { return this.pixelsToTime(this._frameOffset); }; TimelineZoomView.prototype.getEndTime = function() { return this.pixelsToTime(this._frameOffset + this._width); }; TimelineZoomView.prototype.getLineByPosition = function(pos) { return this._sourcesLayer.getLineByPosition(pos); }; TimelineZoomView.prototype.setStartTime = function(time) { if (time < 0) { time = 0; } this.updateTimeline(this.timeToPixels(time)); }; /** * Returns the pixel index for a given time, for the current zoom level. * * @param {Number} time Time, in seconds. * @returns {Number} Pixel index. */ TimelineZoomView.prototype.timeToPixels = function(time) { return Math.round(time * this._timeToPixelsScale); }; /** * Returns the time for a given pixel index, for the current zoom level. * * @param {Number} pixels Pixel index. * @returns {Number} Time, in seconds. */ TimelineZoomView.prototype.pixelsToTime = function(pixels) { return Utils.roundTime(pixels / this._timeToPixelsScale); }; /** * @returns {Number} The start position of the waveform shown in the view, * in pixels. */ TimelineZoomView.prototype.getFrameOffset = function() { return this._frameOffset; }; TimelineZoomView.prototype.setFrameOffset = function(newFrameOffset) { newFrameOffset = Math.max(0, newFrameOffset); this._frameOffset = newFrameOffset; this._timeOffset = this.pixelsToTime(this._frameOffset); }; TimelineZoomView.prototype.getTimeOffset = function() { return this._timeOffset; }; TimelineZoomView.prototype.setTimeOffset = function(newTimeOffset) { this._timeOffset = newTimeOffset; this._frameOffset = this.timeToPixels(this._timeOffset); }; /** * @returns {Number} The offset on Y, * in pixels. */ TimelineZoomView.prototype.getFrameOffsetY = function() { return this._frameOffsetY; }; /** * @returns {Number} The width of the stage, in pixels. */ TimelineZoomView.prototype.getWidth = function() { return this._width; }; /** * @returns {Number} The width of the whole view, in pixels. */ TimelineZoomView.prototype.getOriginalWidth = function() { return this._originalWidth; }; /** * @returns {Number} The height of the view, in pixels. */ TimelineZoomView.prototype.getHeight = function() { return this._height; }; /** * Adjusts the amplitude scale of waveform shown in the view, which allows * users to zoom the waveform vertically. * * @param {Number} scale The new amplitude scale factor */ TimelineZoomView.prototype.setAmplitudeScale = function(scale) { if (!Utils.isNumber(scale) || !Number.isFinite(scale)) { throw new Error('view.setAmplitudeScale(): Scale must be a valid number'); } this._amplitudeScale = scale; }; TimelineZoomView.prototype.getAmplitudeScale = function() { return this._amplitudeScale; }; /** * Updates the region of waveform shown in the view. * * @param {Number} frameOffset The new frame offset, in pixels. */ TimelineZoomView.prototype.updateTimeline = function(frameOffset, frameOffsetY, fixPlayhead, ignoreRescale) { var frameStartTime = null; var frameEndTime = null; if (frameOffset !== undefined && frameOffset !== null) { this.setFrameOffset(frameOffset); frameStartTime = this.pixelsToTime(this._frameOffset); frameEndTime = this.pixelsToTime(this._frameOffset + this._width); if (!fixPlayhead) { // Display playhead if it is within the zoom frame width. var playheadPixel = this._playheadLayer.getPlayheadPixel(); this._playheadLayer.updatePlayheadTime(this.pixelsToTime(playheadPixel)); } this._axis.draw(); this._peaks.emit('zoomview.displaying', frameStartTime, frameEndTime); } if (frameOffsetY !== undefined && frameOffsetY !== null) { this._frameOffsetY = frameOffsetY; } if (frameStartTime === null) { frameStartTime = this.pixelsToTime(this._frameOffset); } if (frameEndTime === null) { frameEndTime = this.pixelsToTime(this._frameOffset + this._width); } if (!ignoreRescale) { this._sourcesLayer.rescale(); } this._sourcesLayer.updateSources(frameStartTime, frameEndTime); }; TimelineZoomView.prototype.toggleMainCursor = function(on, type) { this._isMainCursorToggled = on; this._stage.container().style.cursor = on ? type : this._nativeCursor; }; TimelineZoomView.prototype.setCursor = function(type) { this._nativeCursor = type; if (!this._isMainCursorToggled) { this._stage.container().style.cursor = type; } }; TimelineZoomView.prototype.getCursor = function() { return this._stage.container().style.cursor; }; TimelineZoomView.prototype.showPlayheadTime = function(show) { this._playheadLayer.showPlayheadTime(show); }; TimelineZoomView.prototype.enableAutoScroll = function(enable) { this._enableAutoScroll = enable; }; TimelineZoomView.prototype.fitToContainer = function() { if (this._container.clientWidth === 0 && this._container.clientHeight === 0) { return; } var shouldUpdate = false; if (this._container.clientWidth - this._peaks.options.lineIndicatorWidth !== this._width) { this._width = this._container.clientWidth - this._peaks.options.lineIndicatorWidth; this._stage.width(this._width); shouldUpdate = true; } if (this._container.clientHeight !== this._height) { this._height = this._container.clientHeight; this._stage.height(this._height); shouldUpdate = true; } if (shouldUpdate) { this._sourcesLayer.fitToView(); this._playheadLayer.fitToView(); this.updateTimeline(this._frameOffset); this._stage.draw(); } }; TimelineZoomView.prototype.getFullHeight = function() { return this._sourcesLayer.getFullHeight(); }; TimelineZoomView.prototype.destroy = function() { if (this._resizeTimeoutId) { clearTimeout(this._resizeTimeoutId); this._resizeTimeoutId = null; } // Unregister event handlers this._peaks.off('player_time_update', this._onTimeUpdate); this._peaks.off('user_seek', this._onSeek); this._peaks.off('player_play', this._onPlay); this._peaks.off('player_pause', this._onPause); this._peaks.off('window_resize', this._onWindowResize); this._peaks.off('keyboard.left', this._onKeyboardLeft); this._peaks.off('keyboard.right', this._onKeyboardRight); this._peaks.off('keyboard.shift_left', this._onKeyboardShiftLeft); this._peaks.off('keyboard.shift_right', this._onKeyboardShiftRight); this._peaks.off('default_mode', this._onDefaultMode); this._peaks.off('cut_mode', this._onCutMode); if (this._stage) { this._stage.destroy(); this._stage = null; } }; return TimelineZoomView; });