const pointerGlow = document.getElementById('pointerGlow'); const themeToggle = document.getElementById('themeToggle'); const themeLabel = document.getElementById('themeLabel'); const sceneCards = document.querySelectorAll('.scene-card'); const counters = document.querySelectorAll('[data-counter]'); const sceneDetailTitle = document.getElementById('sceneDetailTitle'); const sceneDetailCopy = document.getElementById('sceneDetailCopy'); const particleCanvas = document.getElementById('particleCanvas'); const themes = [ { className: '', label: 'Aurora' }, { className: 'theme-ember', label: 'Ember' }, { className: 'theme-tide', label: 'Tide' }, ]; let themeIndex = 0; function setTheme(index) { document.body.classList.remove('theme-ember', 'theme-tide'); const theme = themes[index]; if (theme.className) { document.body.classList.add(theme.className); } themeLabel.textContent = theme.label; } function animateCounter(element) { const target = Number(element.dataset.counter || 0); const duration = 1400; const start = performance.now(); function step(now) { const progress = Math.min((now - start) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); element.textContent = Math.round(target * eased); if (progress < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } function mountParticleField() { if (!particleCanvas) return; const ctx = particleCanvas.getContext('2d'); if (!ctx) return; const particles = []; const particleCount = 48; let width = 0; let height = 0; function resize() { width = window.innerWidth; height = window.innerHeight; particleCanvas.width = width * window.devicePixelRatio; particleCanvas.height = height * window.devicePixelRatio; particleCanvas.style.width = `${width}px`; particleCanvas.style.height = `${height}px`; ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0); } function seed() { particles.length = 0; for (let i = 0; i < particleCount; i += 1) { particles.push({ x: Math.random() * width, y: Math.random() * height, r: Math.random() * 1.8 + 0.8, vx: (Math.random() - 0.5) * 0.28, vy: (Math.random() - 0.5) * 0.28, }); } } function draw() { ctx.clearRect(0, 0, width, height); particles.forEach((particle, index) => { particle.x += particle.vx; particle.y += particle.vy; if (particle.x < -20) particle.x = width + 20; if (particle.x > width + 20) particle.x = -20; if (particle.y < -20) particle.y = height + 20; if (particle.y > height + 20) particle.y = -20; ctx.beginPath(); ctx.fillStyle = 'rgba(255,255,255,0.38)'; ctx.arc(particle.x, particle.y, particle.r, 0, Math.PI * 2); ctx.fill(); for (let j = index + 1; j < particles.length; j += 1) { const other = particles[j]; const dx = particle.x - other.x; const dy = particle.y - other.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 130) { ctx.beginPath(); ctx.strokeStyle = `rgba(130, 180, 255, ${0.12 * (1 - distance / 130)})`; ctx.moveTo(particle.x, particle.y); ctx.lineTo(other.x, other.y); ctx.stroke(); } } }); requestAnimationFrame(draw); } resize(); seed(); draw(); window.addEventListener('resize', () => { resize(); seed(); }); } window.addEventListener('pointermove', (event) => { pointerGlow.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`; }); sceneCards.forEach((card) => { card.addEventListener('pointermove', (event) => { const rect = card.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const rotateX = ((y / rect.height) - 0.5) * -8; const rotateY = ((x / rect.width) - 0.5) * 10; card.style.setProperty('--mx', `${(x / rect.width) * 100}%`); card.style.setProperty('--my', `${(y / rect.height) * 100}%`); card.style.transform = `translateY(-6px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; }); card.addEventListener('pointerleave', () => { card.style.transform = ''; }); card.addEventListener('click', () => { sceneCards.forEach((item) => item.classList.remove('is-active')); card.classList.add('is-active'); sceneDetailTitle.textContent = card.dataset.sceneTitle || ''; sceneDetailCopy.textContent = card.dataset.sceneCopy || ''; }); }); themeToggle.addEventListener('click', () => { themeIndex = (themeIndex + 1) % themes.length; setTheme(themeIndex); }); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { animateCounter(entry.target); observer.unobserve(entry.target); } }); }, { threshold: 0.4 }, ); counters.forEach((counter) => observer.observe(counter)); sceneCards[0]?.classList.add('is-active'); setTheme(themeIndex); mountParticleField();