mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-31 22:14:09 +00:00
Type the rest of the visualizer
This commit is contained in:
parent
5ec817dffd
commit
7ebf216ef2
3 changed files with 122 additions and 145 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue