Rework website with bulma instead of bootstrap #6

Manually merged
tlater merged 43 commits from tlater/bulma2 into master 2022-08-14 00:26:49 +01:00
4 changed files with 64 additions and 41 deletions
Showing only changes of commit 86b6e1a246 - Show all commits

View file

@ -2,15 +2,11 @@ import React from "react";
import Controls from "../controls/Controls"; import Controls from "../controls/Controls";
import Visualizer from "../visualizer/Visualizer"; import Visualizer from "../visualizer/Visualizer";
import { musicPlayer } from "./musicPlayerSlice";
function MusicPlayer() { function MusicPlayer() {
return ( return (
<div className="is-flex-grow-1 is-flex is-flex-direction-column"> <div className="is-flex-grow-1 is-flex is-flex-direction-column">
<Visualizer <Visualizer />
audioContext={musicPlayer.audioContext}
audioNode={musicPlayer.audioNode}
/>
<div className="is-flex-grow-0"> <div className="is-flex-grow-0">
<Controls /> <Controls />
</div> </div>

View file

@ -30,27 +30,18 @@ enum PlayState {
//********************* //*********************
class MusicPlayer { class MusicPlayer {
private context: AudioContext; private context?: AudioContext;
private source: HTMLAudioElement; private source: HTMLAudioElement;
private sourceNode: MediaElementAudioSourceNode; private sourceNode?: MediaElementAudioSourceNode;
private volume: GainNode; private volume?: GainNode;
private analyser?: AnalyserNode;
constructor() { constructor() {
this.context = new AudioContext();
this.source = new Audio(); this.source = new Audio();
this.sourceNode = this.context.createMediaElementSource(this.source);
this.volume = this.context.createGain();
this.sourceNode.connect(this.volume);
this.volume.connect(this.context.destination);
} }
get audioContext() { get audioAnalyser() {
return this.context; return this.analyser;
}
get audioNode() {
return this.sourceNode;
} }
set src(source: string) { set src(source: string) {
@ -61,6 +52,22 @@ class MusicPlayer {
_: null, _: null,
{ getState }: { getState: () => RootState } { getState }: { getState: () => RootState }
): Promise<PlayState> => { ): Promise<PlayState> => {
if (this.context === undefined) {
this.context = new AudioContext();
this.sourceNode = this.context.createMediaElementSource(
this.source
);
this.volume = this.context.createGain();
this.analyser = this.context.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8;
this.sourceNode.connect(this.analyser);
this.sourceNode.connect(this.volume);
this.volume.connect(this.context.destination);
}
const playing = getState().musicPlayer.playing; const playing = getState().musicPlayer.playing;
switch (playing) { switch (playing) {

View file

@ -33,16 +33,10 @@ class Renderer {
}; };
constructor( constructor(
context: AudioContext, analyser: AnalyserNode,
node: AudioNode,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
overlay: HTMLSpanElement overlay: HTMLSpanElement
) { ) {
const analyser = context.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
node.connect(analyser);
this.canvas = canvas; this.canvas = canvas;
this.overlay = overlay; this.overlay = overlay;
this.analyser = analyser; this.analyser = analyser;

View file

@ -2,17 +2,41 @@ import React, { useCallback, useState } from "react";
import { Renderer, RendererError } from "./Renderer"; import { Renderer, RendererError } from "./Renderer";
import { ShaderError } from "./Shader"; import { ShaderError } from "./Shader";
function Visualizer({ import { useAppSelector } from "../../hooks";
audioContext, import { PlayState, musicPlayer } from "../musicplayer/musicPlayerSlice";
audioNode,
}: { function Visualizer() {
audioContext: AudioContext; const playing = useAppSelector((state) => state.musicPlayer.playing);
audioNode: AudioNode; const rendererState = useState<Renderer | null>(null);
}) { let renderer = rendererState[0];
const setRenderer = rendererState[1];
const [renderError, setRenderError] = useState<JSX.Element | null>(null); const [renderError, setRenderError] = useState<JSX.Element | null>(null);
const visualizer = useCallback( const visualizer = useCallback(
(visualizer: HTMLDivElement | null) => { (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 // If we're rendering an error message, we won't be
// setting up the visualizer. // setting up the visualizer.
// //
@ -35,12 +59,14 @@ function Visualizer({
); );
} }
const renderer = new Renderer( if (renderer === null) {
audioContext, renderer = new Renderer(
audioNode, musicPlayer.audioAnalyser,
canvas, canvas,
overlay overlay
); );
setRenderer(renderer);
}
try { try {
renderer.initializeScene(); renderer.initializeScene();
@ -92,7 +118,7 @@ function Visualizer({
} }
} }
}, },
[audioContext, audioNode] [playing, renderer, musicPlayer.audioAnalyser]
); );
if (renderError === null) { if (renderError === null) {