This repository has been archived on 2022-09-16. You can view files and clone it, but cannot push or open issues/pull-requests.
tlaternet-templates/src/music/components/visualizer.tsx

219 lines
5.7 KiB
TypeScript

import React from "react";
import * as three from "three";
type VisualizerProps = {
audioContext: AudioContext;
audioSource: AudioNode;
};
class CanvasDrawer {
private analyser: AnalyserNode;
private canvas: HTMLCanvasElement;
private analyserData: Float32Array;
private boxes: Array<three.Mesh>;
private camera: three.Camera;
private renderer: three.Renderer;
private scene: three.Scene;
private angle: number;
private animationFrame: number;
private lastTime: number;
constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) {
this.analyser = analyser;
this.canvas = canvas;
// Set up analyser data storage
this.analyserData = new Float32Array(analyser.frequencyBinCount);
// Initialize the scene
this.scene = new three.Scene();
// Make a bunch of boxes to represent the bars
this.boxes = Array(analyser.frequencyBinCount);
let width = 2 / analyser.frequencyBinCount;
for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
let geometry = new three.BoxGeometry(1, 1, 1);
let material = new three.MeshLambertMaterial({
color: new three.Color(0x99d1ce),
});
let cube = new three.Mesh(geometry, material);
cube.scale.set(width, 1e-6, width);
cube.position.set(-1 + freq * width, 0, 0);
this.scene.add(cube);
this.boxes[freq] = cube;
}
// Add lights for shadowing
let ambientLight = new three.AmbientLight(0xffffff, 0.4);
this.scene.add(ambientLight);
let directionalLight = new three.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-1, 0.3, -1);
directionalLight.castShadow = true;
this.scene.add(directionalLight);
// Add a camera
this.angle = 3;
this.camera = new three.PerspectiveCamera(
70,
canvas.width / canvas.height,
0.01,
10
);
this.camera.lookAt(0, 0, 0);
this.scene.add(this.camera);
this.rotateCamera(1);
// Add a renderer
this.renderer = new three.WebGLRenderer({
antialias: true,
canvas: canvas,
powerPreference: "low-power",
});
this.renderer.setClearColor(new three.Color(0x0f0f0f));
this.renderer.setSize(canvas.width, canvas.height);
// Set up canvas resizing
window.addEventListener("resize", this.resize.bind(this));
// Run the first, set the first animation frame time and start requesting
// animation frames
this.resize();
this.lastTime = 0;
this.animationFrame = requestAnimationFrame(this.render.bind(this));
}
render(time: number) {
// Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame
this.animationFrame = 0;
// Update elapsed time
let elapsed = time - this.lastTime;
this.lastTime = time;
let camera = this.camera;
let renderer = this.renderer;
let scene = this.scene;
this.scaleBoxes();
this.rotateCamera(elapsed);
renderer.render(scene, camera);
this.animationFrame = requestAnimationFrame(this.render.bind(this));
}
scaleBoxes() {
let analyser = this.analyser;
analyser.getFloatFrequencyData(this.analyserData);
for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
let height = analyser.maxDecibels / this.analyserData[freq];
if (height > 0.3) {
height -= 0.3;
} else {
height = 1e-6;
}
this.boxes[freq].scale.y = height;
}
}
rotateCamera(elapsed: number) {
if (this.angle >= Math.PI * 2) {
this.angle = 0;
} else {
this.angle += 0.1 * (elapsed / 1000);
}
let camera = this.camera;
let angle = this.angle;
camera.position.x = 1.01 * Math.sin(angle);
camera.position.z = 1.01 * Math.cos(angle);
/* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */
camera.lookAt(0, 0, 0);
}
resize() {
let canvas = this.canvas;
if (canvas.parentElement === null) {
throw Error("Could not access canvas parent for size calculation");
}
// Compute the height of all our siblings
let combinedHeight = 0;
for (let i = 0; i < canvas.parentElement.children.length; i++) {
const child = canvas.parentElement.children[i];
if (child != canvas) {
combinedHeight += child.clientHeight;
}
}
// The remaining space we want to fill
let remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
canvas.height = remainingHeight;
canvas.width = canvas.parentElement.clientWidth;
this.camera.aspect = canvas.width / remainingHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(canvas.width, remainingHeight);
}
stop() {
if (this.animationFrame != 0) {
cancelAnimationFrame(this.animationFrame);
}
}
}
class Visualizer extends React.Component<VisualizerProps, {}> {
private analyser: AnalyserNode;
private canvas: React.RefObject<HTMLCanvasElement>;
private drawer: CanvasDrawer;
constructor(props: VisualizerProps) {
super(props);
this.canvas = React.createRef();
}
render() {
return (
<canvas
id="visualizer"
ref={this.canvas}
style={{ width: "100%", height: "100%" }}
></canvas>
);
}
componentDidMount() {
if (this.canvas.current === null) {
throw Error("Failed to create canvas; aborting");
}
this.analyser = this.props.audioContext.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8;
this.props.audioSource.connect(this.analyser);
this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
}
componentWillUnmount() {
this.drawer.stop();
this.props.audioSource.disconnect(this.analyser);
}
}
export default Visualizer;