aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/audio/WaveCanvas.tsx
blob: eacda2d427ef45731288099d06c864c09308e36d (plain)
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;
    }
}