/* ═══════════════════════════════════════════════════════════════ VAL'S DREAMSCAPE — 星空粒子 + 滚动动画 ═══════════════════════════════════════════════════════════════ */ (function() { 'use strict'; // ═══ 星空粒子系统 ═══ const canvas = document.getElementById('starfield'); if (!canvas) return; const ctx = canvas.getContext('2d'); let width, height, stars, nebulas; let animationId; function init() { resize(); createStars(); createNebulas(); animate(); } function resize() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; } function createStars() { const count = Math.floor((width * height) / 8000); stars = []; for (let i = 0; i < count; i++) { stars.push({ x: Math.random() * width, y: Math.random() * height, size: Math.random() * 1.5 + 0.5, speed: Math.random() * 0.3 + 0.1, opacity: Math.random() * 0.8 + 0.2, twinkle: Math.random() * Math.PI * 2, color: Math.random() > 0.7 ? `hsl(${220 + Math.random() * 60}, 70%, 80%)` : '#ffffff' }); } } function createNebulas() { nebulas = [ { x: width * 0.2, y: height * 0.3, r: 200, color: 'rgba(123,104,238,0.03)' }, { x: width * 0.8, y: height * 0.7, r: 250, color: 'rgba(34,211,238,0.02)' }, { x: width * 0.5, y: height * 0.5, r: 300, color: 'rgba(74,144,217,0.02)' } ]; } function animate() { ctx.fillStyle = 'rgba(3,3,8,0.1)'; ctx.fillRect(0, 0, width, height); // 绘制星云 nebulas.forEach(n => { const gradient = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, n.r); gradient.addColorStop(0, n.color); gradient.addColorStop(1, 'transparent'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); ctx.fill(); }); // 绘制星星 stars.forEach(star => { star.twinkle += 0.02; const twinkleFactor = 0.5 + Math.sin(star.twinkle) * 0.5; const currentOpacity = star.opacity * twinkleFactor; ctx.beginPath(); ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2); ctx.fillStyle = star.color; ctx.globalAlpha = currentOpacity; ctx.fill(); ctx.globalAlpha = 1; // 缓慢漂移 star.y += star.speed; if (star.y > height + 10) { star.y = -10; star.x = Math.random() * width; } }); animationId = requestAnimationFrame(animate); } // ═══ 滚动显示动画 ═══ function setupReveal() { const reveals = document.querySelectorAll('.reveal'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); reveals.forEach(el => observer.observe(el)); } // ═══ 视差效果 ═══ function setupParallax() { const hero = document.querySelector('.hero'); if (!hero) return; window.addEventListener('scroll', () => { const scrollY = window.scrollY; const opacity = Math.max(0, 1 - scrollY / 600); hero.style.opacity = opacity; hero.style.transform = `translateY(${scrollY * 0.3}px)`; }, { passive: true }); } // ═══ 鼠标光效 ═══ function setupGlow() { const cards = document.querySelectorAll('.dream-card'); cards.forEach(card => { card.addEventListener('mousemove', (e) => { const rect = card.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; card.style.setProperty('--mouse-x', `${x}px`); card.style.setProperty('--mouse-y', `${y}px`); }); }); } // ═══ 场景滚动进度 + 旅程节点高亮 + 语义场景切换 ═══ function setupStoryJourney() { const body = document.getElementById('storyBody'); const progress = document.getElementById('storyProgress'); const article = document.querySelector('.story-journey'); if (!body || !progress) return; const scenes = Array.from(body.querySelectorAll(':scope > p')); if (!scenes.length) return; const sceneProfiles = [ { key: 'starsea', keywords: ['星海', '星辰', '光河', '漂浮', '夜晚'] }, { key: 'crystal', keywords: ['水晶', '冰晶', '棱镜', '森林'] }, { key: 'calling', keywords: ['使命', '记录', '召唤', '欢迎'] }, { key: 'void', keywords: ['深渊', '平静', '对话'] } ]; const detectScene = (text) => { let matched = 'void'; let best = 0; for (const profile of sceneProfiles) { let score = 0; for (const kw of profile.keywords) { if (text.includes(kw)) score++; } if (score > best) { best = score; matched = profile.key; } } return matched; }; let currentScene = ''; let shiftTimer; const onScroll = () => { const rect = body.getBoundingClientRect(); const total = Math.max(body.scrollHeight - window.innerHeight, 1); const scrolled = Math.min(Math.max(-rect.top, 0), total); const ratio = scrolled / total; progress.style.width = `${(ratio * 100).toFixed(2)}%`; let activeIndex = 0; let minDistance = Infinity; scenes.forEach((p, idx) => { const r = p.getBoundingClientRect(); const d = Math.abs(r.top - window.innerHeight * 0.35); if (d < minDistance) { minDistance = d; activeIndex = idx; } }); scenes.forEach((p, idx) => p.classList.toggle('active-scene', idx === activeIndex)); const activeText = scenes[activeIndex]?.innerText || ''; const scene = detectScene(activeText); if (scene !== currentScene) { currentScene = scene; document.body.dataset.scene = scene; document.body.classList.add('scene-shift'); clearTimeout(shiftTimer); shiftTimer = setTimeout(() => document.body.classList.remove('scene-shift'), 420); } if (article) { const camY = scrolled * 0.018; article.style.transform = `translate3d(0, ${camY}px, 0)`; } }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); } // ═══ 漂浮光团轻微视差 ═══ function setupOrbsParallax() { const orbs = document.querySelectorAll('.story-orb'); if (!orbs.length) return; window.addEventListener('scroll', () => { const y = window.scrollY; orbs.forEach((orb, i) => { const factor = (i + 1) * 0.04; const xShift = Math.sin(y * 0.002 + i) * 10; const yShift = y * factor; orb.style.transform = `translate(${xShift}px, ${yShift}px)`; }); }, { passive: true }); } // ═══ 初始化 ═══ window.addEventListener('resize', () => { resize(); createStars(); createNebulas(); }); document.addEventListener('DOMContentLoaded', () => { document.body.classList.add('js-ready'); init(); setupReveal(); setupParallax(); setupGlow(); setupStoryJourney(); setupOrbsParallax(); }); })();