140 lines
5.1 KiB
TypeScript
140 lines
5.1 KiB
TypeScript
import React, { useCallback, useState } from "react";
|
|
import { Renderer, RendererError } from "./Renderer";
|
|
import { ShaderError } from "./Shader";
|
|
|
|
import { useAppSelector } from "../../hooks";
|
|
import { PlayState, musicPlayer } from "../musicplayer/musicPlayerSlice";
|
|
|
|
function Visualizer() {
|
|
const playing = useAppSelector((state) => state.musicPlayer.playing);
|
|
const rendererState = useState<Renderer | null>(null);
|
|
let renderer = rendererState[0];
|
|
const setRenderer = rendererState[1];
|
|
const [renderError, setRenderError] = useState<JSX.Element | null>(null);
|
|
|
|
const visualizer = useCallback(
|
|
(visualizer: HTMLDivElement | null) => {
|
|
// TODO(tlater): Clean up state management. This is all
|
|
// but trivial; there's seemingly no good place to keep
|
|
// these big api objects (WebGLRenderingcontext or
|
|
// AudioContext).
|
|
//
|
|
// It's tricky, too, because obviously react expects to be
|
|
// in control of the DOM, and be allowed to delete our
|
|
// canvas and create a new one.
|
|
//
|
|
// For the moment, this works, but it's a definite hack.
|
|
if (renderer) {
|
|
return;
|
|
}
|
|
|
|
// Until we start playing music, there is nothing to render.
|
|
if (playing !== PlayState.Playing) {
|
|
return;
|
|
}
|
|
|
|
if (musicPlayer.audioAnalyser === undefined) {
|
|
throw new Error("MusicPlayer analyser was not set up on time");
|
|
}
|
|
|
|
// If we're rendering an error message, we won't be
|
|
// setting up the visualizer.
|
|
//
|
|
// Also, nonintuitively, renderError will be null here on
|
|
// subsequent iterations, so we can't rely on it to
|
|
// identify errors.
|
|
if (visualizer === null) {
|
|
return;
|
|
}
|
|
|
|
const canvas = visualizer.children[0];
|
|
const overlay = visualizer.children[1];
|
|
|
|
if (
|
|
!(canvas instanceof HTMLCanvasElement) ||
|
|
!(overlay instanceof HTMLSpanElement)
|
|
) {
|
|
throw new Error(
|
|
"react did not create our visualizer div correctly"
|
|
);
|
|
}
|
|
|
|
if (renderer === null) {
|
|
renderer = new Renderer(
|
|
musicPlayer.audioAnalyser,
|
|
canvas,
|
|
overlay
|
|
);
|
|
setRenderer(renderer);
|
|
}
|
|
|
|
try {
|
|
renderer.initializeScene();
|
|
} catch (error) {
|
|
// Log so we don't lose the stack trace
|
|
console.log(error);
|
|
|
|
if (error instanceof ShaderError) {
|
|
setRenderError(
|
|
<span>
|
|
Failed to compile shader; This is a bug, feel free
|
|
to contact me with this error message:
|
|
<pre>
|
|
<code className="has-text-danger">
|
|
{error.message}
|
|
</code>
|
|
</pre>
|
|
</span>
|
|
);
|
|
} else if (error instanceof RendererError) {
|
|
setRenderError(
|
|
<span>
|
|
This browser does not support WebGL 2, sadly. This
|
|
demo uses WebGL and specifically instanced drawing,
|
|
so unfortunately this means it can't run on your
|
|
browser/device.
|
|
</span>
|
|
);
|
|
} else if (error instanceof Error) {
|
|
setRenderError(
|
|
<span>
|
|
Something went very wrong; apologies, either your
|
|
browser is not behaving or there's a serious bug.
|
|
You can contact me with this error message:
|
|
<pre>
|
|
<code className="has-text-danger">
|
|
{error.message}
|
|
</code>
|
|
</pre>
|
|
</span>
|
|
);
|
|
} else {
|
|
setRenderError(
|
|
<span>
|
|
Something went very wrong; apologies, either your
|
|
browser is not behaving or there's a serious bug.
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
},
|
|
[playing, renderer, musicPlayer.audioAnalyser]
|
|
);
|
|
|
|
if (renderError === null) {
|
|
return (
|
|
<div
|
|
ref={visualizer}
|
|
className="is-flex-grow-1 is-clipped is-relative"
|
|
>
|
|
<canvas className="is-block is-absolute is-border-box"></canvas>
|
|
<span className="is-bottom-left"></span>
|
|
</div>
|
|
);
|
|
} else {
|
|
return renderError;
|
|
}
|
|
}
|
|
|
|
export default Visualizer;
|