Custom Code Experiment •
Music: Ariomaniacs Part 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">
<title>Ariomaniacs Part 1</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
#bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: calc(100% - 110px); z-index: 2; cursor: grab; }
#animationCanvas.dragging { cursor: grabbing; }
#controls {
position: absolute; bottom: 0; left: 0; right: 0;
z-index: 10;
display: flex; flex-direction: column; align-items: center;
gap: 10px; padding: 10px 0 14px;
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
}
#track-info { text-align: center; }
#track-title { font-size: 14px; font-weight: 500; color: rgba(255,200,40,0.9); letter-spacing: 0.06em; font-family: monospace; }
#track-artist { font-size: 11px; color: rgba(255,200,40,0.4); margin-top: 2px; font-family: monospace; }
#progress-wrap { width: 88%; max-width: 580px; display: flex; align-items: center; gap: 10px; }
#time-cur, #time-dur { font-size: 11px; color: rgba(255,200,40,0.4); min-width: 34px; font-variant-numeric: tabular-nums; font-family: monospace; }
#time-dur { text-align: right; }
#progress-bar { flex: 1; height: 3px; background: rgba(255,200,40,0.15); border-radius: 2px; position: relative; cursor: pointer; }
#progress-fill { height: 100%; width: 0%; border-radius: 2px; background: rgba(255,200,40,0.7); pointer-events: none; transition: width 0.1s linear; }
#progress-thumb { position: absolute; right: -5px; top: 50%; transform: translateY(-50%); width: 10px; height: 10px; border-radius: 50%; background: rgba(255,200,40,0.9); pointer-events: none; opacity: 0; transition: opacity 0.2s; }
#progress-bar:hover #progress-thumb { opacity: 1; }
#btn-row { display: flex; align-items: center; gap: 18px; }
#play-btn { width: 48px; height: 48px; border-radius: 50%; border: 1.5px solid rgba(255,200,40,0.35); background: rgba(255,200,40,0.08); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 20px; color: rgba(255,200,40,0.9); transition: transform 0.12s, background 0.15s; flex-shrink: 0; }
#play-btn:hover { background: rgba(255,200,40,0.15); transform: scale(1.06); }
#play-btn:active { transform: scale(0.95); }
#vol-wrap { display: flex; align-items: center; gap: 8px; }
#vol-icon { font-size: 15px; color: rgba(255,200,40,0.4); }
#vol-slider { -webkit-appearance: none; appearance: none; width: 72px; height: 3px; background: rgba(255,200,40,0.2); border-radius: 2px; outline: none; cursor: pointer; }
#vol-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 11px; height: 11px; border-radius: 50%; background: rgba(255,200,40,0.85); cursor: pointer; }
#vol-slider::-moz-range-thumb { width: 11px; height: 11px; border-radius: 50%; background: rgba(255,200,40,0.85); cursor: pointer; border: none; }
#loading-msg { position: absolute; top: 45%; left: 50%; transform: translate(-50%,-50%); font-size: 13px; color: rgba(255,200,40,0.35); pointer-events: none; letter-spacing: 0.08em; z-index: 20; font-family: monospace; }
</style>
</head>
<body>
<canvas id="bgCanvas"></canvas>
<canvas id="animationCanvas"></canvas>
<div id="loading-msg">Click play to load…</div>
<div id="controls">
<div id="track-info">
<div id="track-title">Ariomaniacs Part 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"></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:88px"></div>
</div>
</div>
<script>
(function () {
const AUDIO_URL = 'https://cdn.jsdelivr.net/gh/alaric-visi/music@main/ariomaniacs/01-ariomaniacs-part-1.mp3';
// ---- Audio setup ----
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, isPlaying = false, startTime = 0, pauseOffset = 0, duration = 0;
let freqData, waveData;
let initialized = false;
// Smoothed audio values
let sBass = 0, sMid = 0, sHigh = 0, sEnergy = 0;
function fmt(t) {
if (!isFinite(t) || t < 0) t = 0;
return `${Math.floor(t/60)}:${Math.floor(t%60).toString().padStart(2,'0')}`;
}
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.80;
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 = 'Could not 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;
isPlaying ? pause() : 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)+'%'; }
});
function getAudio() {
let energy=0, bass=0, mid=0, high=0;
if (analyser && isPlaying) {
analyser.getByteFrequencyData(freqData);
analyser.getByteTimeDomainData(waveData);
for (let i=0;i<freqData.length;i++) energy += freqData[i];
energy /= freqData.length * 255;
for (let i=0;i<8;i++) bass += freqData[i]; bass /= 8*255;
for (let i=8;i<80;i++) mid += freqData[i]; mid /= 72*255;
for (let i=80;i<200;i++) high += freqData[i]; high /= 120*255;
} else {
const idle = (Math.sin(clock.getElapsedTime()*0.8)*0.5+0.5)*0.04;
energy=idle; bass=idle*0.5; mid=idle*0.3; high=idle*0.1;
}
const s = 0.18;
sBass += (bass - sBass) * (bass > sBass ? 0.5 : s);
sMid += (mid - sMid) * (mid > sMid ? 0.4 : s);
sHigh += (high - sHigh) * (high > sHigh ? 0.4 : s);
sEnergy += (energy - sEnergy) * (energy > sEnergy ? 0.4 : s);
return { bass: sBass, mid: sMid, high: sHigh, energy: sEnergy };
}
// ---- Background pixel grid ----
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
const px = 7;
let cols, rows, pixels = [];
function initBackground() {
bgCanvas.width = window.innerWidth;
bgCanvas.height = window.innerHeight;
cols = Math.ceil(bgCanvas.width / px);
rows = Math.ceil(bgCanvas.height / px);
pixels = [];
for (let i=0; i<cols*rows; i++) pixels.push({ b: Math.random()*0.3, speed: 0.001+Math.random()*0.003 });
}
function animateBackground() {
for (let y=0; y<rows; y++) {
for (let x=0; x<cols; x++) {
const p = pixels[y*cols+x];
p.b += p.speed;
if (p.b > 0.4) p.b = 0;
bgCtx.fillStyle = `rgb(${Math.floor(p.b*50)},${Math.floor(p.b*32)},${Math.floor(p.b*5)})`;
bgCtx.fillRect(x*px, y*px, px, px);
}
}
}
// ---- Three.js ----
const colors = {
yellow: 0xffc020, black: 0x1a1200, thorax: 0xe8a000,
wing: 0xffe090, vein: 0xfff4c0, eye: 0x203000,
leg: 0x2a1a00, pollen: 0xffdd00, stinger:0x3a2800,
fuzz: 0xffd060, stripe: 0xfff0a0,
};
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('animationCanvas'),
antialias: true, alpha: true
});
let scene, camera, bee;
let wingFL, wingFR, wingHL, wingHR;
let legGroups = [];
const orbit = {
theta:0.4, phi:1.0, radius:8,
isDragging:false, lastX:0, lastY:0,
autoRotate:true, autoRotateSpeed:0.003,
velTheta:0, velPhi:0,
};
const PHI_MIN=0.05, PHI_MAX=Math.PI*0.95, RADIUS_MIN=3, RADIUS_MAX=20;
const clock = new THREE.Clock();
function setCameraFromOrbit() {
camera.position.set(
orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta),
orbit.radius * Math.cos(orbit.phi),
orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta)
);
camera.lookAt(0,0,0);
camera.updateProjectionMatrix();
}
function updateSize() {
renderer.setSize(window.innerWidth, window.innerHeight - 110);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
function updateCamera() {
const w = window.innerWidth;
orbit.radius = w < 480 ? 12 : w < 768 ? 10 : 8;
camera.fov = 52;
camera.aspect = window.innerWidth / (window.innerHeight - 110);
camera.updateProjectionMatrix();
setCameraFromOrbit();
}
// Dynamic material — we'll store mats so we can tint them
const allMats = [];
function mat(color) {
const m = new THREE.MeshBasicMaterial({ color, wireframe: true });
allMats.push(m);
return m;
}
// Track "base" colors per material group
const bodyMats = [], wingMats = [], eyeMats = [], legMats = [], antennaMats = [];
function bmat(color, bucket) {
const m = new THREE.MeshBasicMaterial({ color, wireframe: true });
allMats.push(m); bucket.push({ mat: m, base: color });
return m;
}
function lerpHex(c1, c2, t) {
t = Math.max(0, Math.min(1, t));
const r1=(c1>>16)&0xff, g1=(c1>>8)&0xff, b1=c1&0xff;
const r2=(c2>>16)&0xff, g2=(c2>>8)&0xff, b2=c2&0xff;
return ((Math.round(r1+(r2-r1)*t)<<16)|(Math.round(g1+(g2-g1)*t)<<8)|Math.round(b1+(b2-b1)*t));
}
function addFuzz(group, cx, cy, cz, radius, count, color) {
for (let i=0; i<count; i++) {
const phi = Math.acos(2*Math.random()-1);
const theta = Math.random()*Math.PI*2;
const r = radius + 0.015 + Math.random()*0.04;
const fGeo = new THREE.SphereGeometry(0.018+Math.random()*0.014,4,4);
const fuzz = new THREE.Mesh(fGeo, bmat(color, bodyMats));
fuzz.position.set(cx+r*Math.sin(phi)*Math.cos(theta), cy+r*Math.sin(phi)*Math.sin(theta), cz+r*Math.cos(phi));
group.add(fuzz);
}
}
function makeWing(side, isFore) {
const group = new THREE.Group();
const s = side;
if (isFore) {
const shape = new THREE.Shape();
shape.moveTo(0,0);
shape.bezierCurveTo(s*0.4,0.30,s*1.1,0.55,s*1.7,0.40);
shape.bezierCurveTo(s*2.1,0.28,s*2.2,0.05,s*2.0,-0.18);
shape.bezierCurveTo(s*1.7,-0.38,s*1.0,-0.45,s*0.4,-0.28);
shape.bezierCurveTo(s*0.12,-0.16,0,-0.06,0,0);
const geo = new THREE.ShapeGeometry(shape,14);
group.add(new THREE.Mesh(geo, bmat(colors.wing, wingMats)));
group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({color:colors.vein})));
[{pts:[[0,0],[s*0.7,0.36],[s*1.5,0.36],[s*2.0,0.16]],r:0.012},
{pts:[[s*0.05,-0.05],[s*0.6,0.22],[s*1.3,0.22],[s*1.85,0.04]],r:0.009},
{pts:[[s*0.08,-0.08],[s*0.5,0.06],[s*1.1,0.02],[s*1.7,-0.12]],r:0.008},
{pts:[[s*0.1,-0.12],[s*0.5,-0.10],[s*1.0,-0.18],[s*1.6,-0.30]],r:0.007}
].forEach(v => {
const pts = v.pts.map(p=>new THREE.Vector3(p[0],p[1],0.01));
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts),8,v.r,4,false), bmat(colors.vein,wingMats)));
});
} else {
const shape2 = new THREE.Shape();
shape2.moveTo(0,0);
shape2.bezierCurveTo(s*0.3,0.18,s*0.9,0.32,s*1.3,0.20);
shape2.bezierCurveTo(s*1.6,0.10,s*1.6,-0.10,s*1.4,-0.22);
shape2.bezierCurveTo(s*1.1,-0.34,s*0.5,-0.30,s*0.15,-0.14);
shape2.bezierCurveTo(s*0.04,-0.06,0,0,0,0);
const geo3 = new THREE.ShapeGeometry(shape2,10);
group.add(new THREE.Mesh(geo3, bmat(colors.wing,wingMats)));
group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo3), new THREE.LineBasicMaterial({color:colors.vein})));
}
return group;
}
function makeLeg(side, index) {
const group = new THREE.Group();
const s = side;
const isHind = index===2;
const cPts = [new THREE.Vector3(0,0,0), new THREE.Vector3(s*0.22,-0.10,0.02)];
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts),4,0.030,6,false), bmat(colors.black,legMats)));
const fPts = [new THREE.Vector3(s*0.22,-0.10,0.02),new THREE.Vector3(s*0.50,-0.38,0.02),new THREE.Vector3(s*0.70,-0.55,0.00)];
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(fPts),6,0.028,6,false), bmat(isHind?colors.yellow:colors.black,legMats)));
const tPts = [new THREE.Vector3(s*0.70,-0.55,0.00),new THREE.Vector3(s*0.95,-0.82,-0.02),new THREE.Vector3(s*1.10,-1.10,-0.04)];
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tPts),6,isHind?0.032:0.022,6,false), bmat(isHind?colors.yellow:colors.black,legMats)));
if (isHind) {
const pbGeo = new THREE.SphereGeometry(0.10,10,8);
const pb = new THREE.Mesh(pbGeo, bmat(colors.pollen,legMats));
pb.scale.set(0.6,1.1,0.55); pb.position.set(s*0.92,-0.80,-0.02);
group.add(pb);
}
let tStart = new THREE.Vector3(s*1.10,-1.10,-0.04);
for (let ti=0; ti<3; ti++) {
const tEnd = new THREE.Vector3(tStart.x+s*0.18, tStart.y-0.18+ti*0.04, tStart.z-0.02);
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tStart,tEnd]),4,0.016-ti*0.003,5,false), bmat(ti%2===0?colors.black:colors.yellow,legMats)));
tStart = tEnd;
}
return group;
}
function createBee() {
const group = new THREE.Group();
legGroups = [];
const abdData = [
{r:0.30,col:colors.yellow,z:0.12},{r:0.36,col:colors.black,z:0.42},
{r:0.38,col:colors.yellow,z:0.72},{r:0.33,col:colors.black,z:1.00},
{r:0.22,col:colors.yellow,z:1.26},{r:0.12,col:colors.black,z:1.48},
];
abdData.forEach((s,i) => {
const next = abdData[i+1];
const rTop = next ? next.r : s.r*0.5;
const geo = new THREE.CylinderGeometry(rTop*0.92,s.r,0.32,16,1);
const seg = new THREE.Mesh(geo, bmat(s.col, bodyMats));
seg.rotation.x = Math.PI*0.5; seg.position.set(0,0,s.z);
group.add(seg);
if (s.col===colors.yellow) addFuzz(group,0,0,s.z,s.r,18,colors.fuzz);
const ringGeo = new THREE.TorusGeometry(s.r+0.01,0.010,6,18);
const ring = new THREE.Mesh(ringGeo, bmat(colors.stripe,bodyMats));
ring.position.set(0,0,s.z); group.add(ring);
});
const stingPts=[new THREE.Vector3(0,0,1.62),new THREE.Vector3(0,-0.04,1.76),new THREE.Vector3(0,-0.06,1.88)];
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(stingPts),6,0.016,5,false),bmat(colors.stinger,bodyMats)));
const txGeo = new THREE.SphereGeometry(0.42,18,16);
const thorax = new THREE.Mesh(txGeo, bmat(colors.thorax,bodyMats));
thorax.scale.set(1.0,1.05,1.15); thorax.position.set(0,0.06,-0.28); group.add(thorax);
addFuzz(group,0,0.06,-0.28,0.44,60,colors.fuzz);
const headGeo = new THREE.SphereGeometry(0.26,16,14);
const head = new THREE.Mesh(headGeo, bmat(colors.black,bodyMats));
head.position.set(0,0.00,-0.78); group.add(head);
addFuzz(group,0,0.00,-0.78,0.27,22,colors.fuzz);
[-1,1].forEach(side => {
const eyeGeo = new THREE.SphereGeometry(0.14,14,12);
const eye = new THREE.Mesh(eyeGeo, bmat(colors.eye,eyeMats));
eye.scale.set(0.70,0.90,0.80); eye.position.set(side*0.21,0.04,-0.80); group.add(eye);
});
[-1,1].forEach(side => {
const scPts=[new THREE.Vector3(side*0.08,0.14,-0.90),new THREE.Vector3(side*0.14,0.28,-0.96),new THREE.Vector3(side*0.18,0.40,-0.98)];
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(scPts),6,0.016,5,false),bmat(colors.black,antennaMats)));
const flPts=[new THREE.Vector3(side*0.18,0.42,-0.98),new THREE.Vector3(side*0.32,0.46,-0.96),new THREE.Vector3(side*0.44,0.40,-0.90),new THREE.Vector3(side*0.52,0.32,-0.84)];
group.add(new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(flPts),8,0.011,5,false),bmat(colors.black,antennaMats)));
const knob = new THREE.Mesh(new THREE.SphereGeometry(0.022,6,6), bmat(colors.yellow,antennaMats));
knob.position.set(side*0.52,0.32,-0.84); group.add(knob);
});
wingFL = new THREE.Group(); wingFL.position.set(0,0.16,-0.30); wingFL.rotation.z=0.08; wingFL.rotation.x=-0.12; wingFL.add(makeWing(-1,true)); group.add(wingFL);
wingFR = new THREE.Group(); wingFR.position.set(0,0.16,-0.30); wingFR.rotation.z=-0.08; wingFR.rotation.x=-0.12; wingFR.add(makeWing(1,true)); group.add(wingFR);
wingHL = new THREE.Group(); wingHL.position.set(0,0.10,-0.10); wingHL.rotation.z=0.12; wingHL.rotation.x=-0.10; wingHL.add(makeWing(-1,false)); group.add(wingHL);
wingHR = new THREE.Group(); wingHR.position.set(0,0.10,-0.10); wingHR.rotation.z=-0.12; wingHR.rotation.x=-0.10; wingHR.add(makeWing(1,false)); group.add(wingHR);
[-1,1].forEach(side => {
for (let li=0; li<3; li++) {
const legGroup = makeLeg(side,li);
legGroup.position.set(side*0.38,-0.08,-0.45+li*0.30);
group.add(legGroup);
legGroups.push({g:legGroup,side,li});
}
});
group.position.set(0,0.2,0.2);
return group;
}
function initOrbitControls() {
const cvs = document.getElementById('animationCanvas');
function onDragStart(x,y) { orbit.isDragging=true; orbit.autoRotate=false; orbit.lastX=x; orbit.lastY=y; orbit.velTheta=0; orbit.velPhi=0; cvs.classList.add('dragging'); }
function onDragMove(x,y) {
if (!orbit.isDragging) return;
const dx=x-orbit.lastX, dy=y-orbit.lastY, speed=0.005;
orbit.velTheta=-dx*speed; orbit.velPhi=-dy*speed;
orbit.theta+=orbit.velTheta;
orbit.phi=Math.max(PHI_MIN,Math.min(PHI_MAX,orbit.phi+orbit.velPhi));
orbit.lastX=x; orbit.lastY=y; setCameraFromOrbit();
}
function onDragEnd() { orbit.isDragging=false; cvs.classList.remove('dragging'); setTimeout(()=>{orbit.autoRotate=true;},2000); }
cvs.addEventListener('mousedown',e=>onDragStart(e.clientX,e.clientY));
window.addEventListener('mousemove',e=>onDragMove(e.clientX,e.clientY));
window.addEventListener('mouseup',()=>onDragEnd());
cvs.addEventListener('touchstart',e=>{e.preventDefault();onDragStart(e.touches[0].clientX,e.touches[0].clientY);},{passive:false});
window.addEventListener('touchmove',e=>{e.preventDefault();onDragMove(e.touches[0].clientX,e.touches[0].clientY);},{passive:false});
window.addEventListener('touchend',()=>onDragEnd());
cvs.addEventListener('wheel',e=>{e.preventDefault();orbit.radius=Math.max(RADIUS_MIN,Math.min(RADIUS_MAX,orbit.radius+e.deltaY*0.01));setCameraFromOrbit();},{passive:false});
}
function init() {
initBackground();
updateSize();
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(52, window.innerWidth/(window.innerHeight-110), 0.1, 1000);
updateCamera();
bee = createBee();
scene.add(bee);
scene.add(new THREE.AmbientLight(0xffcc60,0.55));
const pl1 = new THREE.PointLight(0xffa020,1.8,100); pl1.position.set(4,5,3); scene.add(pl1);
const pl2 = new THREE.PointLight(0xffd060,0.9,100); pl2.position.set(-4,-2,2); scene.add(pl2);
initOrbitControls();
animate();
}
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
const audio = getAudio();
animateBackground();
// -- orbit --
if (!orbit.isDragging) {
if (orbit.autoRotate) {
orbit.theta += orbit.autoRotateSpeed * (1 + audio.energy * 3);
} else {
orbit.velTheta *= 0.92; orbit.velPhi *= 0.92;
orbit.theta += orbit.velTheta;
orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi+orbit.velPhi));
}
setCameraFromOrbit();
}
if (bee) {
// Body bob — bass drives big lurches
bee.position.y = 0.2 + Math.sin(t*2.2)*0.08 + audio.bass * 0.55;
bee.rotation.x = Math.sin(t*1.6)*0.04 + audio.bass * 0.12;
bee.rotation.z = Math.sin(t*2.0+0.8)*0.03;
// Body scale pulse on bass
const bScale = 1 + audio.bass * 0.30;
bee.scale.set(bScale, 1 + audio.bass * 0.18, bScale);
// Wings: buzz freq scales with energy, amplitude with mid
const buzzSpeed = 14.0 + audio.energy * 60;
const buzzAmp = 0.60 + audio.mid * 1.2;
const buzz = t * buzzSpeed;
if (wingFL) { wingFL.rotation.z = 0.08 + Math.sin(buzz) * buzzAmp; wingFL.rotation.x = -0.12 + Math.sin(buzz*0.97)*0.10; }
if (wingFR) { wingFR.rotation.z = -0.08 - Math.sin(buzz+0.05) * buzzAmp; wingFR.rotation.x = -0.12 + Math.sin(buzz*0.97)*0.10; }
if (wingHL) { wingHL.rotation.z = 0.12 + Math.sin(buzz+0.3) * buzzAmp*0.75; wingHL.rotation.x = -0.10 + Math.sin(buzz*0.95)*0.08; }
if (wingHR) { wingHR.rotation.z = -0.12 - Math.sin(buzz+0.35) * buzzAmp*0.75; wingHR.rotation.x = -0.10 + Math.sin(buzz*0.95)*0.08; }
// Legs: scuttle speed/amplitude driven by mid
legGroups.forEach(({g,side,li}) => {
const phase = li*0.9 + (side>0 ? 1.4 : 0);
const legSpeed = 2.0 + audio.mid * 12;
const legAmp = 0.05 + audio.mid * 0.4;
g.rotation.z = side*(0.05 + Math.sin(t*legSpeed+phase)*legAmp);
g.rotation.x = Math.sin(t*legSpeed*0.85+phase)*legAmp*0.8;
});
}
// ---- Color reactions ----
// Body: yellow → hot orange/white on bass
bodyMats.forEach(({mat, base}) => {
mat.color.setHex(lerpHex(base, 0xffffff, audio.bass * 1.4));
mat.opacity = 0.75 + audio.energy * 0.25;
});
// Wings: translucent → electric blue/white on high freq
wingMats.forEach(({mat, base}) => {
mat.color.setHex(lerpHex(base, 0xaaddff, audio.high * 2.5));
mat.opacity = 0.55 + audio.high * 0.45;
});
// Eyes: deep green → blazing red on bass
eyeMats.forEach(({mat}) => {
mat.color.setHex(lerpHex(0x203000, 0xff2200, audio.bass * 3));
});
// Legs: shift to hot yellow on mid
legMats.forEach(({mat, base}) => {
mat.color.setHex(lerpHex(base, 0xff8800, audio.mid * 2.5));
});
// Antennae: twitch color with highs
antennaMats.forEach(({mat}) => {
mat.color.setHex(lerpHex(0x1a1200, 0xffff88, audio.high * 3));
});
// Progress
if (isPlaying && audioCtx) {
const cur = Math.min(audioCtx.currentTime - startTime, duration);
progressFill.style.width = (duration > 0 ? (cur/duration)*100 : 0) + '%';
timeCur.textContent = fmt(Math.max(0, cur));
}
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
updateSize(); initBackground();
if (camera) {
camera.aspect = window.innerWidth/(window.innerHeight-110);
camera.updateProjectionMatrix();
updateCamera();
}
});
window.addEventListener('load', init);
})();
</script>
</body>
</html>