import * as util from './util'; /** * Parent class for renderers * * @extends {Observer} */ export default class Drawer extends util.Observer { /** * @param {HTMLElement} container The container node of the wavesurfer instance * @param {WavesurferParams} params The wavesurfer initialisation options */ constructor(container, params) { super(); /** @private */ this.container = container; /** * @type {WavesurferParams} * @private */ this.params = params; /** * The width of the renderer * @type {number} */ this.width = 0; /** * The height of the renderer * @type {number} */ this.height = params.height * this.params.pixelRatio; /** @private */ this.lastPos = 0; /** * The `` element which is added to the container * @type {HTMLElement} */ this.wrapper = null; } /** * Alias of `util.style` * * @param {HTMLElement} el The element that the styles will be applied to * @param {Object} styles The map of propName: attribute, both are used as-is * @return {HTMLElement} el */ style(el, styles) { return util.style(el, styles); } /** * Create the wrapper `` element, style it and set up the events for * interaction */ createWrapper() { this.wrapper = this.container.appendChild( document.createElement('wave') ); this.style(this.wrapper, { display: 'block', position: 'relative', userSelect: 'none', webkitUserSelect: 'none', height: this.params.height + 'px' }); if (this.params.fillParent || this.params.scrollParent) { this.style(this.wrapper, { width: '100%', overflowX: this.params.hideScrollbar ? 'hidden' : 'auto', overflowY: 'hidden' }); } this.setupWrapperEvents(); } /** * Handle click event * * @param {Event} e Click event * @param {?boolean} noPrevent Set to true to not call `e.preventDefault()` * @return {number} Playback position from 0 to 1 */ handleEvent(e, noPrevent) { !noPrevent && e.preventDefault(); const clientX = e.targetTouches ? e.targetTouches[0].clientX : e.clientX; const bbox = this.wrapper.getBoundingClientRect(); const nominalWidth = this.width; const parentWidth = this.getWidth(); let progress; if (!this.params.fillParent && nominalWidth < parentWidth) { progress = ((clientX - bbox.left) * this.params.pixelRatio / nominalWidth) || 0; if (progress > 1) { progress = 1; } } else { progress = ((clientX - bbox.left + this.wrapper.scrollLeft) / this.wrapper.scrollWidth) || 0; } return progress; } /** * @private */ setupWrapperEvents() { this.wrapper.addEventListener('click', e => { const scrollbarHeight = this.wrapper.offsetHeight - this.wrapper.clientHeight; if (scrollbarHeight != 0) { // scrollbar is visible. Check if click was on it const bbox = this.wrapper.getBoundingClientRect(); if (e.clientY >= bbox.bottom - scrollbarHeight) { // ignore mousedown as it was on the scrollbar return; } } if (this.params.interact) { this.fireEvent('click', e, this.handleEvent(e)); } }); this.wrapper.addEventListener('scroll', e => this.fireEvent('scroll', e)); } /** * Draw peaks on the canvas * * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel * rendering * @param {number} length The width of the area that should be drawn * @param {number} start The x-offset of the beginning of the area that * should be rendered * @param {number} end The x-offset of the end of the area that should be * rendered */ drawPeaks(peaks, length, start, end) { if (!this.setWidth(length)) { this.clearWave(); } this.params.barWidth ? this.drawBars(peaks, 0, start, end) : this.drawWave(peaks, 0, start, end); } /** * Scroll to the beginning */ resetScroll() { if (this.wrapper !== null) { this.wrapper.scrollLeft = 0; } } /** * Recenter the viewport at a certain percent of the waveform * * @param {number} percent Value from 0 to 1 on the waveform */ recenter(percent) { const position = this.wrapper.scrollWidth * percent; this.recenterOnPosition(position, true); } /** * Recenter the viewport on a position, either scroll there immediately or * in steps of 5 pixels * * @param {number} position X-offset in pixels * @param {boolean} immediate Set to true to immediately scroll somewhere */ recenterOnPosition(position, immediate) { const scrollLeft = this.wrapper.scrollLeft; const half = ~~(this.wrapper.clientWidth / 2); const maxScroll = this.wrapper.scrollWidth - this.wrapper.clientWidth; let target = position - half; let offset = target - scrollLeft; if (maxScroll == 0) { // no need to continue if scrollbar is not there return; } // if the cursor is currently visible... if (!immediate && -half <= offset && offset < half) { // we'll limit the "re-center" rate. const rate = 5; offset = Math.max(-rate, Math.min(rate, offset)); target = scrollLeft + offset; } // limit target to valid range (0 to maxScroll) target = Math.max(0, Math.min(maxScroll, target)); // no use attempting to scroll if we're not moving if (target != scrollLeft) { this.wrapper.scrollLeft = target; } } /** * Get the current scroll position in pixels * * @return {number} */ getScrollX() { return Math.round(this.wrapper.scrollLeft * this.params.pixelRatio); } /** * Get the width of the container * * @return {number} */ getWidth() { return Math.round(this.container.clientWidth * this.params.pixelRatio); } /** * Set the width of the container * * @param {number} width */ setWidth(width) { if (this.width == width) { return false; } this.width = width; if (this.params.fillParent || this.params.scrollParent) { this.style(this.wrapper, { width: '' }); } else { this.style(this.wrapper, { width: ~~(this.width / this.params.pixelRatio) + 'px' }); } this.updateSize(); return true; } /** * Set the height of the container * * @param {number} height */ setHeight(height) { if (height == this.height) { return false; } this.height = height; this.style(this.wrapper, { height: ~~(this.height / this.params.pixelRatio) + 'px' }); this.updateSize(); return true; } /** * Called by wavesurfer when progress should be renderered * * @param {number} progress From 0 to 1 */ progress(progress) { const minPxDelta = 1 / this.params.pixelRatio; const pos = Math.round(progress * this.width) * minPxDelta; if (pos < this.lastPos || pos - this.lastPos >= minPxDelta) { this.lastPos = pos; if (this.params.scrollParent && this.params.autoCenter) { const newPos = ~~(this.wrapper.scrollWidth * progress); this.recenterOnPosition(newPos); } this.updateProgress(pos); } } /** * This is called when wavesurfer is destroyed */ destroy() { this.unAll(); if (this.wrapper) { if (this.wrapper.parentNode == this.container) { this.container.removeChild(this.wrapper); } this.wrapper = null; } } /* Renderer-specific methods */ /** * Called when the size of the container changes so the renderer can adjust * * @abstract */ updateSize() {} /** * Draw a waveform with bars * * @abstract * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel * rendering * @param {number} channelIndex The index of the current channel. Normally * should be 0 * @param {number} start The x-offset of the beginning of the area that * should be rendered * @param {number} end The x-offset of the end of the area that should be * rendered */ drawBars(peaks, channelIndex, start, end) {} /** * Draw a waveform * * @abstract * @param {number[]|number[][]} peaks Can also be an array of arrays for split channel * rendering * @param {number} channelIndex The index of the current channel. Normally * should be 0 * @param {number} start The x-offset of the beginning of the area that * should be rendered * @param {number} end The x-offset of the end of the area that should be * rendered */ drawWave(peaks, channelIndex, start, end) {} /** * Clear the waveform * * @abstract */ clearWave() {} /** * Render the new progress * * @abstract * @param {number} position X-Offset of progress position in pixels */ updateProgress(position) {} }