Upload your own audio file (.mp3, .wav) or trigger the microphone. This visualizer computes complex algebraic surfaces in a 3D WebGL space, morphing the equations in sync with the music beat.
Select an audio file or enable microphone to activate visualizer responsiveness. In idle mode, it displays smooth coordinate transitions.
Algebraic Equation Preset
3D Visual Shape
3D Drawing Style
Motion Source
Theme
Auto-Rotation
Mesh Resolution (60x60)
Beat-Driven Morphing
Web Audio Analyser processes sub-bass frequencies (20-150Hz). When a bass hit exceeds the baseline energy threshold, it dynamically accelerates the morphing interpolation progress, creating snappy transitions in rhythm with the music.
Frequency Modulations
Middle frequencies (vocal ranges) scale the vertical displacement (Y-amplitude) of the algebraic grid, while high trebles map high-frequency mathematical noise onto the surface mesh vertices.
WebGL GPU Rendering
Plots coordinates using `THREE.BufferGeometry` arrays directly updating on the GPU. When solid mesh mode is enabled, face normals are recomputed on every frame to support physically accurate reflections and emissive glow.
ThreeMathVisualizer.tsx (Three.js Math Shading & Audios)
"use client";
import { useEffect, useRef } from "react";
import type { ReactNode } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { isWebGLAvailable, useWebGLAvailable } from "./isWebGLAvailable";
type EquationType = "ripple" | "saddle" | "waves" | "peaks" | "loop";
type LoopFormulaType = "ex" | "neg_ex" | "abs_x" | "combined";
type RenderStyle = "wireframe" | "points" | "solid";
type GeometryShape = "grid" | "sphere" | "cube" | "line";
type ThreeMathVisualizerProps = {
/** The Web Audio AnalyserNode to parse frequencies from. */
analyserNode: AnalyserNode | null;
/** The currently selected mathematical equation style. */
selectedEquation: EquationType;
/** Active formula preset for the loop equation style. */
loopFormula: LoopFormulaType;
/** Visual render mode: wireframe grid, particles, or solid surface. */
renderStyle: RenderStyle;
/** Color theme palette name. */
colorTheme: "cyan" | "violet" | "green" | "red" | "gold";
/** Resolution of the plotting grid (e.g., 50 translates to a 50x50 grid). */
gridResolution?: number;
/** Rendered when WebGL is unavailable. */
fallback?: ReactNode;
/** Whether the audio is currently playing. */
isPlaying: boolean;
/** Geometry visual shape selection: grid, sphere, cube, or 1D line wave. */
geometryShape: GeometryShape;
/** When enabled, mesh stays static and only deforms/moves with active audio playback. */
audioOnlyMode: boolean;
/** When false, automatic element rotation is completely disabled. */
enableRotation: boolean;
};
// Map color themes to hex colors for lines/points and solid mesh face colors
const THEME_COLORS = {
cyan: { primary: 0x00f0ff, secondary: 0x005f73, mesh: 0x0077b6 },
violet: { primary: 0x9d4edd, secondary: 0x3c096c, mesh: 0x5a189a },
green: { primary: 0x38b000, secondary: 0x007200, mesh: 0x008000 },
red: { primary: 0xff0054, secondary: 0x9e0059, mesh: 0xff5400 },
gold: { primary: 0xffb703, secondary: 0xfb8500, mesh: 0xff9f1c },
};
const ThreeMathVisualizer = ({
analyserNode,
selectedEquation,
loopFormula,
renderStyle,
colorTheme,
gridResolution = 60,
fallback,
isPlaying,
geometryShape,
audioOnlyMode,
enableRotation,
}: ThreeMathVisualizerProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const webglAvailable = useWebGLAvailable();
// Keep references to values we need to read dynamically inside the requestAnimationFrame loop
const selectedEquationRef = useRef(selectedEquation);
const loopFormulaRef = useRef(loopFormula);
const renderStyleRef = useRef(renderStyle);
const colorThemeRef = useRef(colorTheme);
const isPlayingRef = useRef(isPlaying);
const geometryShapeRef = useRef(geometryShape);
const audioOnlyModeRef = useRef(audioOnlyMode);
const enableRotationRef = useRef(enableRotation);
useEffect(() => {
selectedEquationRef.current = selectedEquation;
}, [selectedEquation]);
useEffect(() => {
loopFormulaRef.current = loopFormula;
}, [loopFormula]);
useEffect(() => {
renderStyleRef.current = renderStyle;
}, [renderStyle]);
useEffect(() => {
colorThemeRef.current = colorTheme;
}, [colorTheme]);
useEffect(() => {
isPlayingRef.current = isPlaying;
}, [isPlaying]);
useEffect(() => {
geometryShapeRef.current = geometryShape;
}, [geometryShape]);
useEffect(() => {
audioOnlyModeRef.current = audioOnlyMode;
}, [audioOnlyMode]);
useEffect(() => {
enableRotationRef.current = enableRotation;
}, [enableRotation]);
useEffect(() => {
if (typeof window === "undefined" || !containerRef.current) return;
if (!isWebGLAvailable()) {
console.warn("ThreeMathVisualizer: WebGL is not available.");
return;
}
const container = containerRef.current;
const width = container.clientWidth || 600;
const height = container.clientHeight || 500;
let animationFrameId: number;
// 1. Scene setup
const scene = new THREE.Scene();
// 2. Camera setup
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
camera.position.set(0, 6, 8);
// 3. Renderer setup
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
} catch (error) {
console.error("ThreeMathVisualizer: WebGL renderer initialization failed:", error);
return;
}
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// 4. Orbit Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.05; // Don't go below ground
controls.minDistance = 3;
controls.maxDistance = 20;
// 5. Lighting (Essential for "solid" mesh style)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
const pointLight = new THREE.PointLight(0xffffff, 0.6);
pointLight.position.set(-5, 5, -5);
scene.add(pointLight);
// 6. Mathematical Grid Geometry Setup
const N = gridResolution;
// Grid (Heightmap surface)
const gridGeom = new THREE.BufferGeometry();
const gridVertexCount = (N + 1) * (N + 1);
const gridPositions = new Float32Array(gridVertexCount * 3);
const gridSize = 6.0;
const gridStep = gridSize / N;
for (let j = 0; j <= N; j++) {
const y = -gridSize / 2 + j * gridStep;
for (let i = 0; i <= N; i++) {
const x = -gridSize / 2 + i * gridStep;
const index = j * (N + 1) + i;
gridPositions[index * 3] = x;
gridPositions[index * 3 + 1] = 0;
gridPositions[index * 3 + 2] = y;
}
}
gridGeom.setAttribute("position", new THREE.BufferAttribute(gridPositions, 3));
const meshIndices: number[] = [];
for (let j = 0; j < N; j++) {
for (let i = 0; i < N; i++) {
const a = j * (N + 1) + i;
const b = j * (N + 1) + i + 1;
const c = (j + 1) * (N + 1) + i;
const d = (j + 1) * (N + 1) + i + 1;
meshIndices.push(a, b, c);
meshIndices.push(b, d, c);
}
}
gridGeom.setIndex(meshIndices);
const gridOrig = gridPositions.slice();
// Sphere Geometry
const sphereGeom = new THREE.SphereGeometry(1.8, 30, 30);
const sphereOrig = sphereGeom.getAttribute("position").array.slice() as Float32Array;
// Cube Geometry
const cubeGeom = new THREE.BoxGeometry(2.2, 2.2, 2.2, 12, 12, 12);
const cubeOrig = cubeGeom.getAttribute("position").array.slice() as Float32Array;
// 1D Line Wave Geometry
const lineGeom = new THREE.BufferGeometry();
const lineVertexCount = 100;
const linePositions = new Float32Array(lineVertexCount * 3);
const lineSize = 6.0;
const lineStep = lineSize / (lineVertexCount - 1);
for (let i = 0; i < lineVertexCount; i++) {
const x = -lineSize / 2 + i * lineStep;
linePositions[i * 3] = x;
linePositions[i * 3 + 1] = 0;
linePositions[i * 3 + 2] = 0;
}
lineGeom.setAttribute("position", new THREE.BufferAttribute(linePositions, 3));
const lineOrig = linePositions.slice();
// 7. Materials setup
const pointsMaterial = new THREE.PointsMaterial({
size: 0.05,
transparent: true,
opacity: 0.9,
});
const linesMaterial = new THREE.LineBasicMaterial({
transparent: true,
opacity: 0.8,
linewidth: 1.5,
});
const meshMaterial = new THREE.MeshStandardMaterial({
roughness: 0.2,
metalness: 0.8,
side: THREE.DoubleSide,
flatShading: true,
});
// 8. Objects creation
// Initially set to grid shape
const pointsObj = new THREE.Points(gridGeom, pointsMaterial);
const meshObj = new THREE.Mesh(gridGeom, meshMaterial);
const waveLineObj = new THREE.Line(lineGeom, linesMaterial);
scene.add(pointsObj);
scene.add(meshObj);
scene.add(waveLineObj);
// 9. Audio Analyser helpers
let freqDataArray: Uint8Array | null = null;
if (analyserNode) {
freqDataArray = new Uint8Array(analyserNode.frequencyBinCount);
}
// 10. Equation Calculations
const calculateEquationHeight = (
x: number,
y: number,
eq: EquationType,
time: number,
): number => {
switch (eq) {
case "ripple": {
const r = Math.sqrt(x * x + y * y);
return Math.sin(r * Math.PI - time * 2) * (1 / (1 + r * r * 0.5));
}
case "saddle":
// Twisting/oscillating hyperbolic paraboloid
return (x * x - y * y) * 0.2 * Math.cos(time * 1.5);
case "waves":
// Linear wave ridges moving horizontally
return Math.sin(x * 2.5 - time * 2.5) * 0.5;
case "peaks":
// Checkerboard matrix of pulsating peaks
return Math.sin(x * 2.0) * Math.sin(y * 2.0 + time * 1.5) * 0.6;
case "loop": {
// Dynamic loop preset with formula dropdown mappings
const form = loopFormulaRef.current;
switch (form) {
case "ex":
return Math.exp(x) * 0.03 * Math.cos(time * 1.8);
case "neg_ex":
return Math.exp(-x) * 0.03 * Math.cos(time * 1.8);
case "abs_x":
return Math.abs(x) * 0.25 * Math.cos(time * 1.8);
case "combined":
default: {
// Loop through: x^2+y^2=1, x^3-y^3=1, x^4+y^4=1, x^5-y^5=1
const cycle = (time * 0.4) % 4;
const idx = Math.floor(cycle);
const p = cycle - idx;
// Normalized base equations evaluated on the grid dimensions
const f0 = (x * x + y * y) * 0.15;
const f1 = (x * x * x - y * y * y) * 0.05;
const f2 = (x * x * x * x + y * y * y * y) * 0.015;
const f3 = (x * x * x * x * x - y * y * y * y * y) * 0.006;
let fVal = 0;
if (idx === 0) fVal = THREE.MathUtils.lerp(f0, f1, p);
else if (idx === 1) fVal = THREE.MathUtils.lerp(f1, f2, p);
else if (idx === 2) fVal = THREE.MathUtils.lerp(f2, f3, p);
else fVal = THREE.MathUtils.lerp(f3, f0, p);
return Math.sin(fVal * Math.PI - time * 1.5) * 0.5;
}
}
}
default:
return 0;
}
};
// 11. Render state variables
let time = 0;
let activeEq: EquationType = selectedEquation;
let targetEq: EquationType = selectedEquation;
let morphProgress = 1.0;
// Intersection observer to skip render loops when container is hidden
let isIntersecting = true;
const intersectionObserver = new IntersectionObserver(
([entry]) => {
isIntersecting = entry.isIntersecting;
},
{ threshold: 0.05 },
);
intersectionObserver.observe(container);
// Resize listener
const handleResize = () => {
const w = container.clientWidth;
const h = container.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
};
window.addEventListener("resize", handleResize);
// 12. Animation Loop
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
if (!isIntersecting) return;
// Read audio data if available
let bassEnergy = 0;
let midEnergy = 0;
let trebleEnergy = 0;
if (analyserNode && freqDataArray) {
analyserNode.getByteFrequencyData(freqDataArray as unknown as Uint8Array<ArrayBuffer>);
const binCount = freqDataArray.length;
// Bass frequencies average (e.g. index 0-15)
let bassSum = 0;
const bassEnd = Math.min(16, binCount);
for (let i = 0; i < bassEnd; i++) {
bassSum += freqDataArray[i];
}
bassEnergy = bassSum / (bassEnd * 255);
// Mids frequencies average (e.g. index 16-120)
let midSum = 0;
const midStart = 16;
const midEnd = Math.min(120, binCount);
for (let i = midStart; i < midEnd; i++) {
midSum += freqDataArray[i];
}
midEnergy = midSum / ((midEnd - midStart) * 255);
// Treble frequencies average (e.g. index 121-256)
let trebleSum = 0;
const trebleStart = Math.min(121, binCount);
const trebleEnd = Math.min(256, binCount);
for (let i = trebleStart; i < trebleEnd; i++) {
trebleSum += freqDataArray[i];
}
trebleEnergy = trebleSum / (Math.max(1, trebleEnd - trebleStart) * 255);
}
// 12.1 Dynamic Speeds & Audio Only Freezing
const hasAudio =
analyserNode && isPlayingRef.current && (bassEnergy > 0.005 || midEnergy > 0.005);
let timeSpeed = 0;
let rotationSpeed = 0;
let morphSpeed = 0;
if (audioOnlyModeRef.current) {
if (hasAudio) {
timeSpeed = midEnergy * 0.09;
morphSpeed = 0.005 + bassEnergy * 0.045;
rotationSpeed = bassEnergy * 0.025;
} else {
timeSpeed = 0;
morphSpeed = 0;
rotationSpeed = 0;
}
} else {
timeSpeed = 0.015 + midEnergy * 0.03;
morphSpeed = 0.01 + bassEnergy * 0.035;
rotationSpeed = 0.003 + bassEnergy * 0.012;
}
if (timeSpeed > 0) {
time += timeSpeed;
}
// Handle element rotation toggle
if (enableRotationRef.current) {
if (rotationSpeed > 0) {
scene.rotation.y += rotationSpeed;
} else if (!audioOnlyModeRef.current) {
scene.rotation.y = time * 0.05;
}
} else {
scene.rotation.y = 0;
}
// Update morph transition state
if (targetEq !== selectedEquationRef.current) {
activeEq = targetEq;
targetEq = selectedEquationRef.current;
morphProgress = 0.0;
}
if (morphProgress < 1.0) {
if (!audioOnlyModeRef.current || hasAudio) {
morphProgress = Math.min(1.0, morphProgress + (morphSpeed || 0.015));
}
}
// 12.2 Identify Active Geometry & Undeformed Base Positions
let activeGeom: THREE.BufferGeometry;
let origPositions: Float32Array;
const activeShape = geometryShapeRef.current;
switch (activeShape) {
case "sphere":
activeGeom = sphereGeom;
origPositions = sphereOrig;
break;
case "cube":
activeGeom = cubeGeom;
origPositions = cubeOrig;
break;
case "line":
activeGeom = lineGeom;
origPositions = lineOrig;
break;
case "grid":
default:
activeGeom = gridGeom;
origPositions = gridOrig;
break;
}
// Swap geometry references on render objects
pointsObj.geometry = activeGeom;
meshObj.geometry = activeGeom;
// Update geometry positions attribute
const posAttr = activeGeom.getAttribute("position") as THREE.BufferAttribute;
const posArray = posAttr.array as Float32Array;
const vertexCount = posArray.length / 3;
// Reactively compute coordinate displacements
const midHeightFactor = 1.0 + midEnergy * 1.5;
const trebleNoiseFactor = trebleEnergy * 0.25;
for (let i = 0; i < vertexCount; i++) {
const x0 = origPositions[i * 3];
const y0 = origPositions[i * 3 + 1];
const z0 = origPositions[i * 3 + 2];
// Evaluate equations using horizontal coordinates
const zActive = calculateEquationHeight(x0, z0, activeEq, time);
const zTarget = calculateEquationHeight(x0, z0, targetEq, time);
// Interpolated displacement height
let height = THREE.MathUtils.lerp(zActive, zTarget, morphProgress) * midHeightFactor;
// Treble noise ripple overlay
if (trebleNoiseFactor > 0.01) {
const noise = Math.sin(x0 * 12 + z0 * 12 + time * 4.0) * trebleNoiseFactor;
height += noise;
}
// Apply deformation depending on active shape
if (activeShape === "grid") {
posArray[i * 3] = x0;
posArray[i * 3 + 1] = height; // height mapped to Y axis
posArray[i * 3 + 2] = z0;
} else if (activeShape === "line") {
posArray[i * 3] = x0;
posArray[i * 3 + 1] = height;
posArray[i * 3 + 2] = 0;
} else {
// Deform spherical or box coordinates outward/inward along radial vector
const len = Math.sqrt(x0 * x0 + y0 * y0 + z0 * z0);
if (len > 0.001) {
// Apply scale offsets relative to original center length
const scale = 1.0 + height * 0.4;
posArray[i * 3] = x0 * scale;
posArray[i * 3 + 1] = y0 * scale;
posArray[i * 3 + 2] = z0 * scale;
}
}
}
posAttr.needsUpdate = true;
// Update normals for solid mesh lighting
if (renderStyleRef.current === "solid" && activeShape !== "line") {
activeGeom.computeVertexNormals();
}
// 12.3 Toggle rendering visibility states
const style = renderStyleRef.current;
if (activeShape === "line") {
meshObj.visible = false;
pointsObj.visible = style === "points";
waveLineObj.visible = style !== "points";
} else {
waveLineObj.visible = false;
pointsObj.visible = style === "points";
meshObj.visible = style !== "points";
meshMaterial.wireframe = style === "wireframe";
}
// 12.4 React dynamically to theme colors
const themeColors = THEME_COLORS[colorThemeRef.current];
pointsMaterial.color.setHex(themeColors.primary);
linesMaterial.color.setHex(themeColors.primary);
meshMaterial.color.setHex(themeColors.mesh);
meshMaterial.emissive.setHex(themeColors.secondary);
meshMaterial.emissiveIntensity = 0.25 + midEnergy * 0.5;
controls.update();
renderer.render(scene, camera);
};
// Start loop
animate();
// Cleanup
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener("resize", handleResize);
intersectionObserver.disconnect();
controls.dispose();
if (container.contains(renderer.domElement)) {
container.removeChild(renderer.domElement);
}
gridGeom.dispose();
sphereGeom.dispose();
cubeGeom.dispose();
lineGeom.dispose();
pointsMaterial.dispose();
linesMaterial.dispose();
meshMaterial.dispose();
renderer.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gridResolution, analyserNode]);
if (!webglAvailable && fallback) return <>{fallback}</>;
return (
<div
ref={containerRef}
style={{
width: "100%",
height: "100%",
minHeight: "500px",
position: "relative",
}}
/>
);
};
export default ThreeMathVisualizer;