Files
NebulaShell/website/public/js/animations.js
2026-04-25 15:48:07 +08:00

465 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 动画和过渡效果管理
*/
/**
* 初始化所有动画
*/
function initAnimations() {
console.log('初始化动画系统...');
// 初始化滚动动画
initScrollAnimations();
// 初始化视差效果
initParallaxEffects();
// 初始化计数器动画
initCounterAnimations();
// 初始化进度条动画
initProgressBars();
// 初始化打字机效果
initTypewriterEffects();
// 初始化骨架屏
initSkeletonScreens();
console.log('动画系统初始化完成');
}
/**
* 初始化滚动动画
*/
function initScrollAnimations() {
const animatedElements = document.querySelectorAll('.animate-on-scroll');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animated');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
animatedElements.forEach(element => observer.observe(element));
} else {
// 回退方案:直接添加动画类
animatedElements.forEach(element => {
element.classList.add('animated');
});
}
}
/**
* 初始化视差效果
*/
function initParallaxEffects() {
const parallaxElements = document.querySelectorAll('.parallax');
if (parallaxElements.length === 0) return;
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
parallaxElements.forEach(element => {
const speed = parseFloat(element.dataset.speed) || 0.5;
const yPos = -(scrolled * speed);
element.style.transform = `translateY(${yPos}px)`;
});
});
}
/**
* 初始化计数器动画
*/
function initCounterAnimations() {
const counters = document.querySelectorAll('.counter');
if (counters.length === 0) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const counter = entry.target;
const target = parseInt(counter.dataset.target) || 0;
const duration = parseInt(counter.dataset.duration) || 2000;
const increment = target / (duration / 16); // 60fps
let current = 0;
const updateCounter = () => {
current += increment;
if (current < target) {
counter.textContent = Math.floor(current).toLocaleString();
requestAnimationFrame(updateCounter);
} else {
counter.textContent = target.toLocaleString();
}
};
updateCounter();
observer.unobserve(counter);
}
});
});
counters.forEach(counter => observer.observe(counter));
}
/**
* 初始化进度条动画
*/
function initProgressBars() {
const progressBars = document.querySelectorAll('.progress-bar');
if (progressBars.length === 0) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const progressBar = entry.target;
const width = progressBar.dataset.width || '100%';
const duration = parseInt(progressBar.dataset.duration) || 1000;
progressBar.style.transition = `width ${duration}ms ease`;
progressBar.style.width = width;
observer.unobserve(progressBar);
}
});
});
progressBars.forEach(bar => observer.observe(bar));
}
/**
* 初始化打字机效果
*/
function initTypewriterEffects() {
const typewriters = document.querySelectorAll('.typewriter');
typewriters.forEach(element => {
const text = element.textContent;
element.textContent = '';
element.style.width = '0';
let i = 0;
const type = () => {
if (i < text.length) {
element.textContent += text.charAt(i);
element.style.width = `${(i + 1) / text.length * 100}%`;
i++;
setTimeout(type, 100);
}
};
// 延迟开始打字效果
setTimeout(type, 500);
});
}
/**
* 初始化骨架屏
*/
function initSkeletonScreens() {
const skeletonElements = document.querySelectorAll('.skeleton');
if (skeletonElements.length === 0) return;
// 模拟内容加载
setTimeout(() => {
skeletonElements.forEach(element => {
element.classList.remove('skeleton');
element.classList.add('fade-in-load');
});
}, 1000);
}
/**
* 创建骨架屏占位符
* @param {HTMLElement} container - 容器元素
* @param {number} count - 骨架屏数量
* @param {string} type - 骨架屏类型: 'text', 'card', 'image'
*/
function createSkeletonPlaceholders(container, count = 3, type = 'card') {
const skeletonClasses = {
'text': 'skeleton skeleton-text',
'card': 'skeleton skeleton-card',
'image': 'skeleton skeleton-image'
};
const skeletonClass = skeletonClasses[type] || skeletonClasses.card;
for (let i = 0; i < count; i++) {
const skeleton = document.createElement('div');
skeleton.className = skeletonClass;
container.appendChild(skeleton);
}
}
/**
* 显示页面切换动画
* @param {string} direction - 切换方向: 'left', 'right', 'up', 'down'
*/
function showPageTransition(direction = 'right') {
const transition = document.getElementById('page-transition');
if (!transition) return;
const directions = {
'left': 'slideInLeft',
'right': 'slideInRight',
'up': 'slideInUp',
'down': 'slideInDown'
};
const animation = directions[direction] || 'slideInRight';
transition.className = `page-transition animate-${animation}`;
transition.classList.add('active');
return new Promise(resolve => {
setTimeout(() => {
transition.classList.remove('active');
setTimeout(resolve, 300);
}, 300);
});
}
/**
* 显示元素动画
* @param {HTMLElement} element - 要动画的元素
* @param {string} animation - 动画名称
* @param {number} duration - 动画持续时间(毫秒)
*/
function animateElement(element, animation = 'fadeIn', duration = 300) {
if (!element) return;
element.style.animation = `${animation} ${duration}ms ease`;
element.classList.add('animated');
// 动画完成后移除动画类
setTimeout(() => {
element.style.animation = '';
}, duration);
}
/**
* 创建波纹效果
* @param {Event} event - 点击事件
* @param {string} color - 波纹颜色
*/
function createRippleEffect(event, color = 'rgba(255, 255, 255, 0.7)') {
const button = event.currentTarget;
const circle = document.createElement('span');
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - button.offsetLeft - radius}px`;
circle.style.top = `${event.clientY - button.offsetTop - radius}px`;
circle.style.backgroundColor = color;
circle.style.position = 'absolute';
circle.style.borderRadius = '50%';
circle.style.transform = 'scale(0)';
circle.style.animation = 'ripple 600ms linear';
const ripple = button.querySelector('.ripple');
if (ripple) {
ripple.remove();
}
button.appendChild(circle);
// 动画完成后移除波纹元素
setTimeout(() => {
if (circle.parentNode === button) {
button.removeChild(circle);
}
}, 600);
}
/**
* 添加波纹效果到按钮
* @param {string} selector - 按钮选择器
*/
function addRippleEffectToButtons(selector = '.btn') {
const buttons = document.querySelectorAll(selector);
buttons.forEach(button => {
button.style.position = 'relative';
button.style.overflow = 'hidden';
button.addEventListener('click', function(event) {
createRippleEffect(event);
});
});
// 添加波纹动画CSS
if (!document.querySelector('#ripple-styles')) {
const style = document.createElement('style');
style.id = 'ripple-styles';
style.textContent = `
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
}
/**
* 初始化图片加载动画
*/
function initImageLoadingAnimations() {
const images = document.querySelectorAll('img:not([data-src])');
images.forEach(img => {
if (!img.complete) {
img.classList.add('loading');
img.addEventListener('load', function() {
this.classList.remove('loading');
this.classList.add('loaded');
animateElement(this, 'scaleIn', 500);
});
img.addEventListener('error', function() {
this.classList.remove('loading');
this.classList.add('error');
console.error('图片加载失败:', this.src);
});
} else {
img.classList.add('loaded');
}
});
}
/**
* 添加滚动到顶部按钮
*/
function initScrollToTopButton() {
// 创建按钮
const button = document.createElement('button');
button.id = 'scroll-to-top';
button.className = 'scroll-to-top-btn';
button.innerHTML = '<i class="fas fa-chevron-up"></i>';
button.title = '回到顶部';
button.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
width: 50px;
height: 50px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
box-shadow: var(--shadow-lg);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
`;
document.body.appendChild(button);
// 滚动事件监听
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
button.style.opacity = '1';
button.style.visibility = 'visible';
button.style.transform = 'translateY(0)';
} else {
button.style.opacity = '0';
button.style.visibility = 'hidden';
button.style.transform = 'translateY(20px)';
}
});
// 点击事件
button.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// 添加悬停效果
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = 'var(--primary-dark)';
button.style.transform = 'scale(1.1)';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = 'var(--primary-color)';
button.style.transform = 'scale(1)';
});
}
/**
* 性能优化:减少重绘
*/
function optimizeForPerformance() {
// 为动画元素添加 will-change
const animatedElements = document.querySelectorAll('.animate-on-scroll, .parallax, .counter');
animatedElements.forEach(element => {
element.style.willChange = 'transform, opacity';
});
// 使用 requestAnimationFrame 优化动画
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
// 执行滚动相关的动画
ticking = false;
});
ticking = true;
}
});
// 使用 transform 和 opacity 进行动画GPU加速
console.log('性能优化已应用使用GPU加速动画');
}
// DOM加载完成后初始化动画
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initAnimations();
addRippleEffectToButtons();
initImageLoadingAnimations();
initScrollToTopButton();
optimizeForPerformance();
});
} else {
initAnimations();
addRippleEffectToButtons();
initImageLoadingAnimations();
initScrollToTopButton();
optimizeForPerformance();
}
// 导出函数
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
initAnimations,
showPageTransition,
animateElement,
createSkeletonPlaceholders,
addRippleEffectToButtons
};
}