Custom Code Experiment •
Music: As The Sting 1
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>As The Sting — Fullscreen Visualizer</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%; height: 100%;
background: #05060f;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#root {
position: relative;
width: 100%; height: 100%;
}
#vis-canvas {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
display: block;
z-index: 1;
}
#controls {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
width: 90%;
max-width: 560px;
background: rgba(5, 6, 15, 0.6);
backdrop-filter: blur(12px);
border-radius: 32px;
padding: 18px 24px;
border: 1px solid rgba(255,255,255,0.1);
transition: opacity 0.3s;
}
#track-info { text-align: center; }
#track-title {
font-size: 16px; font-weight: 500;
color: rgba(255,255,255,0.95); letter-spacing: 0.04em;
}
#track-artist {
font-size: 12px; color: rgba(255,255,255,0.5); margin-top: 3px;
}
#progress-wrap {
width: 100%; display: flex; align-items: center; gap: 10px;
}
#time-cur, #time-dur {
font-size: 11px; color: rgba(255,255,255,0.6);
min-width: 34px; font-variant-numeric: tabular-nums;
}
#time-dur { text-align: right; }
#progress-bar {
flex: 1; height: 4px;
background: rgba(255,255,255,0.2);
border-radius: 2px; position: relative;
cursor: pointer; overflow: visible;
}
#progress-fill {
height: 100%; width: 0%;
border-radius: 2px;
background: rgba(255,255,255,0.9);
pointer-events: none;
transition: width 0.05s linear;
}
#progress-thumb {
position: absolute; right: -6px; top: 50%;
transform: translateY(-50%);
width: 12px; height: 12px;
border-radius: 50%;
background: #fff;
pointer-events: none;
opacity: 0; transition: opacity 0.2s;
}
#progress-bar:hover #progress-thumb { opacity: 1; }
#play-btn {
width: 54px; height: 54px;
border-radius: 50%;
border: none;
background: rgba(255,255,255,0.15);
cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 26px; color: white;
transition: transform 0.12s, background 0.15s;
flex-shrink: 0;
backdrop-filter: blur(4px);
}
#play-btn:hover { background: rgba(255,255,255,0.25); transform: scale(1.06); }
#play-btn:active { transform: scale(0.95); }
#btn-row {
display: flex; align-items: center; justify-content: space-between;
width: 100%;
gap: 18px;
}
#vol-wrap { display: flex; align-items: center; gap: 8px; flex: 1; }
#vol-icon { font-size: 18px; color: rgba(255,255,255,0.7); }
#vol-slider {
-webkit-appearance: none; appearance: none;
width: 80px; height: 4px;
background: rgba(255,255,255,0.3);
border-radius: 2px; outline: none; cursor: pointer;
}
#vol-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 12px; height: 12px;
border-radius: 50%; background: rgba(255,255,255,0.9);
cursor: pointer;
}
#vol-slider::-moz-range-thumb {
width: 12px; height: 12px;
border-radius: 50%; background: rgba(255,255,255,0.9);
cursor: pointer; border: none;
}
#loading-msg {
position: absolute;
bottom: 40%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 3;
font-size: 14px;
color: rgba(255,255,255,0.5);
background: rgba(0,0,0,0.4);
padding: 8px 18px;
border-radius: 40px;
backdrop-filter: blur(8px);
pointer-events: none;
font-weight: 400;
letter-spacing: 0.5px;
white-space: nowrap;
}
@media (max-width: 600px) {
#controls { padding: 14px 18px; bottom: 16px; width: 92%; }
#vol-slider { width: 60px; }
#play-btn { width: 48px; height: 48px; font-size: 22px; }
#loading-msg { font-size: 12px; white-space: nowrap; }
}
</style>
</head>
<body>
<div id="root">
<canvas id="vis-canvas"></canvas>
<div id="loading-msg">🎧 Click play to load audio</div>
<div id="controls">
<div id="track-info">
<div id="track-title">As The Sting 1</div>
<div id="track-artist">Mike H</div>
</div>
<div id="progress-wrap">
<span id="time-cur">0:00</span>
<div id="progress-bar">
<div id="progress-fill"></div>
<div id="progress-thumb"></div>
</div>
<span id="time-dur">0:00</span>
</div>
<div id="btn-row">
<div id="vol-wrap">
<i class="ti ti-volume" id="vol-icon" aria-hidden="true"></i>
<input type="range" id="vol-slider" min="0" max="1" step="0.01" value="0.8">
</div>
<button id="play-btn" aria-label="Play">
<i class="ti ti-player-play" id="play-icon"></i>
</button>
<div style="width: 96px;"></div>
</div>
</div>
</div>
<script>
(function () {
const canvas = document.getElementById('vis-canvas');
const ctx = canvas.getContext('2d');
const playBtn = document.getElementById('play-btn');
const playIcon = document.getElementById('play-icon');
const progressBar = document.getElementById('progress-bar');
const progressFill = document.getElementById('progress-fill');
const timeCur = document.getElementById('time-cur');
const timeDur = document.getElementById('time-dur');
const loadingMsg = document.getElementById('loading-msg');
const volSlider = document.getElementById('vol-slider');
const volIcon = document.getElementById('vol-icon');
let audioCtx, analyser, source, gainNode;
let audioBuffer = null;
let isPlaying = false;
let startTime = 0;
let pauseOffset = 0;
let freqData, waveData;
let duration = 0;
let initialized = false;
const AUDIO_URL = 'https://cdn.jsdelivr.net/gh/alaric-visi/music@main/as-the-sting/as-the-sting-1.mp3';
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const DPR = window.devicePixelRatio || 1;
canvas.width = width * DPR;
canvas.height = height * DPR;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
window.addEventListener('resize', () => { resizeCanvas(); nodes = []; });
resizeCanvas();
function fmt(t) {
if (!isFinite(t) || t < 0) t = 0;
const m = Math.floor(t / 60);
const s = Math.floor(t % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
async function initAudio() {
if (initialized) return;
initialized = true;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioCtx.createGain();
gainNode.gain.value = parseFloat(volSlider.value);
gainNode.connect(audioCtx.destination);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.82;
analyser.connect(gainNode);
freqData = new Uint8Array(analyser.frequencyBinCount);
waveData = new Uint8Array(analyser.fftSize);
loadingMsg.textContent = 'Loading audio...';
try {
const res = await fetch(AUDIO_URL);
const ab = await res.arrayBuffer();
audioBuffer = await audioCtx.decodeAudioData(ab);
duration = audioBuffer.duration;
timeDur.textContent = fmt(duration);
loadingMsg.style.display = 'none';
play();
} catch (e) {
loadingMsg.textContent = 'Failed to load audio.';
initialized = false;
}
}
function play() {
if (!audioBuffer) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
source = audioCtx.createBufferSource();
source.buffer = audioBuffer;
source.connect(analyser);
source.start(0, pauseOffset);
startTime = audioCtx.currentTime - pauseOffset;
isPlaying = true;
playIcon.className = 'ti ti-player-pause';
source.onended = () => {
if (isPlaying) { pauseOffset = 0; isPlaying = false; playIcon.className = 'ti ti-player-play'; }
};
}
function pause() {
if (source) { source.onended = null; source.stop(); }
pauseOffset = Math.max(0, audioCtx.currentTime - startTime);
isPlaying = false;
playIcon.className = 'ti ti-player-play';
}
playBtn.addEventListener('click', async () => {
if (!initialized) { await initAudio(); return; }
if (!audioBuffer) return;
if (isPlaying) { pause(); } else { play(); }
});
volSlider.addEventListener('input', () => {
if (gainNode) gainNode.gain.value = parseFloat(volSlider.value);
volIcon.className = parseFloat(volSlider.value) < 0.01 ? 'ti ti-volume-off' : 'ti ti-volume';
});
progressBar.addEventListener('click', (e) => {
if (!audioBuffer) return;
const rect = progressBar.getBoundingClientRect();
const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
pauseOffset = frac * duration;
if (isPlaying) { if (source) { source.onended = null; source.stop(); } play(); }
else { timeCur.textContent = fmt(pauseOffset); progressFill.style.width = (frac * 100) + '%'; }
});
let nodes = [];
let particles = [];
let tentacles = [];
let time = 0;
function initScene(W, H) {
nodes = [];
const count = 32;
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const r = Math.min(W, H) * 0.18 + Math.random() * 40;
nodes.push({
x: W / 2 + Math.cos(angle) * r,
y: H / 2 + Math.sin(angle) * r,
angle, baseR: r,
size: 2.5 + Math.random() * 3,
freqBin: Math.floor(Math.random() * 80 + 2),
hue: 160 + Math.random() * 140,
phase: Math.random() * Math.PI * 2
});
}
tentacles = [];
for (let i = 0; i < 10; i++) {
tentacles.push({ angle: (i / 10) * Math.PI * 2, hue: 180 + i * 18 });
}
particles = [];
}
function spawnParticles(W, H, energy) {
if (particles.length > 180) return;
const count = Math.min(24, Math.floor(energy * 5));
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.2 + Math.random() * energy * 4;
particles.push({
x: W / 2, y: H / 2,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1, decay: 0.01 + Math.random() * 0.012,
size: 1.2 + Math.random() * 2.8,
hue: 160 + Math.random() * 140
});
}
}
function draw() {
requestAnimationFrame(draw);
time += 0.016;
const W = canvas.width / (window.devicePixelRatio || 1);
const H = canvas.height / (window.devicePixelRatio || 1);
if (W === 0 || H === 0) return;
ctx.clearRect(0, 0, W, H);
if (nodes.length === 0) initScene(W, H);
let energy = 0, bassEnergy = 0, midEnergy = 0, highEnergy = 0;
if (analyser && isPlaying && freqData) {
analyser.getByteFrequencyData(freqData);
analyser.getByteTimeDomainData(waveData);
let sum = 0;
for (let i = 0; i < freqData.length; i++) sum += freqData[i];
energy = sum / (freqData.length * 255);
let bassSum = 0, midSum = 0, highSum = 0;
for (let i = 0; i < 12; i++) bassSum += freqData[i];
bassEnergy = bassSum / (12 * 255);
for (let i = 12; i < 80; i++) midSum += freqData[i];
midEnergy = midSum / (68 * 255);
for (let i = 80; i < 200; i++) highSum += freqData[i];
highEnergy = highSum / (120 * 255);
energy = Math.min(1, energy * 1.2);
} else {
const idle = (Math.sin(time * 0.7) * 0.5 + 0.5) * 0.07;
energy = idle; bassEnergy = idle * 0.4; midEnergy = idle * 0.3; highEnergy = idle * 0.1;
}
const centerX = W / 2, centerY = H / 2;
const maxDim = Math.max(W, H);
const bgGrad = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, maxDim * 0.7);
bgGrad.addColorStop(0, `hsla(230,30%,8%,1)`);
bgGrad.addColorStop(1, `hsla(230,20%,2%,1)`);
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, W, H);
const glowRad = 90 + bassEnergy * 180;
const glowGrad = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, glowRad);
glowGrad.addColorStop(0, `hsla(${190 + energy * 60},85%,60%,${0.22 + bassEnergy * 0.6})`);
glowGrad.addColorStop(0.6, `hsla(${210 + energy * 40},70%,45%,${0.08 + midEnergy * 0.2})`);
glowGrad.addColorStop(1, `hsla(220,60%,20%,0)`);
ctx.fillStyle = glowGrad;
ctx.beginPath();
ctx.arc(centerX, centerY, glowRad, 0, Math.PI * 2);
ctx.fill();
for (let i = 0; i < 6; i++) {
const pulse = bassEnergy * (1 - i * 0.12) + midEnergy * 0.2;
const r = 45 + i * 32 + pulse * 70 + Math.sin(time * 2.2 + i) * 5;
ctx.save(); ctx.translate(centerX, centerY);
ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.strokeStyle = `hsla(${170 + i * 20 + energy * 50},75%,68%,${0.12 + pulse * 0.55})`;
ctx.lineWidth = 1 + pulse * 3;
ctx.stroke(); ctx.restore();
}
if (freqData) {
const bins = 140;
const binStep = Math.floor(freqData.length / bins);
ctx.save(); ctx.translate(centerX, centerY);
for (let i = 0; i < bins; i++) {
let val = 0;
for (let j = 0; j < binStep; j++) val += freqData[(i * binStep + j) % freqData.length];
val = (val / (binStep * 255)) * 1.2;
const angle = (i / bins) * Math.PI * 2 - Math.PI / 2;
const innerR = 42, len = val * 130 + 3;
const hue = 180 + (i / bins) * 140 + energy * 50;
ctx.beginPath();
ctx.moveTo(Math.cos(angle) * innerR, Math.sin(angle) * innerR);
ctx.lineTo(Math.cos(angle) * (innerR + len), Math.sin(angle) * (innerR + len));
ctx.strokeStyle = `hsla(${hue},85%,68%,${0.35 + val * 0.65})`;
ctx.lineWidth = 1.2 + val * 3.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(Math.cos(angle) * -innerR * 0.8, Math.sin(angle) * -innerR * 0.8);
ctx.lineTo(Math.cos(angle) * -(innerR * 0.8 + len * 0.5), Math.sin(angle) * -(innerR * 0.8 + len * 0.5));
ctx.strokeStyle = `hsla(${hue + 40},80%,60%,${0.2 + val * 0.5})`;
ctx.lineWidth = 0.8 + val * 1.8;
ctx.stroke();
}
ctx.restore();
}
for (let t = 0; t < tentacles.length; t++) {
const tent = tentacles[t];
const baseAngle = tent.angle + time * 0.12;
const freqIdx = Math.floor((t / tentacles.length) * 70 + 5) % (freqData?.length || 1);
const fval = (freqData && freqData[freqIdx]) ? freqData[freqIdx] / 255 : 0.05;
ctx.save(); ctx.translate(centerX, centerY);
ctx.beginPath();
let px = Math.cos(baseAngle) * 50, py = Math.sin(baseAngle) * 50;
ctx.moveTo(px, py);
const segs = 16;
for (let s = 1; s <= segs; s++) {
const frac = s / segs;
const wobble = Math.sin(time * 3.2 + s * 0.7 + t) * fval * 42 * frac;
const r = 50 + frac * 130 * (1 + fval * 1.2);
const angleOff = wobble * 0.05;
const nx = Math.cos(baseAngle + angleOff) * r;
const ny = Math.sin(baseAngle + angleOff) * r;
const cp1x = px + (nx - px) * 0.4 + Math.cos(baseAngle + Math.PI / 2.5) * wobble;
const cp1y = py + (ny - py) * 0.4 + Math.sin(baseAngle + Math.PI / 2.5) * wobble;
ctx.quadraticCurveTo(cp1x, cp1y, nx, ny);
px = nx; py = ny;
}
ctx.strokeStyle = `hsla(${tent.hue + energy * 70},85%,70%,${0.18 + fval * 0.55})`;
ctx.lineWidth = 1 + fval * 3;
ctx.stroke(); ctx.restore();
}
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
const fval = (freqData && freqData[Math.min(n.freqBin, freqData.length - 1)]) ? freqData[Math.min(n.freqBin, freqData.length - 1)] / 255 : 0.05;
const pulse = Math.sin(time * 2 + n.phase) * 0.5 + 0.5;
const targetR = n.baseR + fval * 70 + bassEnergy * 40;
n.x += (centerX + Math.cos(n.angle) * targetR - n.x) * 0.12;
n.y += (centerY + Math.sin(n.angle) * targetR - n.y) * 0.12;
const s = n.size + fval * 9 + pulse * 2.2;
const alpha = 0.55 + fval * 0.5;
for (let j = i + 1; j < nodes.length; j++) {
const n2 = nodes[j];
const dx = n2.x - n.x, dy = n2.y - n.y;
const dist = Math.hypot(dx, dy);
if (dist < 150) {
const lineAlpha = (1 - dist / 150) * 0.3 * (0.5 + energy * 0.6);
ctx.beginPath(); ctx.moveTo(n.x, n.y); ctx.lineTo(n2.x, n2.y);
ctx.strokeStyle = `hsla(${(n.hue + n2.hue) / 2},80%,68%,${lineAlpha})`;
ctx.lineWidth = 0.6 + fval * 1.5;
ctx.stroke();
}
}
const nodeGlow = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, s * 4);
nodeGlow.addColorStop(0, `hsla(${n.hue + energy * 60},90%,75%,${alpha * 0.8})`);
nodeGlow.addColorStop(1, `hsla(${n.hue},80%,55%,0)`);
ctx.fillStyle = nodeGlow;
ctx.beginPath(); ctx.arc(n.x, n.y, s * 3.8, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = `hsla(${n.hue + fval * 40},95%,85%,${alpha})`;
ctx.beginPath(); ctx.arc(n.x, n.y, s, 0, Math.PI * 2); ctx.fill();
}
if (waveData) {
const waveRad = 55;
ctx.save(); ctx.translate(centerX, centerY);
ctx.beginPath();
for (let i = 0; i < waveData.length; i += 2) {
const angle = (i / waveData.length) * Math.PI * 2 - Math.PI / 2;
const sample = (waveData[i] - 128) / 128;
const r = waveRad + sample * (18 + midEnergy * 55);
const x = Math.cos(angle) * r, y = Math.sin(angle) * r;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.strokeStyle = `hsla(${200 + energy * 70},85%,72%,${0.5 + midEnergy * 0.5})`;
ctx.lineWidth = 1.4 + midEnergy * 2.5;
ctx.stroke(); ctx.restore();
}
if (energy > 0.1) spawnParticles(W, H, energy);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx; p.y += p.vy;
p.vx *= 0.985; p.vy *= 0.985;
p.life -= p.decay;
if (p.life <= 0 || p.x < -100 || p.x > W + 100 || p.y < -100 || p.y > H + 100) {
particles.splice(i, 1);
continue;
}
ctx.fillStyle = `hsla(${p.hue},95%,78%,${p.life * 0.85})`;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2); ctx.fill();
}
if (highEnergy > 0.1) {
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2 + time * 0.6;
const len = 70 + highEnergy * 100;
ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(angle);
ctx.beginPath();
for (let x = -len; x < len; x += 3) {
const y = Math.sin(x * 0.28 + time * 9) * highEnergy * 18;
if (x === -len) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = `hsla(${280 + i * 25},85%,72%,${highEnergy * 0.55})`;
ctx.lineWidth = 1; ctx.stroke(); ctx.restore();
}
}
if (isPlaying && audioCtx && duration) {
const cur = Math.min(Math.max(0, audioCtx.currentTime - startTime), duration);
const percent = (cur / duration) * 100;
progressFill.style.width = percent + '%';
timeCur.textContent = fmt(cur);
}
}
draw();
})();
</script>
</body>
</html>