1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
/* 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<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;
// 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 ? (
<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;
}
}
|