tlaternet-webserver/templates/src/music/features/visualizer/Visualizer.tsx

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;