webamp/js/components/Visualizer.tsx
2020-04-08 21:23:20 -07:00

164 lines
4.3 KiB
TypeScript

import React, { useMemo, useCallback, useState, useLayoutEffect } from "react";
import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { useTypedSelector, useActionCreator } from "../hooks";
import { usePaintOscilloscopeFrame } from "./useOscilloscopeVisualizer";
import { usePaintBarFrame, usePaintBar } from "./useBarVisualizer";
import { VISUALIZERS, MEDIA_STATUS } from "../constants";
const PIXEL_DENSITY = 2;
type Props = {
analyser: AnalyserNode;
};
// Pre-render the background grid
function preRenderBg(
width: number,
height: number,
bgColor: string,
fgColor: string,
windowShade: boolean
): HTMLCanvasElement {
// Off-screen canvas for pre-rendering the background
const bgCanvas = document.createElement("canvas");
bgCanvas.width = width;
bgCanvas.height = height;
const distance = 2 * PIXEL_DENSITY;
const bgCanvasCtx = bgCanvas.getContext("2d");
if (bgCanvasCtx == null) {
throw new Error("Could not construct canvas context");
}
bgCanvasCtx.fillStyle = bgColor;
bgCanvasCtx.fillRect(0, 0, width, height);
if (!windowShade) {
bgCanvasCtx.fillStyle = fgColor;
for (let x = 0; x < width; x += distance) {
for (let y = PIXEL_DENSITY; y < height; y += distance) {
bgCanvasCtx.fillRect(x, y, PIXEL_DENSITY, PIXEL_DENSITY);
}
}
}
return bgCanvas;
}
function Visualizer({ analyser }: Props) {
useLayoutEffect(() => {
analyser.fftSize = 2048;
}, [analyser.fftSize]);
const colors = useTypedSelector(Selectors.getSkinColors);
const style = useTypedSelector(Selectors.getVisualizerStyle);
const status = useTypedSelector(Selectors.getMediaStatus);
const getWindowShade = useTypedSelector(Selectors.getWindowShade);
const dummyVizData = useTypedSelector(Selectors.getDummyVizData);
const toggleVisualizerStyle = useActionCreator(Actions.toggleVisualizerStyle);
const windowShade = getWindowShade("main");
const renderWidth = windowShade ? 38 : 76;
const renderHeight = windowShade ? 5 : 16;
const width = renderWidth * PIXEL_DENSITY;
const height = renderHeight * PIXEL_DENSITY;
const bgCanvas = useMemo(() => {
return preRenderBg(
width,
height,
colors[0],
colors[1],
Boolean(windowShade)
);
}, [colors, height, width, windowShade]);
const paintOscilloscopeFrame = usePaintOscilloscopeFrame({
analyser,
height,
width,
renderWidth,
});
const paintBarFrame = usePaintBarFrame({
analyser,
height,
renderHeight,
});
const paintBar = usePaintBar({ height, renderHeight });
const paintFrame = useCallback(
(canvasCtx: CanvasRenderingContext2D) => {
if (status !== MEDIA_STATUS.PLAYING) {
return;
}
if (dummyVizData) {
Object.entries(dummyVizData).forEach(([i, value]) => {
paintBar(canvasCtx, Number(i), value, -1);
});
return;
}
switch (style) {
case VISUALIZERS.OSCILLOSCOPE:
canvasCtx.drawImage(bgCanvas, 0, 0);
paintOscilloscopeFrame(canvasCtx);
break;
case VISUALIZERS.BAR:
canvasCtx.drawImage(bgCanvas, 0, 0);
paintBarFrame(canvasCtx);
break;
default:
canvasCtx.clearRect(0, 0, width, height);
}
},
[
bgCanvas,
dummyVizData,
height,
paintBar,
paintBarFrame,
paintOscilloscopeFrame,
status,
style,
width,
]
);
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null);
useLayoutEffect(() => {
if (canvas == null) {
return;
}
const canvasCtx = canvas.getContext("2d");
if (canvasCtx == null) {
return;
}
canvasCtx.imageSmoothingEnabled = false;
let animationRequest: number | null = null;
// Kick off the animation loop
const loop = () => {
paintFrame(canvasCtx);
animationRequest = window.requestAnimationFrame(loop);
};
loop();
return () => {
if (animationRequest != null) {
window.cancelAnimationFrame(animationRequest);
}
};
}, [canvas, paintFrame]);
return (
<canvas
id="visualizer"
ref={setCanvas}
style={{ width: renderWidth, height: renderHeight }}
width={width}
height={height}
onClick={toggleVisualizerStyle}
/>
);
}
export default Visualizer;