const x=(v.x+1)*rect.width/2,y=(-v.y+1)*rect.height/2;
el.style.left=x+’px’;el.style.top=y+’px’;
el.classList.toggle(‘on-screen’,v.z<1 && x>35 && xrect.height*.26 && y.8&&!canInteract){canInteract=true;chips.classList.add(‘visible’);bottom.classList.add(‘visible’);}
if(progress===1)entering=false;
}
if(introDust){
const dustPositions=introDust.geometry.attributes.position.array;
const drift=introDust.userData.drift;
const dustSpeed=entering?(reducedMotion?0:2.15):(reducedMotion?0:.32);
for(let i=0;i-26?1.7:1;
dustPositions[i]+=drift[di]*delta*14*dustSpeed*closeBoost;
dustPositions[i+1]+=drift[di+1]*delta*9*dustSpeed*closeBoost;
dustPositions[i+2]+=delta*39*dustSpeed*closeBoost;
drift[di]*=.9965;
if(dustPositions[i+2]>camera.position.z+1.5 || Math.abs(dustPositions[i])>31 || Math.abs(dustPositions[i+1])>12.5){
resetIntroDustParticle(dustPositions,drift,i);
}
}
introDust.geometry.attributes.position.needsUpdate=true;
const introProgress=clamp((performance.now()-beginAt)/(reducedMotion?1100:5200),0,1);
introDust.material.opacity=entering?(reducedMotion?0:(.96 – introProgress*.34)):.14;
}
if(universe){
currentX+=(targetX-currentX)*.055;currentY+=(targetY-currentY)*.055;
universe.rotation.y=currentY+(reducedMotion?0:Math.sin(elapsed*.15)*.045);
universe.rotation.x=currentX;
if(!reducedMotion){
universe.userData.disk.rotation.y=elapsed*.036;
universe.userData.disk.rotation.z=.035+Math.sin(elapsed*.34)*.045;
universe.userData.secondaryDisk.rotation.y=-elapsed*.054;
universe.userData.secondaryDisk.rotation.x=Math.sin(elapsed*.28)*.08;
universe.userData.stars.rotation.y=elapsed*.016;
universe.userData.stars.rotation.x=Math.sin(elapsed*.18)*.06;
universe.userData.stars.rotation.z=Math.cos(elapsed*.14)*.04;
universe.userData.stars.position.x=Math.sin(elapsed*.1)*.75;
universe.userData.stars.position.y=Math.cos(elapsed*.12)*.38;
universe.userData.heart.material.opacity=.82+.15*Math.sin(elapsed*1.6);
}
universe.updateMatrixWorld(true);placeChips();
}
renderer.render(scene,camera);
}
function openUniverse(){
if(started)return;started=true;entering=true;beginAt=performance.now();
opening.classList.add(‘hidden’);toolbar.classList.add(‘visible’);top.classList.add(‘visible’);
playRomanticSong(true);
setTimeout(()=>showToast(‘Toca una flor: algunas guardan un mensaje para ti. 🌻’),reducedMotion?1200:5000);
}
function openLetter(){
$(‘letterWrap’).classList.add(‘visible’);$(‘letterWrap’).setAttribute(‘aria-hidden’,’false’);
$(‘closeLetter’).focus();
}
function closeLetter(){
$(‘letterWrap’).classList.remove(‘visible’);$(‘letterWrap’).setAttribute(‘aria-hidden’,’true’);$(‘openLetter’).focus();
}
function updateMusicButton(isPlaying){
$(‘music’).textContent=isPlaying?’♪’:’♫’;
$(‘music’).setAttribute(‘aria-label’,isPlaying?’Pausar música’:’Reproducir música’);
$(‘music’).title=isPlaying?’Pausar música’:’Reproducir música’;
}
function playRomanticSong(showMissingToast=false){
if(!romanticSong) return;
romanticSong.volume=.78;
const promise=romanticSong.play();
if(promise&&promise.catch){
promise.then(()=>updateMusicButton(true)).catch(()=>{
updateMusicButton(false);
if(showMissingToast) showToast(‘Para usar «Photograph», coloca un archivo llamado photograph.mp3 en esta misma carpeta. ❤️’);
});
} else {
updateMusicButton(!romanticSong.paused);
}
}
function toggleMusic(){
if(!romanticSong || !romanticSong.getAttribute(‘src’)){ showToast(‘Agrega el archivo photograph.mp3 para usar la canción.’); return; }
if(romanticSong.paused) playRomanticSong(true);
else { romanticSong.pause(); updateMusicButton(false); }
}
$(‘start’).addEventListener(‘click’,openUniverse);
$(‘openLetter’).addEventListener(‘click’,openLetter);
$(‘closeLetter’).addEventListener(‘click’,closeLetter);
$(‘letterWrap’).addEventListener(‘click’,e=>{if(e.target===$(‘letterWrap’))closeLetter();});
window.addEventListener(‘keydown’,e=>{if(e.key===’Escape’&&$(‘letterWrap’).classList.contains(‘visible’))closeLetter();});
updateMusicButton(false);
$(‘music’).addEventListener(‘click’,toggleMusic);
$(‘reset’).addEventListener(‘click’,()=>{targetX=0;targetY=0;showToast(‘De vuelta al corazón de tu universo. ♡’);});
[…chips.children].forEach(el=>el.addEventListener(‘click’,()=>showToast(mensajes[Number(el.dataset.index)])));
if(!init3D()){
$(‘start’).textContent=’Abrir mi carta para ti 💌’;
$(‘start’).addEventListener(‘click’,openLetter);
$(‘fallback’).setAttribute(‘aria-hidden’,’false’);
showToast(‘La vista 3D no está disponible, pero tu carta sí. 💛’);
}else{resize();}