| |
| |
| |
|
|
|
|
| export class UIAnimations {
|
| |
| |
| |
| |
| |
| |
|
|
| static animateNumber(element, target, duration = 1000, suffix = '') {
|
| if (!element) return;
|
|
|
| const start = parseFloat(element.textContent) || 0;
|
| const increment = (target - start) / (duration / 16);
|
| let current = start;
|
|
|
| const timer = setInterval(() => {
|
| current += increment;
|
|
|
| if ((increment > 0 && current >= target) || (increment < 0 && current <= target)) {
|
| current = target;
|
| clearInterval(timer);
|
| }
|
|
|
| element.textContent = Math.round(current) + suffix;
|
| }, 16);
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| static animateEntrance(element, direction = 'up', delay = 0) {
|
| if (!element) return;
|
|
|
| const directions = {
|
| up: { x: 0, y: 20 },
|
| down: { x: 0, y: -20 },
|
| left: { x: 20, y: 0 },
|
| right: { x: -20, y: 0 }
|
| };
|
|
|
| const { x, y } = directions[direction] || directions.up;
|
|
|
| element.style.opacity = '0';
|
| element.style.transform = `translate(${x}px, ${y}px)`;
|
| element.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
|
|
| setTimeout(() => {
|
| element.style.opacity = '1';
|
| element.style.transform = 'translate(0, 0)';
|
| }, delay);
|
| }
|
|
|
| |
| |
| |
| |
|
|
| static staggerAnimation(elements, staggerDelay = 100) {
|
| if (!elements || elements.length === 0) return;
|
|
|
| elements.forEach((element, index) => {
|
| this.animateEntrance(element, 'up', index * staggerDelay);
|
| });
|
| }
|
|
|
| |
| |
| |
| |
|
|
| static createRipple(event, element) {
|
| if (!element) return;
|
|
|
| const ripple = document.createElement('span');
|
| const rect = element.getBoundingClientRect();
|
| const size = Math.max(rect.width, rect.height);
|
| const x = event.clientX - rect.left - size / 2;
|
| const y = event.clientY - rect.top - size / 2;
|
|
|
| ripple.style.cssText = `
|
| position: absolute;
|
| width: ${size}px;
|
| height: ${size}px;
|
| left: ${x}px;
|
| top: ${y}px;
|
| background: rgba(255, 255, 255, 0.5);
|
| border-radius: 50%;
|
| transform: scale(0);
|
| animation: ripple 0.6s ease-out;
|
| pointer-events: none;
|
| `;
|
|
|
| element.style.position = 'relative';
|
| element.style.overflow = 'hidden';
|
| element.appendChild(ripple);
|
|
|
| setTimeout(() => ripple.remove(), 600);
|
| }
|
|
|
| |
| |
| |
| |
|
|
| static smoothScrollTo(target, offset = 0) {
|
| const element = typeof target === 'string'
|
| ? document.querySelector(target)
|
| : target;
|
|
|
| if (!element) return;
|
|
|
| const targetPosition = element.getBoundingClientRect().top + window.pageYOffset - offset;
|
|
|
| window.scrollTo({
|
| top: targetPosition,
|
| behavior: 'smooth'
|
| });
|
| }
|
|
|
| |
| |
| |
| |
|
|
| static initParallax(element, speed = 0.5) {
|
| if (!element) return;
|
|
|
| const handleScroll = () => {
|
| const scrolled = window.pageYOffset;
|
| const rate = scrolled * speed;
|
| element.style.transform = `translateY(${rate}px)`;
|
| };
|
|
|
| window.addEventListener('scroll', handleScroll, { passive: true });
|
|
|
| return () => window.removeEventListener('scroll', handleScroll);
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| static observeElements(selector, callback, options = {}) {
|
| const defaultOptions = {
|
| threshold: 0.1,
|
| rootMargin: '0px',
|
| ...options
|
| };
|
|
|
| const observer = new IntersectionObserver((entries) => {
|
| entries.forEach(entry => {
|
| if (entry.isIntersecting) {
|
| callback(entry.target);
|
| observer.unobserve(entry.target);
|
| }
|
| });
|
| }, defaultOptions);
|
|
|
| document.querySelectorAll(selector).forEach(el => observer.observe(el));
|
|
|
| return observer;
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| static createSparkline(data, width = 60, height = 24) {
|
| if (!data || data.length === 0) return '';
|
|
|
| const max = Math.max(...data);
|
| const min = Math.min(...data);
|
| const range = max - min || 1;
|
|
|
| const points = data.map((value, index) => {
|
| const x = (index / (data.length - 1)) * width;
|
| const y = height - ((value - min) / range) * height;
|
| return `${x},${y}`;
|
| }).join(' ');
|
|
|
| return `
|
| <svg class="sparkline" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
| <polyline points="${points}" fill="none" stroke="currentColor" stroke-width="2" />
|
| </svg>
|
| `;
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| static animateProgress(element, percentage, duration = 1000) {
|
| if (!element) return;
|
|
|
| const start = parseFloat(element.style.width) || 0;
|
| const target = Math.min(Math.max(percentage, 0), 100);
|
| const increment = (target - start) / (duration / 16);
|
| let current = start;
|
|
|
| const timer = setInterval(() => {
|
| current += increment;
|
|
|
| if ((increment > 0 && current >= target) || (increment < 0 && current <= target)) {
|
| current = target;
|
| clearInterval(timer);
|
| }
|
|
|
| element.style.width = `${current}%`;
|
| }, 16);
|
| }
|
|
|
| |
| |
| |
|
|
| static shake(element) {
|
| if (!element) return;
|
|
|
| element.style.animation = 'shake 0.5s ease';
|
|
|
| setTimeout(() => {
|
| element.style.animation = '';
|
| }, 500);
|
| }
|
|
|
| |
| |
| |
| |
|
|
| static pulse(element, duration = 1000) {
|
| if (!element) return;
|
|
|
| element.style.animation = `pulse ${duration}ms ease`;
|
|
|
| setTimeout(() => {
|
| element.style.animation = '';
|
| }, duration);
|
| }
|
|
|
| |
| |
| |
| |
| |
|
|
| static typewriter(element, text, speed = 50) {
|
| if (!element) return;
|
|
|
| element.textContent = '';
|
| let index = 0;
|
|
|
| const timer = setInterval(() => {
|
| if (index < text.length) {
|
| element.textContent += text.charAt(index);
|
| index++;
|
| } else {
|
| clearInterval(timer);
|
| }
|
| }, speed);
|
|
|
| return timer;
|
| }
|
|
|
| |
| |
| |
|
|
| static confetti(options = {}) {
|
| const defaults = {
|
| particleCount: 50,
|
| spread: 70,
|
| origin: { y: 0.6 },
|
| colors: ['#2dd4bf', '#22d3ee', '#3b82f6']
|
| };
|
|
|
| const config = { ...defaults, ...options };
|
| const container = document.createElement('div');
|
| container.style.cssText = `
|
| position: fixed;
|
| inset: 0;
|
| pointer-events: none;
|
| z-index: 9999;
|
| `;
|
| document.body.appendChild(container);
|
|
|
| for (let i = 0; i < config.particleCount; i++) {
|
| const particle = document.createElement('div');
|
| const color = config.colors[Math.floor(Math.random() * config.colors.length)];
|
| const angle = Math.random() * config.spread - config.spread / 2;
|
| const velocity = Math.random() * 10 + 5;
|
|
|
| particle.style.cssText = `
|
| position: absolute;
|
| width: 8px;
|
| height: 8px;
|
| background: ${color};
|
| left: 50%;
|
| top: ${config.origin.y * 100}%;
|
| border-radius: 50%;
|
| animation: confetti 2s ease-out forwards;
|
| transform: rotate(${angle}deg) translateY(-${velocity}px);
|
| `;
|
|
|
| container.appendChild(particle);
|
| }
|
|
|
| setTimeout(() => container.remove(), 2000);
|
| }
|
|
|
| |
| |
|
|
| static init() {
|
|
|
| document.querySelectorAll('.btn-primary, .btn-gradient').forEach(button => {
|
| button.addEventListener('click', (e) => this.createRipple(e, button));
|
| });
|
|
|
|
|
| this.observeElements('.stat-card-enhanced, .glass-card', (element) => {
|
| this.animateEntrance(element, 'up');
|
| });
|
|
|
|
|
| if (!document.querySelector('#ui-animations-styles')) {
|
| const style = document.createElement('style');
|
| style.id = 'ui-animations-styles';
|
| style.textContent = `
|
| @keyframes ripple {
|
| to {
|
| transform: scale(4);
|
| opacity: 0;
|
| }
|
| }
|
|
|
| @keyframes shake {
|
| 0%, 100% { transform: translateX(0); }
|
| 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
| 20%, 40%, 60%, 80% { transform: translateX(5px); }
|
| }
|
|
|
| @keyframes confetti {
|
| 0% {
|
| transform: translateY(0) rotate(0deg);
|
| opacity: 1;
|
| }
|
| 100% {
|
| transform: translateY(100vh) rotate(720deg);
|
| opacity: 0;
|
| }
|
| }
|
| `;
|
| document.head.appendChild(style);
|
| }
|
| }
|
| }
|
|
|
|
|
| if (document.readyState === 'loading') {
|
| document.addEventListener('DOMContentLoaded', () => UIAnimations.init());
|
| } else {
|
| UIAnimations.init();
|
| }
|
|
|
| export default UIAnimations;
|
|
|