/* eslint-disable react/require-default-props */ import React from 'react'; interface WaveCanvasProps { barWidth: number; color: string; progress: number; progressColor: string; gradientColors?: { stopPosition: number; color: string }[]; // stopPosition between 0 and 1 peaks: number[]; width: number; height: number; pixelRatio: number; } export class WaveCanvas extends React.Component { // If the first value of peaks is negative, addToIndices will be 1 posPeaks = (peaks: number[], addToIndices: number) => peaks.filter((_, index) => (index + addToIndices) % 2 === 0); drawBars = (waveCanvasCtx: CanvasRenderingContext2D, width: number, halfH: number, peaks: number[]) => { // Bar wave draws the bottom only as a reflection of the top, // so we don't need negative values const posPeaks = peaks.some(val => val < 0) ? this.posPeaks(peaks, peaks[0] < 0 ? 1 : 0) : peaks; // A half-pixel offset makes lines crisp const $ = 0.5 / this.props.pixelRatio; const bar = this.props.barWidth * this.props.pixelRatio; const gap = Math.max(this.props.pixelRatio, 2); const max = Math.max(...posPeaks); const scale = posPeaks.length / width; for (let i = 0; i < width; i += bar + gap) { if (i > width * this.props.progress) waveCanvasCtx.fillStyle = this.props.color; const h = Math.round((posPeaks[Math.floor(i * scale)] / max) * halfH) || 1; waveCanvasCtx.fillRect(i + $, halfH - h, bar + $, h * 2); } }; addNegPeaks = (peaks: number[]) => peaks.reduce((reflectedPeaks, peak) => reflectedPeaks.push(peak, -peak) ? reflectedPeaks:[], [] as number[]); // prettier-ignore drawWaves = (waveCanvasCtx: CanvasRenderingContext2D, width: number, halfH: number, peaks: number[]) => { const allPeaks = peaks.some(val => val < 0) ? peaks : this.addNegPeaks(peaks); // add negative peaks to arrays without negative peaks // A half-pixel offset makes lines crisp const $ = 0.5 / this.props.pixelRatio; // eslint-disable-next-line no-bitwise const length = ~~(allPeaks.length / 2); // ~~ is Math.floor for positive numbers. const scale = width / length; const absmax = Math.max(...allPeaks.map(peak => Math.abs(peak))); waveCanvasCtx.beginPath(); waveCanvasCtx.moveTo($, halfH); for (let i = 0; i < length; i++) { const h = Math.round((allPeaks[2 * i] / absmax) * halfH); waveCanvasCtx.lineTo(i * scale + $, halfH - h); } // Draw the bottom edge going backwards, to make a single closed hull to fill. for (let i = length - 1; i >= 0; i--) { const h = Math.round((allPeaks[2 * i + 1] / absmax) * halfH); waveCanvasCtx.lineTo(i * scale + $, halfH - h); } waveCanvasCtx.fill(); // Always draw a median line waveCanvasCtx.fillRect(0, halfH - $, width, $); }; updateSize = (width: number, height: number, peaks: number[], waveCanvasCtx: CanvasRenderingContext2D) => { const displayWidth = Math.round(width / this.props.pixelRatio); const displayHeight = Math.round(height / this.props.pixelRatio); waveCanvasCtx.canvas.width = width; waveCanvasCtx.canvas.height = height; waveCanvasCtx.canvas.style.width = `${displayWidth}px`; waveCanvasCtx.canvas.style.height = `${displayHeight}px`; waveCanvasCtx.clearRect(0, 0, width, height); const gradient = this.props.gradientColors && waveCanvasCtx.createLinearGradient(0, 0, width, 0); gradient && this.props.gradientColors?.forEach(color => gradient.addColorStop(color.stopPosition, color.color)); waveCanvasCtx.fillStyle = gradient ?? this.props.progressColor; const waveDrawer = this.props.barWidth ? this.drawBars : this.drawWaves; waveDrawer(waveCanvasCtx, width, height / 2, peaks); }; render() { return this.props.peaks ? (
(ctx => ctx && this.updateSize(this.props.width, this.props.height, this.props.peaks, ctx))(instance?.getContext('2d'))} />
) : null; } }