Pull visualizers out into hooks

This commit is contained in:
Jordan Eldredge 2020-01-01 20:51:27 -08:00
parent 87d43ba168
commit be9bb4645e
5 changed files with 214 additions and 173 deletions

View file

@ -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 <Visualizer {...visualizerProps} />;
}
function Visualizer(props) {
const canvasRef = useRef(null);
useAnimationLoop({ canvas: canvasRef.current, paintFrame: props.paintFrame });
const { renderWidth, renderHeight, width, height } = props;
return (
<canvas
id="visualizer"
ref={canvasRef}
style={{ width: renderWidth, height: renderHeight }}
width={width}
height={height}
onClick={props.toggleVisualizerStyle}
/>
);
}
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);

View file

@ -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<HTMLCanvasElement | null>(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 (
<canvas
id="visualizer"
ref={setCanvas}
style={{ width: renderWidth, height: renderHeight }}
width={width}
height={height}
onClick={toggleVisualizerStyle}
/>
);
}

View file

@ -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,

View file

@ -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<T>(propValue: Promise<T>): 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() {

View file

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