wavesurfer.js
Version:
Interactive navigable audio visualization using Web Audio and Canvas
414 lines (362 loc) • 12.2 kB
JavaScript
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();
this.container = util.withOrientation(container, params.vertical);
/**
* @type {WavesurferParams}
*/
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;
this.lastPos = 0;
/**
* The `<wave>` 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 `<wave>` element, style it and set up the events for
* interaction
*/
createWrapper() {
this.wrapper = util.withOrientation(
this.container.appendChild(document.createElement('wave')),
this.params.vertical
);
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%',
cursor: this.params.hideCursor ? 'none' : 'auto',
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 = util.withOrientation(
e.targetTouches ? e.targetTouches[0] : e,
this.params.vertical
).clientX;
const bbox = this.wrapper.getBoundingClientRect();
const nominalWidth = this.width;
const parentWidth = this.getWidth();
const progressPixels = this.getProgressPixels(bbox, clientX);
let progress;
if (!this.params.fillParent && nominalWidth < parentWidth) {
progress = progressPixels *
(this.params.pixelRatio / nominalWidth) || 0;
} else {
progress = (progressPixels + this.wrapper.scrollLeft) /
this.wrapper.scrollWidth || 0;
}
return util.clamp(progress, 0, 1);
}
getProgressPixels(wrapperBbox, clientX) {
if (this.params.rtl) {
return wrapperBbox.right - clientX;
} else {
return clientX - wrapperBbox.left;
}
}
setupWrapperEvents() {
this.wrapper.addEventListener('click', e => {
const orientedEvent = util.withOrientation(e, this.params.vertical);
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 (orientedEvent.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('dblclick', e => {
if (this.params.interact) {
this.fireEvent('dblclick', e, this.handleEvent(e));
}
});
this.wrapper.addEventListener('scroll', e =>
this.fireEvent('scroll', e)
);
}
/**
* Draw peaks on the canvas
*
* @param {number[]|Number.<Array[]>} 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 view-port 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 view-port 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) {
// set rate at which waveform is centered
let rate = this.params.autoCenterRate;
// make rate depend on width of view and length of waveform
rate /= half;
rate *= maxScroll;
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} Horizontal scroll position in pixels
*/
getScrollX() {
let x = 0;
if (this.wrapper) {
const pixelRatio = this.params.pixelRatio;
x = Math.round(this.wrapper.scrollLeft * pixelRatio);
// In cases of elastic scroll (safari with mouse wheel) you can
// scroll beyond the limits of the container
// Calculate and floor the scrollable extent to make sure an out
// of bounds value is not returned
// Ticket #1312
if (this.params.scrollParent) {
const maxScroll = ~~(
this.wrapper.scrollWidth * pixelRatio -
this.getWidth()
);
x = Math.min(maxScroll, Math.max(0, x));
}
}
return x;
}
/**
* Get the width of the container
*
* @return {number} The width of the container
*/
getWidth() {
return Math.round(this.container.clientWidth * this.params.pixelRatio);
}
/**
* Set the width of the container
*
* @param {number} width The new width of the container
* @return {boolean} Whether the width of the container was updated or not
*/
setWidth(width) {
if (this.width == width) {
return false;
}
this.width = width;
if (this.params.fillParent || this.params.scrollParent) {
this.style(this.wrapper, {
width: ''
});
} else {
const newWidth = ~~(this.width / this.params.pixelRatio) + 'px';
this.style(this.wrapper, {
width: newWidth
});
}
this.updateSize();
return true;
}
/**
* Set the height of the container
*
* @param {number} height The new height of the container.
* @return {boolean} Whether the height of the container was updated or not
*/
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 rendered
*
* @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.params.autoCenterImmediately
);
}
this.updateProgress(pos);
}
}
/**
* This is called when wavesurfer is destroyed
*/
destroy() {
this.unAll();
if (this.wrapper) {
if (this.wrapper.parentNode == this.container.domElement) {
this.container.removeChild(this.wrapper.domElement);
}
this.wrapper = null;
}
}
/* Renderer-specific methods */
/**
* Called after cursor related params have changed.
*
* @abstract
*/
updateCursor() {}
/**
* Called when the size of the container changes so the renderer can adjust
*
* @abstract
*/
updateSize() {}
/**
* Draw a waveform with bars
*
* @abstract
* @param {number[]|Number.<Array[]>} 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.<Array[]>} 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) {}
}