diff options
Diffstat (limited to 'src/client/views/nodes/audio/WaveCanvas.tsx')
| -rw-r--r-- | src/client/views/nodes/audio/WaveCanvas.tsx | 100 |
1 files changed, 100 insertions, 0 deletions
diff --git a/src/client/views/nodes/audio/WaveCanvas.tsx b/src/client/views/nodes/audio/WaveCanvas.tsx new file mode 100644 index 000000000..d3f5669a2 --- /dev/null +++ b/src/client/views/nodes/audio/WaveCanvas.tsx @@ -0,0 +1,100 @@ +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<WaveCanvasProps> { + // 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; + 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 (var i = 0; i < length; i++) { + var 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 (var i = length - 1; i >= 0; i--) { + var 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 ? ( + <div style={{ position: 'relative', width: '100%', height: '100%', cursor: 'pointer' }}> + <canvas ref={instance => (ctx => ctx && this.updateSize(this.props.width, this.props.height, this.props.peaks, ctx))(instance?.getContext('2d'))} /> + </div> + ) : null; + } +} |
