Type the rest of the visualizer

This commit is contained in:
Jordan Eldredge 2020-04-08 20:15:11 -07:00
parent 5ec817dffd
commit 7ebf216ef2
3 changed files with 122 additions and 145 deletions

View file

@ -3,13 +3,13 @@ import React, { useMemo, useCallback, useState, useLayoutEffect } from "react";
import * as Actions from "../actionCreators";
import * as Selectors from "../selectors";
import { useTypedSelector, useActionCreator } from "../hooks";
import VisualizerInner from "./VisualizerInner";
import { VISUALIZERS } from "../constants";
import { VISUALIZERS, MEDIA_STATUS } from "../constants";
const PIXEL_DENSITY = 2;
const BAR_WIDTH = 3 * PIXEL_DENSITY;
const GRADIENT_COLOR_COUNT = 16;
const PEAK_COLOR_INDEX = 23;
const BAR_PEAK_DROP_RATE = 0.01;
const NUM_BARS = 20;
type Props = {
@ -125,6 +125,9 @@ function preRenderBar(
return barCanvas;
}
function Visualizer(props: Props) {
const [barPeaks] = useState(() => new Array(NUM_BARS).fill(0));
const [barPeakFrames] = useState(() => new Array(NUM_BARS).fill(0));
useLayoutEffect(() => {
props.analyser.fftSize = 2048;
}, [props.analyser]);
@ -251,24 +254,121 @@ function Visualizer(props: Props) {
]
);
const innerProps = {
width: renderWidth,
height: renderHeight,
colors,
style,
status,
windowShade,
dummyVizData,
toggleVisualizerStyle,
bgCanvas,
barCanvas,
printBar,
paintOscilloscopeFrame,
bufferLength,
dataArray,
octaveBuckets,
};
return <VisualizerInner {...innerProps} {...props} />;
const paintBarFrame = useCallback(
(canvasCtx: CanvasRenderingContext2D) => {
props.analyser.getByteFrequencyData(dataArray);
const heightMultiplier = renderHeight / 256;
const xOffset = BAR_WIDTH + PIXEL_DENSITY; // Bar width, plus a pixel of spacing to the right.
for (let j = 0; j < NUM_BARS - 1; j++) {
const start = octaveBuckets[j];
const end = octaveBuckets[j + 1];
let amplitude = 0;
for (let k = start; k < end; k++) {
amplitude += dataArray[k];
}
amplitude /= end - start;
// The drop rate should probably be normalized to the rendering FPS, for now assume 60 FPS
let barPeak =
barPeaks[j] - BAR_PEAK_DROP_RATE * Math.pow(barPeakFrames[j], 2);
if (barPeak < amplitude) {
barPeak = amplitude;
barPeakFrames[j] = 0;
} else {
barPeakFrames[j] += 1;
}
barPeaks[j] = barPeak;
printBar(
canvasCtx,
j * xOffset,
amplitude * heightMultiplier,
barPeak * heightMultiplier
);
}
},
[
barPeakFrames,
barPeaks,
dataArray,
octaveBuckets,
printBar,
props.analyser,
renderHeight,
]
);
const paintFrame = useCallback(
(canvasCtx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => {
if (status !== MEDIA_STATUS.PLAYING) {
return;
}
if (dummyVizData) {
Object.entries(dummyVizData).forEach(([i, value]) => {
printBar(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, canvas.width, canvas.height);
}
},
[
bgCanvas,
dummyVizData,
paintBarFrame,
paintOscilloscopeFrame,
printBar,
status,
style,
]
);
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(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, canvas);
animationRequest = window.requestAnimationFrame(loop);
};
loop();
return () => {
if (animationRequest != null) {
window.cancelAnimationFrame(animationRequest);
}
};
}, [canvas, paintFrame]);
return (
<canvas
id="visualizer"
ref={setCanvas}
style={{ width: renderWidth, height: renderHeight }}
width={width}
height={height}
onClick={toggleVisualizerStyle}
/>
);
}
export default Visualizer;

