Source code for the music visualiser:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced Electric Wave Visualizer</title>
<script src="
cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #020204;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color:
#ffffff;
}
.glass-panel {
background: rgba(15, 15, 20, 0.45);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255,255,255,0.02);
}
.glass-btn {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color:
#ffffff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
}
.glass-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.glass-btn:active {
transform: translateY(0);
background: rgba(255, 255, 255, 0.05);
}
input[type=range] {
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
height: 3px;
border-radius: 2px;
outline: none;
transition: background 0.3s ease;
}
input[type=range]:hover {
background: rgba(255, 255, 255, 0.2);
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 10px;
height: 10px;
border-radius: 50%;
background:
#fff;
cursor: pointer;
box-shadow: 0 0 10px rgba(255,255,255,0.8);
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.4);
}
input[type="file"] { display: none; }
#canvas-container {
position: absolute;
top: 0; left: 0; width: 100vw; height: 100vh;
z-index: 0;
}
#drop-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
border: 4px dashed rgba(255,255,255,0.2);
margin: 10px;
border-radius: 20px;
}
#drop-overlay.active {
opacity: 1;
}
.marquee-container {
overflow: hidden;
white-space: nowrap;
mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
-webkit-mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
}
.marquee {
display: inline-block;
animation: marquee 10s linear infinite;
}
@keyframes marquee {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
</style>
<script type="importmap">
{
"imports": {
"three": "
cdn.jsdelivr.net/npm/three@0…",
"three/addons/": "
cdn.jsdelivr.net/npm/three@0…"
}
}
</script>
<div id="canvas-container"></div>
<div id="drop-overlay">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mb-4 text-white/70">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<h2 class="text-2xl font-light tracking-wider">Drop Audio File Here</h2>
</div>
<div class="fixed bottom-4 left-1/2 -translate-x-1/2 flex flex-col p-2.5 rounded-xl glass-panel z-50 w-[85%] max-w-[240px] gap-2">
<div class="flex items-center gap-2 w-full">
<span id="currentTime" class="text-[9px] text-white/50 font-mono w-7 text-right">0:00</span>
<input type="range" id="progressSlider" value="0" min="0" max="100" step="0.1" class="flex-1 w-full cursor-pointer">
<span id="totalTime" class="text-[9px] text-white/50 font-mono w-7 text-left">0:00</span>
</div>
<div class="flex items-center justify-center gap-3">
<label class="glass-btn w-7 h-7 rounded-full cursor-pointer" title="Upload Audio">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<input type="file" id="fileInput" accept="audio/*">
</label>
<button id="playPauseBtn" class="glass-btn w-9 h-9 rounded-full mx-1" title="Play/Pause">
<svg id="playIcon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="margin-left: 2px;"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<svg id="pauseIcon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="hidden"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
</button>
<button id="stopBtn" class="glass-btn w-7 h-7 rounded-full" title="Stop">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>
</button>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
let audioCtx, analyser, source;
let isPlaying = false;
let isDraggingSlider = false;
let bassIntensity = 0;
let targetBloom = 2.2;
const audio = new Audio();
audio.crossOrigin = "anonymous";
const progressSlider = document.getElementById('progressSlider');
const currentTimeEl = document.getElementById('currentTime');
const totalTimeEl = document.getElementById('totalTime');
const playIcon = document.getElementById('playIcon');
const pauseIcon = document.getElementById('pauseIcon');
const formatTime = (timeInSeconds) => {
if (isNaN(timeInSeconds)) return "0:00";
const m = Math.floor(timeInSeconds / 60);
const s = Math.floor(timeInSeconds % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
};
const dataArray = new Uint8Array(256);
const audioTexture = new THREE.DataTexture(dataArray, 256, 1, THREE.RedFormat, THREE.UnsignedByteType);
const initAudio = () => {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 512;
analyser.smoothingTimeConstant = 0.85;
source = audioCtx.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioCtx.destination);
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
};
const loadAudioFile = (file) => {
initAudio();
const objectUrl = URL.createObjectURL(file);
audio.src = objectUrl;
playAudio();
};
const playAudio = () => {
initAudio();
if (audio.src) {
audio.play();
isPlaying = true;
playIcon.classList.add('hidden');
pauseIcon.classList.remove('hidden');
}
};
const pauseAudio = () => {
if (audio.src) {
audio.pause();
isPlaying = false;
playIcon.classList.remove('hidden');
pauseIcon.classList.add('hidden');
}
};
const stopAudio = () => {
if (audio.src) {
audio.pause();
audio.currentTime = 0;
isPlaying = false;
playIcon.classList.remove('hidden');
pauseIcon.classList.add('hidden');
dataArray.fill(0);
audioTexture.image.data.set(dataArray);
audioTexture.needsUpdate = true;
progressSlider.value = 0;
currentTimeEl.textContent = "0:00";
}
};
document.getElementById('fileInput').addEventListener('change', function(e) {
if (this.files.length > 0) loadAudioFile(this.files[0]);
});
document.getElementById('playPauseBtn').addEventListener('click', () => {
isPlaying ? pauseAudio() : playAudio();
});
document.getElementById('stopBtn').addEventListener('click', stopAudio);
const dropOverlay = document.getElementById('drop-overlay');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
window.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
window.addEventListener('dragenter', () => dropOverlay.classList.add('active'));
window.addEventListener('dragleave', (e) => {
if (e.relatedTarget === null) dropOverlay.classList.remove('active');
});
window.addEventListener('drop', (e) => {
dropOverlay.classList.remove('active');
let dt = e.dataTransfer;
let files = dt.files;
if (files.length && files[0].type.startsWith('audio/')) {
loadAudioFile(files[0]);
}
});
audio.addEventListener('timeupdate', () => {
if (audio.duration && !isDraggingSlider) {
progressSlider.value = (audio.currentTime / audio.duration) * 100;
currentTimeEl.textContent = formatTime(audio.currentTime);
}
});
audio.addEventListener('loadedmetadata', () => {
totalTimeEl.textContent = formatTime(audio.duration);
});
audio.addEventListener('ended', stopAudio);
progressSlider.addEventListener('input', (e) => {
isDraggingSlider = true;
if (audio.duration) {
currentTimeEl.textContent = formatTime((
e.target.value / 100) * audio.duration);
}
});
progressSlider.addEventListener('change', (e) => {
if (audio.duration) {
audio.currentTime = (
e.target.value / 100) * audio.duration;
}
isDraggingSlider = false;
});
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020204, 0.03);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 12);
let targetCameraX = 0;
let targetCameraY = 0;
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x020204);
container.appendChild(renderer.domElement);
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.8,
1.2,
0.05
);
const composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
const particleCount = 800;
const pGeometry = new THREE.BufferGeometry();
const pPositions = new Float32Array(particleCount * 3);
const pSizes = new Float32Array(particleCount);
for(let i=0; i < particleCount; i ) {
pPositions[i*3] = (Math.random() - 0.5) * 40;
pPositions[i*3 1] = (Math.random() - 0.5) * 20;
pPositions[i*3 2] = (Math.random() - 0.5) * 30 - 5;
pSizes[i] = Math.random();
}
pGeometry.setAttribute('position', new THREE.BufferAttribute(pPositions, 3));
pGeometry.setAttribute('aSize', new THREE.BufferAttribute(pSizes, 1));
const pMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uBass: { value: 0 }
},
vertexShader: `
uniform float uTime;
uniform float uBass;
attribute float aSize;
varying float vAlpha;
varying vec3 vPos;
void main() {
vec3 pos = position;
pos.x = sin(uTime * 0.2 pos.y) * 0.5;
pos.y = cos(uTime * 0.15 pos.x) * 0.5;
vPos = pos;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = aSize * (15.0 uBass * 40.0) * (1.0 / -mvPosition.z);
vAlpha = aSize * 0.6 (uBass * 0.6);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying float vAlpha;
varying vec3 vPos;
void main() {
float dist = length(gl_PointCoord - vec2(0.5));
if (dist > 0.5) discard;
float alpha = (0.5 - dist) * 2.0 * vAlpha;
vec3 color1 = vec3(0.0, 0.8, 1.0);
vec3 color2 = vec3(1.0, 0.2, 0.8);
vec3 color = mix(color1, color2, sin(vPos.x * 0.1 vPos.y * 0.2) * 0.5 0.5);
gl_FragColor = vec4(color, alpha * 0.4);
}
`,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const particles = new THREE.Points(pGeometry, pMaterial);
scene.add(particles);
const vertexShader = `
uniform float uTime;
uniform float uOffset;
uniform sampler2D uAudioTex;
varying vec2 vUv;
varying float vAudio;
float random (in vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
float noise (in vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
float a = random(i); float b = random(i vec2(1.0, 0.0));
float c = random(i vec2(0.0, 1.0)); float d = random(i vec2(1.0, 1.0));
vec2 u = f*f*(3.0-2.0*f);
return mix(a, b, u.x) (c - a)* u.y * (1.0 - u.x) (d - b) * u.x * u.y;
}
void main() {
vUv = uv;
float freqPos = abs(uv.x - 0.5) * 2.0;
float audio = texture2D(uAudioTex, vec2(freqPos, 0.0)).r;
vAudio = audio;
vec3 pos = position;
float t = uTime uOffset;
float idle = sin(uv.x * 5.0 t * 0.8) * 0.2 noise(vec2(uv.x * 6.0, t * 0.4)) * 0.3 - 0.15;
float energy1 = (noise(vec2(uv.x * 15.0, t * 3.0)) - 0.5) * 2.5;
float energy2 = (noise(vec2(uv.x * 30.0, t * 6.0)) - 0.5) * 1.5;
float sharpNoise = (abs(noise(vec2(uv.x * 45.0, t * 10.0)) - 0.5)) * 1.2;
float displacement = idle (energy1 energy2 sharpNoise) * pow(audio, 1.5) * 2.5;
float taper = smoothstep(0.0, 0.15, uv.x) * smoothstep(1.0, 0.85, uv.x);
pos.y = displacement * taper;
float zDisplacement = sin(uv.x * 12.0 t * 1.5) * 1.2 * audio;
pos.z = zDisplacement * taper;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;
const fragmentShader = `
uniform float uTime;
uniform float uReflection;
uniform vec3 uBaseColor;
uniform float uThickness;
uniform float uBassBoost;
varying vec2 vUv;
varying float vAudio;
void main() {
float dynamicThickness = uThickness * (1.0 vAudio * 0.5);
float core = 1.0 - abs(vUv.y - 0.5) * (2.0 / dynamicThickness);
core = pow(max(core, 0.0), 2.0);
float energy = vAudio uBassBoost * 0.5;
float glow = core * (1.0 energy * 3.5);
vec3 finalColor = mix(uBaseColor, uBaseColor vec3(0.35), core * energy * 0.5);
finalColor *= glow * uReflection;
float fadeX = smoothstep(0.0, 0.05, vUv.x) * smoothstep(1.0, 0.95, vUv.x);
gl_FragColor = vec4(finalColor, core * uReflection * fadeX);
}
`;
const baseUniforms = {
uTime: { value: 0 },
uAudioTex: { value: audioTexture },
uReflection: { value: 1.0 },
uOffset: { value: 0.0 },
uBaseColor: { value: new THREE.Color(0xffffff) },
uThickness: { value: 1.0 },
uBassBoost: { value: 0.0 }
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: baseUniforms,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide
});
const geometry = new THREE.PlaneGeometry(38, 0.1, 700, 1);
const allWaves = [];
const waveConfigs = [
{ offset: 0.0, color: new THREE.Color(0x00d2ff), thickness: 1.3, y: 0.8 },
{ offset: 20.4, color: new THREE.Color(0xff007f), thickness: 0.7, y: 1.0 },
{ offset: 45.2, color: new THREE.Color(0x7000ff), thickness: 0.35, y: 0.6 },
{ offset: 60.5, color: new THREE.Color(0x00ff88), thickness: 0.15, y: 0.9 }
];
waveConfigs.forEach(config => {
const mat = material.clone();
mat.uniforms = { ...baseUniforms,
uOffset: { value: config.offset },
uBaseColor: { value: config.color },
uThickness: { value: config.thickness },
uReflection: { value: 1.0 }
};
const wave = new THREE.Mesh(geometry, mat);
wave.position.y = config.y;
scene.add(wave);
allWaves.push(wave);
const refMat = mat.clone();
refMat.uniforms = { ...mat.uniforms, uReflection: { value: 0.35 } };
const refWave = new THREE.Mesh(geometry, refMat);
refWave.position.y = -config.y - 0.05;
refWave.scale.y = -1.0;
scene.add(refWave);
allWaves.push(refWave);
});
const floorGeo = new THREE.PlaneGeometry(60, 20);
const floorMat = new THREE.MeshBasicMaterial({
color: 0x010102,
transparent: true,
opacity: 0.7,
depthWrite: false
});
const glassFloor = new THREE.Mesh(floorGeo, floorMat);
glassFloor.rotation.x = -Math.PI / 2;
glassFloor.position.y = 0;
scene.add(glassFloor);
document.addEventListener('mousemove', (e) => {
const x = (e.clientX / window.innerWidth) * 2 - 1;
const y = -(e.clientY / window.innerHeight) * 2 1;
targetCameraX = x * 2.0;
targetCameraY = y * 1.0;
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
camera.position.x = (targetCameraX - camera.position.x) * 0.03;
camera.position.y = (targetCameraY - camera.position.y) * 0.03;
camera.lookAt(0, 0, 0);
if (isPlaying && analyser) {
analyser.getByteFrequencyData(dataArray);
audioTexture.image.data.set(dataArray);
audioTexture.needsUpdate = true;
let bassSum = 0;
for(let i=0; i<10; i ) bassSum = dataArray[i];
const newBass = (bassSum / 10.0) / 255.0;
bassIntensity = (newBass - bassIntensity) * 0.2;
targetBloom = 1.8 bassIntensity * 3.5;
} else {
bassIntensity *= 0.95;
targetBloom = 1.8;
}
bloomPass.strength = (targetBloom - bloomPass.strength) * 0.1;
allWaves.forEach(wave => {
wave.material.uniforms.uTime.value = time;
wave.material.uniforms.uAudioTex.value = audioTexture;
wave.material.uniforms.uBassBoost.value = bassIntensity;
});
pMaterial.uniforms.uTime.value = time;
pMaterial.uniforms.uBass.value = bassIntensity;
composer.render();
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>