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();}