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;
}