diff --git a/js/components/Visualizer.js b/js/components/Visualizer.js deleted file mode 100644 index 4abf111c..00000000 --- a/js/components/Visualizer.js +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useMemo, useState, useEffect, useRef } from "react"; -import { connect } from "react-redux"; - -import { toggleVisualizerStyle } from "../actionCreators"; -import { getWindowShade, getVisualizerStyle } from "../selectors"; -import { VISUALIZERS, MEDIA_STATUS } from "../constants"; -import { - preRenderBg, - preRenderBar, - paintOscilloscopeFrame, - octaveBucketsForBufferLength, - paintBarFrame, - NUM_BARS, - PIXEL_DENSITY, -} from "./visualizerUtils"; -import { useAnimationLoop } from "../hooks"; - -function Wrapper(props) { - const [barPeaks] = useState(() => new Array(NUM_BARS).fill(0)); - const [barPeakFrames] = useState(() => new Array(NUM_BARS).fill(0)); - const renderWidth = props.canvasWidth; - const renderHeight = props.canvasHeight; - const height = props.canvasHeight * PIXEL_DENSITY; - const width = props.canvasWidth * PIXEL_DENSITY; - - const bgCanvas = useMemo(() => { - return preRenderBg( - width, - height, - props.colors[0], - props.colors[1], - props.windowShade - ); - }, [height, props.colors, props.windowShade, width]); - - const barCanvas = useMemo(() => { - return preRenderBar(height, props.colors, renderHeight); - }, [height, props.colors, renderHeight]); - - useEffect(() => { - props.analyser.fftSize = 2048; - }, [props.analyser]); - - const dataArray = useMemo(() => { - if (props.style === VISUALIZERS.OSCILLOSCOPE) { - return new Uint8Array(props.analyser.fftSize); - } else if (props.style === VISUALIZERS.BAR) { - return new Uint8Array(props.analyser.frequencyBinCount); - } - }, [props.analyser.fftSize, props.analyser.frequencyBinCount, props.style]); - - const octaveBuckets = useMemo(() => { - return octaveBucketsForBufferLength(dataArray.length); - }, [dataArray.length]); - - const paintFrame = useMemo(() => { - if (dataArray == null || props.status !== MEDIA_STATUS.PLAYING) { - return null; - } - return canvasCtx => { - switch (props.style) { - case VISUALIZERS.OSCILLOSCOPE: - canvasCtx.drawImage(bgCanvas, 0, 0); - paintOscilloscopeFrame({ - analyser: props.analyser, - dataArray, - canvasCtx, - height, - width, - colors: props.colors, - renderWidth, - }); - break; - case VISUALIZERS.BAR: - canvasCtx.drawImage(bgCanvas, 0, 0); - paintBarFrame({ - analyser: props.analyser, - dataArray, - renderHeight, - octaveBuckets, - barPeaks, - barPeakFrames, - height, - canvasCtx, - barCanvas, - windowShade: props.windowShade, - colors: props.colors, - }); - break; - default: - this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height); - } - }; - }, [ - barCanvas, - barPeakFrames, - barPeaks, - bgCanvas, - dataArray, - height, - octaveBuckets, - props.analyser, - props.colors, - props.status, - props.style, - props.windowShade, - renderHeight, - renderWidth, - width, - ]); - - const visualizerProps = { - ...props, - renderHeight, - renderWidth, - height, - width, - paintFrame, - }; - - return ; -} - -function Visualizer(props) { - const canvasRef = useRef(null); - - useAnimationLoop({ canvas: canvasRef.current, paintFrame: props.paintFrame }); - - const { renderWidth, renderHeight, width, height } = props; - return ( - - ); -} - -const mapStateToProps = state => ({ - colors: state.display.skinColors, - style: getVisualizerStyle(state), - canvasWidth: getWindowShade(state)("main") ? 38 : 76, - canvasHeight: getWindowShade(state)("main") ? 5 : 16, - status: state.media.status, - windowShade: getWindowShade(state)("main"), - dummyVizData: state.display.dummyVizData, -}); - -const mapDispatchToProps = { - toggleVisualizerStyle, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(Wrapper); diff --git a/js/components/Visualizer.tsx b/js/components/Visualizer.tsx new file mode 100644 index 00000000..00dcd2b3 --- /dev/null +++ b/js/components/Visualizer.tsx @@ -0,0 +1,96 @@ +import React, { useMemo, useEffect, useState } from "react"; + +import * as Actions from "../actionCreators"; +import * as Selectors from "../selectors"; +import { VISUALIZERS, MEDIA_STATUS } from "../constants"; +import { + preRenderBg, + usePaintOscilloscope, + PIXEL_DENSITY, + usePaintBars, +} from "./visualizerUtils"; +import { useAnimationLoop, useTypedSelector, useActionCreator } from "../hooks"; + +type Props = { + analyser: AnalyserNode; +}; + +export default function Visualizer({ analyser }: Props) { + const windowShade = useTypedSelector(Selectors.getWindowShade)("main")!; + const renderWidth = windowShade ? 38 : 76; + const renderHeight = windowShade ? 5 : 16; + const colors = useTypedSelector(Selectors.getSkinColors); + const style = useTypedSelector(Selectors.getVisualizerStyle); + const status = useTypedSelector(Selectors.getMediaStatus); + // const dummyVizData = useTypedSelector(Selectors.getDummyVizData); + const toggleVisualizerStyle = useActionCreator(Actions.toggleVisualizerStyle); + + const [canvas, setCanvas] = useState(null); + + const height = renderHeight * PIXEL_DENSITY; + const width = renderWidth * PIXEL_DENSITY; + + const canvasCtx = useMemo(() => { + return canvas?.getContext("2d") ?? null; + }, [canvas]); + + const bgCanvas = useMemo(() => { + return preRenderBg(width, height, colors[0], colors[1], windowShade); + }, [colors, height, width, windowShade]); + + useEffect(() => { + if (style === VISUALIZERS.OSCILLOSCOPE || style === VISUALIZERS.BAR) { + analyser.fftSize = 2048; + } + }, [analyser.fftSize, style]); + + const paintBars = usePaintBars({ + analyser, + canvasCtx, + renderHeight, + height, + windowShade, + colors, + }); + + const paintOscilloscope = usePaintOscilloscope({ + analyser, + canvasCtx, + renderWidth, + width, + height, + colors, + }); + + const paintFrame = useMemo(() => { + if (canvasCtx == null || status !== MEDIA_STATUS.PLAYING) { + return null; + } + switch (style) { + case VISUALIZERS.OSCILLOSCOPE: + return () => { + canvasCtx.drawImage(bgCanvas, 0, 0); + paintOscilloscope(); + }; + case VISUALIZERS.BAR: + return () => { + canvasCtx.drawImage(bgCanvas, 0, 0); + paintBars(); + }; + } + return null; + }, [bgCanvas, canvasCtx, paintBars, paintOscilloscope, status, style]); + + useAnimationLoop({ paintFrame }); + + return ( + + ); +} diff --git a/js/components/visualizerUtils.ts b/js/components/visualizerUtils.ts index 322acccd..1edb6de6 100644 --- a/js/components/visualizerUtils.ts +++ b/js/components/visualizerUtils.ts @@ -1,3 +1,4 @@ +import { useCallback, useMemo, useState } from "react"; export const PIXEL_DENSITY = 2; export const NUM_BARS = 20; export const BAR_WIDTH = 3 * PIXEL_DENSITY; @@ -5,6 +6,107 @@ export const BAR_PEAK_DROP_RATE = 0.01; export const GRADIENT_COLOR_COUNT = 16; export const PEAK_COLOR_INDEX = 23; +type UsePaintBarsOptions = { + analyser: AnalyserNode; + canvasCtx: CanvasRenderingContext2D | null; + renderHeight: number; + height: number; + windowShade: boolean; + colors: string[]; +}; + +export function usePaintBars({ + analyser, + canvasCtx, + renderHeight, + height, + windowShade, + colors, +}: UsePaintBarsOptions) { + const [barPeaks] = useState(() => new Array(NUM_BARS).fill(0)); + const [barPeakFrames] = useState(() => new Array(NUM_BARS).fill(0)); + + const barCanvas = useMemo(() => { + return preRenderBar(height, colors, renderHeight); + }, [colors, height, renderHeight]); + + const dataArray = useMemo(() => { + return new Uint8Array(analyser.frequencyBinCount); + }, [analyser.frequencyBinCount]); + + const octaveBuckets = useMemo(() => { + return octaveBucketsForBufferLength(dataArray.length); + }, [dataArray.length]); + + return useCallback(() => { + if (canvasCtx == null) { + return; + } + paintBarFrame({ + analyser, + dataArray, + renderHeight, + octaveBuckets, + barPeaks, + barPeakFrames, + height, + canvasCtx, + barCanvas, + windowShade, + colors, + }); + }, [ + analyser, + barCanvas, + barPeakFrames, + barPeaks, + canvasCtx, + colors, + dataArray, + height, + octaveBuckets, + renderHeight, + windowShade, + ]); +} + +type UsePaintOscilloscopeOptions = { + analyser: AnalyserNode; + canvasCtx: CanvasRenderingContext2D | null; + renderWidth: number; + height: number; + width: number; + colors: string[]; +}; + +export function usePaintOscilloscope({ + canvasCtx, + analyser, + height, + colors, + width, + renderWidth, +}: UsePaintOscilloscopeOptions) { + const dataArray = useMemo(() => { + return new Uint8Array(analyser.fftSize); + }, [analyser.fftSize]); + + return useCallback(() => { + if (canvasCtx == null) { + return; + } + paintOscilloscopeFrame({ + analyser, + dataArray, + canvasCtx, + height, + width, + colors, + renderWidth, + }); + }, [analyser, canvasCtx, colors, dataArray, height, renderWidth, width]); +} + // Pre-render the background grid export function preRenderBg( width: number, diff --git a/js/hooks.ts b/js/hooks.ts index d1575fb8..f8b007af 100644 --- a/js/hooks.ts +++ b/js/hooks.ts @@ -4,7 +4,6 @@ import { useCallback, useLayoutEffect, useRef, - useMemo, } from "react"; import { useDispatch, useSelector } from "react-redux"; import * as Utils from "./utils"; @@ -45,28 +44,19 @@ export function usePromiseValueOrNull(propValue: Promise): T | null { } type AnimationLoopOptions = { - canvas: HTMLCanvasElement; - paintFrame: null | ((canvasCtx: CanvasRenderingContext2D) => void); + paintFrame: null | (() => void); }; -export function useAnimationLoop({ canvas, paintFrame }: AnimationLoopOptions) { - const canvasCtx = useMemo(() => { - if (canvas == null) { - return null; - } - - return canvas.getContext("2d"); - }, [canvas]); - +export function useAnimationLoop({ paintFrame }: AnimationLoopOptions) { useLayoutEffect(() => { - if (canvasCtx == null || paintFrame == null) { + if (paintFrame == null) { return; } let animationRequest: number | null = null; function loop() { // @ts-ignore This can't be null - paintFrame(canvasCtx); + paintFrame(); animationRequest = window.requestAnimationFrame(loop); } @@ -77,7 +67,7 @@ export function useAnimationLoop({ canvas, paintFrame }: AnimationLoopOptions) { window.cancelAnimationFrame(animationRequest); } }; - }, [canvasCtx, paintFrame]); + }, [paintFrame]); } export function useScreenSize() { diff --git a/js/selectors.ts b/js/selectors.ts index 09c044cb..bee0dfb0 100644 --- a/js/selectors.ts +++ b/js/selectors.ts @@ -14,6 +14,7 @@ import { Cursors, SkinRegion, GenLetterWidths, + DummyVizData, } from "./types"; import { createSelector, defaultMemoize } from "reselect"; import * as Utils from "./utils"; @@ -426,14 +427,18 @@ export function getPositionsAreRelative(state: AppState) { return state.windows.positionsAreRelative; } -export function getDoubled(state: AppState) { +export function getDoubled(state: AppState): boolean { return state.display.doubled; } -export function getLlamaMode(state: AppState) { +export function getLlamaMode(state: AppState): boolean { return state.display.llama; } +export function getDummyVizData(state: AppState): DummyVizData | null { + return state.display.dummyVizData; +} + export function getZIndex(state: AppState): number { return state.display.zIndex; } @@ -693,6 +698,10 @@ export function getSkinImages(state: AppState): SkinImages { return state.display.skinImages; } +export function getSkinColors(state: AppState): string[] { + return state.display.skinColors; +} + export function getSkinCursors(state: AppState): Cursors | null { return state.display.skinCursors; }