mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 01:57:29 +00:00
Pull visualizers out into hooks
This commit is contained in:
parent
87d43ba168
commit
be9bb4645e
5 changed files with 214 additions and 173 deletions
|
|
@ -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);
|
||||
96
js/components/Visualizer.tsx
Normal file
96
js/components/Visualizer.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
20
js/hooks.ts
20
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<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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue