Files
val-blog/themes/dreamscape/assets/js/dreamscape.js
Chen Gu 82a2d231c3
All checks were successful
Deploy / deploy (push) Successful in 2s
feat: story-journey immersive layer (progress, scene focus, floating orbs)
2026-04-24 09:31:21 +08:00

204 lines
6.9 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════════
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');
if (!body || !progress) return;
const scenes = Array.from(body.querySelectorAll(':scope > p'));
if (!scenes.length) return;
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));
};
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();
});
})();