View file

@ -1,123 +0,0 @@
import React from "react";
import { VISUALIZERS, MEDIA_STATUS } from "../constants";
const PIXEL_DENSITY = 2;
const NUM_BARS = 20;
const BAR_WIDTH = 3 * PIXEL_DENSITY;
const BAR_PEAK_DROP_RATE = 0.01;
class VisualizerInner extends React.Component {
componentDidMount() {
this.barPeaks = new Array(NUM_BARS).fill(0);
this.barPeakFrames = new Array(NUM_BARS).fill(0);
this.canvasCtx = this.canvas.getContext("2d");
this.canvasCtx.imageSmoothingEnabled = false;
// Kick off the animation loop
const loop = () => {
if (this.props.status === MEDIA_STATUS.PLAYING) {
if (this.props.dummyVizData) {
Object.keys(this.props.dummyVizData).forEach((i) => {
this.props.printBar(this.canvasCtx, i, this.props.dummyVizData[i]);
});
} else {
this.paintFrame();
}
}
this._animationRequest = window.requestAnimationFrame(loop);
};
loop();
}
componentWillUnmount() {
if (this._animationRequest) {
window.cancelAnimationFrame(this._animationRequest);
}
}
componentDidUpdate() {
// Redraw the current frame, since the skin may have changed.
this.paintFrame();
}
_renderWidth() {
return this.props.width;
}
_renderHeight() {
return this.props.height;
}
_height() {
return this.props.height * PIXEL_DENSITY;
}
_width() {
return this.props.width * PIXEL_DENSITY;
}
paintFrame() {
switch (this.props.style) {
case VISUALIZERS.OSCILLOSCOPE:
this.canvasCtx.drawImage(this.props.bgCanvas, 0, 0);
this.props.paintOscilloscopeFrame(this.canvasCtx);
break;
case VISUALIZERS.BAR:
this.canvasCtx.drawImage(this.props.bgCanvas, 0, 0);
this._paintBarFrame();
break;
default:
this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
_paintBarFrame() {
this.props.analyser.getByteFrequencyData(this.props.dataArray);
const heightMultiplier = this._renderHeight() / 256;
const xOffset = BAR_WIDTH + PIXEL_DENSITY; // Bar width, plus a pixel of spacing to the right.
for (let j = 0; j < NUM_BARS - 1; j++) {
const start = this.props.octaveBuckets[j];
const end = this.props.octaveBuckets[j + 1];
let amplitude = 0;
for (let k = start; k < end; k++) {
amplitude += this.props.dataArray[k];
}
amplitude /= end - start;
// The drop rate should probably be normalized to the rendering FPS, for now assume 60 FPS
let barPeak =
this.barPeaks[j] -
BAR_PEAK_DROP_RATE * Math.pow(this.barPeakFrames[j], 2);
if (barPeak < amplitude) {
barPeak = amplitude;
this.barPeakFrames[j] = 0;
} else {
this.barPeakFrames[j] += 1;
}
this.barPeaks[j] = barPeak;
this.props.printBar(
this.canvasCtx,
j * xOffset,
amplitude * heightMultiplier,
barPeak * heightMultiplier
);
}
}
render() {
const { width, height } = this.props;
return (
<canvas
id="visualizer"
ref={(node) => (this.canvas = node)}
style={{ width, height }}
width={width * PIXEL_DENSITY}
height={height * PIXEL_DENSITY}
onClick={this.props.toggleVisualizerStyle}
/>
);
}
}
export default VisualizerInner;

View file

@ -94,7 +94,7 @@ export type SkinImages = { [sprite: string]: string };
// TODO: type these keys
export type SkinRegion = { [windowName: string]: string[] };
export interface DummyVizData {
export type DummyVizData = {
0: 11.75;
8: 11.0625;
16: 8.5;
@ -111,7 +111,7 @@ export interface DummyVizData {
104: 4.375;
112: 3.625;
120: 1.5625;
}
};
export interface SkinGenExColors {
itemBackground: string;