UNPKG

wavesurfer.js

Version:

Interactive navigable audio visualization using Web Audio and Canvas

428 lines (390 loc) 13.3 kB
/** * @since 3.0.0 */ import style from './util/style'; import getId from './util/get-id'; /** * The `CanvasEntry` class represents an element consisting of a wave `canvas` * and an (optional) progress wave `canvas`. * * The `MultiCanvas` renderer uses one or more `CanvasEntry` instances to * render a waveform, depending on the zoom level. */ export default class CanvasEntry { constructor() { /** * The wave node * * @type {HTMLCanvasElement} */ this.wave = null; /** * The wave canvas rendering context * * @type {CanvasRenderingContext2D} */ this.waveCtx = null; /** * The (optional) progress wave node * * @type {HTMLCanvasElement} */ this.progress = null; /** * The (optional) progress wave canvas rendering context * * @type {CanvasRenderingContext2D} */ this.progressCtx = null; /** * Start of the area the canvas should render, between 0 and 1 * * @type {number} */ this.start = 0; /** * End of the area the canvas should render, between 0 and 1 * * @type {number} */ this.end = 1; /** * Unique identifier for this entry * * @type {string} */ this.id = getId( typeof this.constructor.name !== 'undefined' ? this.constructor.name.toLowerCase() + '_' : 'canvasentry_' ); /** * Canvas 2d context attributes * * @type {object} */ this.canvasContextAttributes = {}; } /** * Store the wave canvas element and create the 2D rendering context * * @param {HTMLCanvasElement} element The wave `canvas` element. */ initWave(element) { this.wave = element; this.waveCtx = this.wave.getContext('2d', this.canvasContextAttributes); } /** * Store the progress wave canvas element and create the 2D rendering * context * * @param {HTMLCanvasElement} element The progress wave `canvas` element. */ initProgress(element) { this.progress = element; this.progressCtx = this.progress.getContext( '2d', this.canvasContextAttributes ); } /** * Update the dimensions * * @param {number} elementWidth Width of the entry * @param {number} totalWidth Total width of the multi canvas renderer * @param {number} width The new width of the element * @param {number} height The new height of the element */ updateDimensions(elementWidth, totalWidth, width, height) { // where the canvas starts and ends in the waveform, represented as a // decimal between 0 and 1 this.start = this.wave.offsetLeft / totalWidth || 0; this.end = this.start + elementWidth / totalWidth; // set wave canvas dimensions this.wave.width = width; this.wave.height = height; let elementSize = { width: elementWidth + 'px' }; style(this.wave, elementSize); if (this.hasProgressCanvas) { // set progress canvas dimensions this.progress.width = width; this.progress.height = height; style(this.progress, elementSize); } } /** * Clear the wave and progress rendering contexts */ clearWave() { // wave this.waveCtx.clearRect( 0, 0, this.waveCtx.canvas.width, this.waveCtx.canvas.height ); // progress if (this.hasProgressCanvas) { this.progressCtx.clearRect( 0, 0, this.progressCtx.canvas.width, this.progressCtx.canvas.height ); } } /** * Set the fill styles for wave and progress * @param {string|string[]} waveColor Fill color for the wave canvas, * or an array of colors to apply as a gradient * @param {?string|string[]} progressColor Fill color for the progress canvas, * or an array of colors to apply as a gradient */ setFillStyles(waveColor, progressColor) { this.waveCtx.fillStyle = this.getFillStyle(this.waveCtx, waveColor); if (this.hasProgressCanvas) { this.progressCtx.fillStyle = this.getFillStyle(this.progressCtx, progressColor); } } /** * Utility function to handle wave color arguments * * When the color argument type is a string or CanvasGradient instance, * it will be returned as is. Otherwise, it will be treated as an array, * and a new CanvasGradient will be returned * * @since 6.0.0 * @param {CanvasRenderingContext2D} ctx Rendering context of target canvas * @param {string|string[]|CanvasGradient} color Either a single fill color * for the wave canvas, an existing CanvasGradient instance, or an array * of colors to apply as a gradient * @returns {string|CanvasGradient} Returns a string fillstyle value, or a * canvas gradient */ getFillStyle(ctx, color) { if (typeof color == 'string' || color instanceof CanvasGradient) { return color; } const waveGradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.height); color.forEach((value, index) => waveGradient.addColorStop((index / color.length), value)); return waveGradient; } /** * Set the canvas transforms for wave and progress * * @param {boolean} vertical Whether to render vertically */ applyCanvasTransforms(vertical) { if (vertical) { // Reflect the waveform across the line y = -x this.waveCtx.setTransform(0, 1, 1, 0, 0, 0); if (this.hasProgressCanvas) { this.progressCtx.setTransform(0, 1, 1, 0, 0, 0); } } } /** * Draw a rectangle for wave and progress * * @param {number} x X start position * @param {number} y Y start position * @param {number} width Width of the rectangle * @param {number} height Height of the rectangle * @param {number} radius Radius of the rectangle */ fillRects(x, y, width, height, radius) { this.fillRectToContext(this.waveCtx, x, y, width, height, radius); if (this.hasProgressCanvas) { this.fillRectToContext( this.progressCtx, x, y, width, height, radius ); } } /** * Draw the actual rectangle on a `canvas` element * * @param {CanvasRenderingContext2D} ctx Rendering context of target canvas * @param {number} x X start position * @param {number} y Y start position * @param {number} width Width of the rectangle * @param {number} height Height of the rectangle * @param {number} radius Radius of the rectangle */ fillRectToContext(ctx, x, y, width, height, radius) { if (!ctx) { return; } if (radius) { this.drawRoundedRect(ctx, x, y, width, height, radius); } else { ctx.fillRect(x, y, width, height); } } /** * Draw a rounded rectangle on Canvas * * @param {CanvasRenderingContext2D} ctx Canvas context * @param {number} x X-position of the rectangle * @param {number} y Y-position of the rectangle * @param {number} width Width of the rectangle * @param {number} height Height of the rectangle * @param {number} radius Radius of the rectangle * * @return {void} * @example drawRoundedRect(ctx, 50, 50, 5, 10, 3) */ drawRoundedRect(ctx, x, y, width, height, radius) { if (height === 0) { return; } // peaks are float values from -1 to 1. Use absolute height values in // order to correctly calculate rounded rectangle coordinates if (height < 0) { height *= -1; y -= height; } ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo( x + width, y + height, x + width - radius, y + height ); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); ctx.fill(); } /** * Render the actual wave and progress lines * * @param {number[]} peaks Array with peaks data * @param {number} absmax Maximum peak value (absolute) * @param {number} halfH Half the height of the waveform * @param {number} offsetY Offset to the top * @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 */ drawLines(peaks, absmax, halfH, offsetY, start, end) { this.drawLineToContext( this.waveCtx, peaks, absmax, halfH, offsetY, start, end ); if (this.hasProgressCanvas) { this.drawLineToContext( this.progressCtx, peaks, absmax, halfH, offsetY, start, end ); } } /** * Render the actual waveform line on a `canvas` element * * @param {CanvasRenderingContext2D} ctx Rendering context of target canvas * @param {number[]} peaks Array with peaks data * @param {number} absmax Maximum peak value (absolute) * @param {number} halfH Half the height of the waveform * @param {number} offsetY Offset to the top * @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 */ drawLineToContext(ctx, peaks, absmax, halfH, offsetY, start, end) { if (!ctx) { return; } const length = peaks.length / 2; const first = Math.round(length * this.start); // use one more peak value to make sure we join peaks at ends -- unless, // of course, this is the last canvas const last = Math.round(length * this.end) + 1; const canvasStart = first; const canvasEnd = last; const scale = this.wave.width / (canvasEnd - canvasStart - 1); // optimization const halfOffset = halfH + offsetY; const absmaxHalf = absmax / halfH; ctx.beginPath(); ctx.moveTo((canvasStart - first) * scale, halfOffset); ctx.lineTo( (canvasStart - first) * scale, halfOffset - Math.round((peaks[2 * canvasStart] || 0) / absmaxHalf) ); let i, peak, h; for (i = canvasStart; i < canvasEnd; i++) { peak = peaks[2 * i] || 0; h = Math.round(peak / absmaxHalf); ctx.lineTo((i - first) * scale + this.halfPixel, halfOffset - h); } // draw the bottom edge going backwards, to make a single // closed hull to fill let j = canvasEnd - 1; for (j; j >= canvasStart; j--) { peak = peaks[2 * j + 1] || 0; h = Math.round(peak / absmaxHalf); ctx.lineTo((j - first) * scale + this.halfPixel, halfOffset - h); } ctx.lineTo( (canvasStart - first) * scale, halfOffset - Math.round((peaks[2 * canvasStart + 1] || 0) / absmaxHalf) ); ctx.closePath(); ctx.fill(); } /** * Destroys this entry */ destroy() { this.waveCtx = null; this.wave = null; this.progressCtx = null; this.progress = null; } /** * Return image data of the wave `canvas` element * * When using a `type` of `'blob'`, this will return a `Promise` that * resolves with a `Blob` instance. * * @param {string} format='image/png' An optional value of a format type. * @param {number} quality=0.92 An optional value between 0 and 1. * @param {string} type='dataURL' Either 'dataURL' or 'blob'. * @return {string|Promise} When using the default `'dataURL'` `type` this * returns a data URL. When using the `'blob'` `type` this returns a * `Promise` that resolves with a `Blob` instance. */ getImage(format, quality, type) { if (type === 'blob') { return new Promise(resolve => { this.wave.toBlob(resolve, format, quality); }); } else if (type === 'dataURL') { return this.wave.toDataURL(format, quality); } } }