mitch-xpt's picture
Show Three.js scene once loaded
7beff98
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>G1 Pull Over</title>
<style>
html,body{margin:0;width:100vw;height:100%;height:100dvh;overflow:hidden;background:#020006;overscroll-behavior:none;touch-action:manipulation}
body{position:fixed;inset:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#eaffff}
canvas{display:block;position:fixed;inset:0;width:100vw!important;height:100dvh!important;max-width:100vw;max-height:100dvh;opacity:0;transition:opacity .45s ease}
body.ready canvas{opacity:1}
#loader{position:fixed;inset:0;z-index:5;display:grid;place-items:center;padding:calc(18px + env(safe-area-inset-top)) 22px calc(24px + env(safe-area-inset-bottom));background:radial-gradient(circle at 50% 76%,rgba(0,230,255,.18),transparent 24%),radial-gradient(circle at 50% 20%,rgba(255,45,210,.20),transparent 34%),linear-gradient(180deg,#12001f,#020006 58%,#000);transition:opacity .45s ease,visibility .45s ease}
body.ready #loader{opacity:0;visibility:hidden;pointer-events:none}
.loader-card{width:min(86vw,430px);text-align:center;border:1px solid rgba(116,239,255,.28);border-radius:28px;padding:26px 20px 22px;background:rgba(0,0,0,.42);box-shadow:0 0 52px rgba(0,229,255,.15), inset 0 0 38px rgba(255,43,211,.08);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}
.loader-title{font-weight:900;font-size:clamp(34px,12vw,58px);line-height:.9;letter-spacing:.02em;text-shadow:0 0 22px rgba(255,44,207,.75),0 0 12px rgba(37,240,255,.55)}
.loader-sub{margin-top:12px;font-size:12px;letter-spacing:.16em;color:rgba(230,250,255,.74)}
#progressTrack{height:10px;border-radius:999px;overflow:hidden;margin:22px 4px 12px;background:rgba(255,255,255,.10);border:1px solid rgba(125,240,255,.24)}
#progressBar{height:100%;width:3%;border-radius:999px;background:linear-gradient(90deg,#24f0ff,#ff2ccf,#ffe66b);box-shadow:0 0 18px rgba(35,235,255,.7);transition:width .18s ease}
#loadStatus{font-size:12px;letter-spacing:.10em;color:rgba(230,250,255,.78);min-height:18px}
#tapHint{margin-top:18px;font:900 13px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;letter-spacing:.14em;color:#eaffff;background:rgba(0,0,0,.50);border:1px solid rgba(120,240,255,.50);border-radius:999px;padding:13px 18px;pointer-events:auto;user-select:none;box-shadow:0 0 32px rgba(35,220,255,.22);transition:transform .18s, background .2s}
#tapHint:active{transform:scale(.96);background:rgba(31,244,255,.14)}
#audioState{margin-top:10px;font-size:11px;letter-spacing:.08em;color:rgba(230,250,255,.58)}
.audio-on #tapHint{border-color:rgba(67,255,184,.72);box-shadow:0 0 36px rgba(67,255,184,.25)}
@media (orientation:landscape){.loader-card{width:min(72vw,520px);padding:18px 22px}.loader-title{font-size:clamp(28px,7vw,48px)}#progressTrack{margin-top:14px}}
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/"}}</script>
</head><body>
<div id="loader"><div class="loader-card">
<div class="loader-title">PULL<br>OVER</div>
<div class="loader-sub">G1 NIGHTCLUB STUDIO</div>
<div id="progressTrack"><div id="progressBar"></div></div>
<div id="loadStatus">STARTING 3D STUDIO</div>
<button id="tapHint" type="button">TAP TO START AUDIO</button>
<div id="audioState">Scene loads first. Tap anywhere for sound.</div>
</div></div>
<audio id="danceAudio" src="dance_audio.mp3" preload="auto" loop playsinline webkit-playsinline></audio>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
const tapHint = document.getElementById('tapHint');
const danceAudio = document.getElementById('danceAudio');
const progressBar = document.getElementById('progressBar');
const loadStatus = document.getElementById('loadStatus');
const audioState = document.getElementById('audioState');
let audioReady = false;
let audioStarted = false;
let loadPct = 3;
function setProgress(p, label){
loadPct = Math.max(loadPct, Math.min(100, p));
if(progressBar) progressBar.style.width = loadPct.toFixed(0) + '%';
if(label && loadStatus) loadStatus.textContent = label;
}
function markAudioReady(){ audioReady = true; if(audioState && !audioStarted) audioState.textContent = 'Audio loaded. Tap once for sound.'; }
async function startAudio(){
if(!danceAudio) return;
try{
danceAudio.muted = false;
danceAudio.volume = 1.0;
await danceAudio.play();
audioStarted = true;
document.body.classList.add('audio-on');
tapHint.textContent = 'SOUND ON';
if(audioState) audioState.textContent = 'Audio playing';
}catch(err){
tapHint.textContent = 'TAP AGAIN FOR SOUND';
if(audioState) audioState.textContent = 'Safari blocked audio. Tap this button once.';
}
}
tapHint.addEventListener('click', startAudio);
tapHint.addEventListener('touchend', (e)=>{ e.preventDefault(); startAudio(); }, {passive:false});
document.addEventListener('pointerdown', () => { if(!audioStarted) startAudio(); }, {once:false, passive:true});
danceAudio.addEventListener('canplay', markAudioReady);
danceAudio.addEventListener('canplaythrough', markAudioReady);
danceAudio.addEventListener('error', ()=>{ if(audioState) audioState.textContent='Audio failed to load. Reload once.'; });
setProgress(6, 'LOADING AUDIO + 3D ASSETS');
let viewW = 720, viewH = 1280;
const renderer = new THREE.WebGLRenderer({antialias:true, preserveDrawingBuffer:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(viewW, viewH, false);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.85;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05000b);
scene.fog = new THREE.Fog(0x07000d, 7.2, 15.5);
const camera = new THREE.PerspectiveCamera(37, viewW/viewH, 0.03, 100);
camera.position.set(0, 1.36, 5.05);
camera.lookAt(0, 1.10, 0);
function resizeToViewport(){
const w = Math.max(1, Math.round(window.innerWidth || document.documentElement.clientWidth || 720));
const h = Math.max(1, Math.round(window.innerHeight || document.documentElement.clientHeight || 1280));
if(w === viewW && h === viewH) return;
viewW = w; viewH = h;
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(viewW, viewH, false);
camera.aspect = viewW / viewH;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', resizeToViewport, {passive:true});
window.addEventListener('orientationchange', () => setTimeout(resizeToViewport, 250), {passive:true});
resizeToViewport();
// Full-height neon backdrop so the vertical mobile frame never reads as letterboxed black bars.
function gradientTexture(){
const c=document.createElement('canvas'); c.width=16; c.height=1024; const x=c.getContext('2d');
const g=x.createLinearGradient(0,0,0,1024);
g.addColorStop(0,'#16002c'); g.addColorStop(.22,'#060018'); g.addColorStop(.50,'#020008'); g.addColorStop(.78,'#110020'); g.addColorStop(1,'#280034');
x.fillStyle=g; x.fillRect(0,0,16,1024);
const tex=new THREE.CanvasTexture(c); tex.colorSpace=THREE.SRGBColorSpace; return tex;
}
const bgGlow = new THREE.Mesh(new THREE.PlaneGeometry(18,12), new THREE.MeshBasicMaterial({map:gradientTexture(), fog:false, depthWrite:false, transparent:true, opacity:.95}));
bgGlow.position.set(0,2.7,-9.6); scene.add(bgGlow);
scene.add(new THREE.HemisphereLight(0x8ab8ff, 0x250031, .92));
const amb = new THREE.AmbientLight(0xffffff, .58); scene.add(amb);
const key = new THREE.DirectionalLight(0xffffff, 2.75); key.position.set(2.3,4.2,3.2); key.castShadow=true; key.shadow.mapSize.set(1024,1024); scene.add(key);
const robotFill = new THREE.PointLight(0xffffff, 1.65, 8); robotFill.position.set(0,1.8,3.1); scene.add(robotFill);
const rimCyan = new THREE.DirectionalLight(0x21e8ff, 1.55); rimCyan.position.set(-3.2,2.2,2.5); scene.add(rimCyan);
const rimMag = new THREE.DirectionalLight(0xff31cc, .85); rimMag.position.set(3.4,2.1,-2.3); scene.add(rimMag);
// Reference-style purple dance floor with perspective grid and rings.
const floorMat = new THREE.MeshBasicMaterial({color:0x25002f, transparent:true, opacity:.96});
const floor = new THREE.Mesh(new THREE.PlaneGeometry(18,18,1,1), floorMat);
floor.rotation.x=-Math.PI/2; floor.position.y=-.02; scene.add(floor);
const grid = new THREE.GridHelper(18,34,0x7a2ca5,0x283c7f); grid.position.y=.003; grid.material.transparent=true; grid.material.opacity=.52; scene.add(grid);
const tileGroup = new THREE.Group(); scene.add(tileGroup);
const tileMats=[
new THREE.MeshBasicMaterial({color:0x2b0042,transparent:true,opacity:.34,blending:THREE.AdditiveBlending,depthWrite:false}),
new THREE.MeshBasicMaterial({color:0x001a46,transparent:true,opacity:.25,blending:THREE.AdditiveBlending,depthWrite:false}),
new THREE.MeshBasicMaterial({color:0x3a002a,transparent:true,opacity:.24,blending:THREE.AdditiveBlending,depthWrite:false})
];
const tiles=[];
for(let ix=-8;ix<8;ix++) for(let iz=-7;iz<5;iz++){
const tile=new THREE.Mesh(new THREE.PlaneGeometry(.94,.94), tileMats[Math.abs(ix+iz)%tileMats.length]);
tile.rotation.x=-Math.PI/2; tile.position.set(ix+.5,.010,iz+.5); tile.userData.phase=(ix*11+iz*7); tileGroup.add(tile); tiles.push(tile);
}
function floorTextTexture(){
const c=document.createElement('canvas'); c.width=1024; c.height=256; const x=c.getContext('2d');
x.clearRect(0,0,c.width,c.height); x.textAlign='center'; x.textBaseline='middle';
x.font='700 62px Arial, sans-serif'; x.lineWidth=5; x.strokeStyle='rgba(5,0,20,.95)'; x.strokeText('@mitchbookpro',512,128);
x.shadowColor='rgba(90,245,255,.95)'; x.shadowBlur=16; x.fillStyle='rgba(225,250,255,.88)'; x.fillText('@mitchbookpro',512,128);
const tex=new THREE.CanvasTexture(c); tex.colorSpace=THREE.SRGBColorSpace; return tex;
}
const floorTag = new THREE.Mesh(new THREE.PlaneGeometry(1.65,.34), new THREE.MeshBasicMaterial({map:floorTextTexture(),transparent:true,opacity:.82,fog:false,blending:THREE.AdditiveBlending,depthWrite:false,depthTest:false,side:THREE.DoubleSide}));
floorTag.rotation.x=-Math.PI/2; floorTag.position.set(0,.085,1.28); scene.add(floorTag);
const rings=[];
for(let r=1.25;r<9;r+=1.25){
const ring=new THREE.Mesh(new THREE.TorusGeometry(r,.014,8,220),new THREE.MeshBasicMaterial({color:0x8c64bd,transparent:true,opacity:.33,blending:THREE.AdditiveBlending}));
ring.rotation.x=Math.PI/2; ring.position.y=.018; scene.add(ring); rings.push(ring);
}
// Star/speckle field on black background.
const pgeom=new THREE.BufferGeometry(), count=850, parr=new Float32Array(count*3);
for(let i=0;i<count;i++){parr[i*3]=(Math.random()-.5)*13.5;parr[i*3+1]=Math.random()*5.1+.28;parr[i*3+2]=-3.3-Math.random()*6.0;}
pgeom.setAttribute('position', new THREE.BufferAttribute(parr,3));
const particles=new THREE.Points(pgeom,new THREE.PointsMaterial({color:0xd8ecff,size:.012,transparent:true,opacity:.48,blending:THREE.AdditiveBlending,depthWrite:false})); scene.add(particles);
function textTexture(){
const c=document.createElement('canvas'); c.width=1536; c.height=1536; const x=c.getContext('2d');
x.clearRect(0,0,c.width,c.height); x.textAlign='center'; x.textBaseline='middle';
x.font='900 430px Arial Black, Impact, sans-serif';
function drawLine(txt,y){
x.lineWidth=36; x.strokeStyle='rgba(18,0,28,.96)'; x.strokeText(txt,768,y);
x.shadowColor='rgba(255,42,210,.95)'; x.shadowBlur=42; x.fillStyle='#ff2ccf'; x.fillText(txt,768,y);
x.shadowColor='rgba(37,240,255,.9)'; x.shadowBlur=20; x.strokeStyle='rgba(37,240,255,.70)'; x.lineWidth=10; x.strokeText(txt,768,y);
x.shadowBlur=0;
}
drawLine('PULL',560);
drawLine('OVER',1010);
const tex=new THREE.CanvasTexture(c); tex.colorSpace=THREE.SRGBColorSpace; tex.anisotropy=8; return tex;
}
const pullMat = new THREE.MeshBasicMaterial({map:textTexture(), transparent:true, opacity:.9, depthWrite:false, fog:false, blending:THREE.AdditiveBlending});
const pullText = new THREE.Mesh(new THREE.PlaneGeometry(3.45,3.45), pullMat);
pullText.position.set(-0.05,2.82,-8.72); scene.add(pullText); // Behind equalizer bars, nudged up/back for more robot clearance.
// Disco ball behind the PULL OVER text, like the reference.
const disco = new THREE.Group();
const core = new THREE.Mesh(new THREE.SphereGeometry(.58,36,18), new THREE.MeshStandardMaterial({color:0x0b1838,metalness:.88,roughness:.14,emissive:0x132555,emissiveIntensity:.78,fog:false})); disco.add(core);
for(let i=0;i<150;i++){
const shard=new THREE.Mesh(new THREE.PlaneGeometry(.085,.085),new THREE.MeshBasicMaterial({color:i%3===0?0xf4fbff:i%3===1?0xf3b8ee:0xbdf4ff,side:THREE.DoubleSide,fog:false}));
const phi=Math.acos(2*Math.random()-1),th=Math.random()*Math.PI*2;
shard.position.set(.595*Math.sin(phi)*Math.cos(th),.595*Math.cos(phi),.595*Math.sin(phi)*Math.sin(th)); shard.lookAt(0,0,0); disco.add(shard);
}
disco.position.set(0,4.55,-8.9); scene.add(disco);
// Cooler multilayer equalizer: neon glass columns plus top caps and glow lanes.
const barPalette=[0x00e5ff,0x42ffb8,0xff42e6,0xffbf2f,0x7c5cff,0xff4c9c,0xd6ff4a,0x36a3ff];
const bars=[];
const barData=[];
for(let i=0;i<20;i++){
const x=-5.55+i*(11.10/19);
const h=.62+((i*37)%11)/10*.95+(i%5===0?.35:0);
barData.push([x,h,i%barPalette.length]);
}
barData.forEach((d,i)=>{
const [x,h,ci]=d;
const mat=new THREE.MeshBasicMaterial({color:barPalette[ci],transparent:true,opacity:.78,fog:false,blending:THREE.AdditiveBlending});
const bar=new THREE.Mesh(new THREE.BoxGeometry(.22,h,.08),mat); bar.position.set(x,h/2+.20,-7.85-Math.abs(x)*.035); bar.userData.base=h; bar.userData.phase=i*.58; scene.add(bar); bars.push(bar);
const cap=new THREE.Mesh(new THREE.BoxGeometry(.18,.025,.09),new THREE.MeshBasicMaterial({color:barPalette[ci],transparent:true,opacity:.30,fog:false,blending:THREE.AdditiveBlending}));
cap.position.set(x,h+.24,bar.position.z-.01); cap.userData.parentBar=bar; scene.add(cap); bars.push(cap);
});
// Moving stage lights and visible beams, kept behind/around the robot.
const spots=[]; const beams=[];
function beam(color,x,z,rotZ){
const m=new THREE.MeshBasicMaterial({color,transparent:true,opacity:.19,fog:false,blending:THREE.AdditiveBlending,depthWrite:false,side:THREE.DoubleSide});
const b=new THREE.Mesh(new THREE.ConeGeometry(.34,7.0,4,1,true),m);
b.position.set(x,2.25,z); b.rotation.set(Math.PI/2,.18,rotZ); scene.add(b); beams.push(b); return b;
}
function spot(color,x,z,rz){const s=new THREE.SpotLight(color,8.5,13,Math.PI/8,.55,1.0);s.position.set(x,4.25,z);s.target.position.set(0,1,0);scene.add(s);scene.add(s.target);spots.push(s);beam(color,x,z,rz);}
spot(0xff2ed2,-3.8,2.4,-.42); spot(0x5ff4ff,3.8,2.2,.42); spot(0xecc466,-3.1,-3.9,.36); spot(0x7c5cff,3.1,-3.9,-.36);
const lasers=[];
for(let i=0;i<7;i++){
const l=new THREE.Mesh(new THREE.PlaneGeometry(7.0,.018),new THREE.MeshBasicMaterial({color:i%2?0x35e8ff:0xff38df,transparent:true,opacity:.36,fog:false,blending:THREE.AdditiveBlending,depthWrite:false,side:THREE.DoubleSide}));
l.position.set(0,3.05+i*.24,-8.2); l.rotation.z=(-.22+i*.075); scene.add(l); lasers.push(l);
}
const g1 = new THREE.Group(); g1.scale.setScalar(1.13); g1.position.set(0,-.015,.35); scene.add(g1);
const bodyObjects = new Map(); let MOTION;
function cleanMatFor(name, src){
const isHand=name.includes('rubber_hand');
const black = isHand || name.includes('pelvis') || name.includes('waist') || name.includes('ankle_roll') || name.includes('wrist') || name.includes('hip_pitch');
const joint = name.includes('knee') || name.includes('elbow') || name.includes('shoulder') || name.includes('hip') || name.includes('ankle_pitch');
const head = name.includes('head');
const torso = name.includes('torso') || name.includes('logo');
// User correction: do NOT let the robot read as plain white. Use darker G1-style silver/graphite panels.
let color = black ? 0x151b24 : (joint ? 0x4a5561 : (head ? 0x5c6670 : (torso ? 0x68737f : 0x737f8b)));
let emissive = black ? 0x080b12 : 0x111720;
// Clean direct mesh materials: no chromakey, no missing white panels, no fake black holes.
return new THREE.MeshBasicMaterial({
color,
side:THREE.DoubleSide,
fog:false
});
}
const loadingManager = new THREE.LoadingManager();
loadingManager.onStart = () => setProgress(18, 'LOADING ROBOT MESHES');
loadingManager.onProgress = (url, loaded, total) => {
const pct = total ? 18 + (loaded / total) * 72 : Math.min(88, loadPct + 1.5);
const name = (url || '').split('/').pop() || 'asset';
setProgress(pct, `LOADING ${loaded}/${total || '?'} ${name.toUpperCase()}`);
};
loadingManager.onLoad = () => setProgress(94, 'ASSEMBLING NIGHTCLUB STUDIO');
function loadGltfMesh(item){
return new Promise((resolve,reject)=>{
const loader = new GLTFLoader(loadingManager);
loader.load('g1_three_assets/' + item.file, gltf=>{
const obj = gltf.scene;
const s=item.scale||[1,1,1]; obj.scale.set(s[0],s[1],s[2]);
obj.traverse(c=>{ if(c.isMesh){
c.castShadow=true; c.receiveShadow=true; c.frustumCulled=false;
c.material = cleanMatFor(item.body || '', c.material);
}});
resolve(obj);
}, undefined, reject);
});
}
async function loadAssets(){
setProgress(10, 'LOADING MOTION DATA');
MOTION = await (await fetch('g1_three_assets/g1_motion_poses.json')).json();
setProgress(15, 'LOADING ROBOT MANIFEST');
const manifest = await (await fetch('g1_three_assets/mesh_manifest.json')).json();
await Promise.all(manifest.map(async item=>{
const obj = await loadGltfMesh(item);
g1.add(obj); bodyObjects.set(item.body,obj);
}));
setProgress(100, audioStarted ? 'LAUNCHING STUDIO' : 'READY. TAP FOR SOUND');
window.__ready = true;
}
const q0=new THREE.Quaternion(), q1=new THREE.Quaternion();
function applyMotion(t){
if(!MOTION) return;
const exact = (t*MOTION.fps)%MOTION.numFrames, i0=Math.floor(exact), i1=(i0+1)%MOTION.numFrames, f=exact-i0;
const P0=MOTION.positions[i0], P1=MOTION.positions[i1], Q0=MOTION.quaternions[i0], Q1=MOTION.quaternions[i1];
for(let i=0;i<MOTION.bodyNames.length;i++){
const obj=bodyObjects.get(MOTION.bodyNames[i]); if(!obj) continue;
const p0=P0[i], p1=P1[i];
obj.position.set(p0[0]+(p1[0]-p0[0])*f, p0[1]+(p1[1]-p0[1])*f, p0[2]+(p1[2]-p0[2])*f);
q0.set(Q0[i][0],Q0[i][1],Q0[i][2],Q0[i][3]); q1.set(Q1[i][0],Q1[i][1],Q1[i][2],Q1[i][3]); obj.quaternion.copy(q0.slerp(q1,f));
}
}
window.__renderStill = function(t=0, beat=.3, high=.2){
g1.visible = !window.__hideRobot;
applyMotion(t);
const flash = .45 + .55*Math.abs(Math.sin(t*4.8));
pullMat.opacity = .58 + flash*.42;
pullText.scale.setScalar(1 + .025*Math.sin(t*9.6));
disco.rotation.y=t*.95; disco.rotation.x=Math.sin(t*.45)*.08;
bgGlow.material.opacity = .86 + beat*.10;
particles.rotation.y=t*.035; particles.material.opacity=.30+high*.38;
grid.material.opacity=.38+beat*.26; floor.material.opacity=.91+beat*.07;
tiles.forEach((tile,i)=>{tile.material.opacity=(.15+((i%3)*.035))+beat*.22+Math.max(0,Math.sin(t*2.2+tile.userData.phase)*.08);});
floorTag.material.opacity=.46+beat*.20;
rings.forEach((r,i)=>{r.scale.setScalar(1+Math.sin(t*1.15+i)*.015+beat*.035);r.material.opacity=.21+beat*.22;});
bars.forEach((bar,i)=>{
if(bar.userData.parentBar){
const p=bar.userData.parentBar; bar.position.y=p.position.y+(p.userData.base*p.scale.y)/2+.07; bar.material.opacity=.45+flash*.18+beat*.20; return;
}
const pulse=.48+Math.abs(Math.sin(t*3.2+bar.userData.phase))*0.95+beat*.65+high*.25;
bar.scale.y=Math.max(.20,pulse); bar.position.y=.20+(bar.userData.base*bar.scale.y)/2; bar.material.opacity=.68+flash*.18+beat*.16;
});
spots.forEach((s,i)=>{const a=t*(.72+i*.16)+i*1.7;s.intensity=5.5+beat*12+high*7;s.target.position.set(Math.sin(a)*2.35,.88+Math.cos(a*1.7)*.55,Math.cos(a)*2.05);});
beams.forEach((b,i)=>{b.rotation.z=(i%2?1:-1)*(.22+.16*Math.sin(t*.75+i)); b.material.opacity=.10+beat*.22+high*.08;});
lasers.forEach((l,i)=>{l.rotation.z=(-.24+i*.075)+Math.sin(t*.95+i)*.045; l.material.opacity=.16+beat*.28;});
amb.intensity=.85+beat*.15; key.intensity=3.8+beat*.55; robotFill.intensity=4.2+beat*.55; rimCyan.intensity=1.55+high*.45; rimMag.intensity=.95+beat*.32;
const camX=Math.sin(t*.55)*.115 + Math.sin(t*1.7)*.026;
const camY=1.36+Math.sin(t*.32)*.030+beat*.018;
const camZ=5.08+Math.sin(t*.42+1.1)*.18;
camera.position.set(camX,camY,camZ);
camera.lookAt(Math.sin(t*.50)*.10,1.08+Math.sin(t*.37)*.035,0.02);
renderer.render(scene,camera);
};
function animate(now=performance.now()){
window.__frameCount = (window.__frameCount || 0) + 1;
resizeToViewport();
const t = (audioStarted && danceAudio && !danceAudio.paused) ? danceAudio.currentTime : now * 0.001;
const beat = 0.5 + 0.5 * Math.sin(t * 3.15);
const high = 0.5 + 0.5 * Math.sin(t * 8.2 + 0.9);
window.__audioStarted = audioStarted;
window.__renderStill(t, beat, high);
}
function startLoop(){
if(window.__loopStarted) return;
window.__loopStarted = true;
// setInterval keeps the shared link visibly live in browser in-app views that throttle requestAnimationFrame.
animate();
setInterval(()=>animate(performance.now()), 1000/30);
}
loadAssets().then(()=>{
document.body.classList.add('ready');
renderer.domElement.style.opacity = '1';
if(!audioStarted) tapHint.textContent = 'TAP TO START AUDIO';
startLoop();
});
</script></body></html>