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(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 ( ); } export default Visualizer;