Files
Website/js/animations.js
2025-08-17 14:16:58 +02:00

403 Zeilen
13 KiB
JavaScript

/**
* Animation module for IntelSight website
* Contains all animation logic and visual effects
*/
// Particle Animation System
const ParticleAnimation = {
canvas: null,
ctx: null,
particles: [],
/**
* Initialize particle animation
*/
init() {
this.canvas = document.querySelector(SELECTORS.PARTICLE_CANVAS);
if (!this.canvas) return;
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
this.createParticles();
this.animate();
// Handle window resize
window.addEventListener('resize', () => this.resizeCanvas());
},
/**
* Resize canvas to window size
*/
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
},
/**
* Create particle objects
*/
createParticles() {
this.particles = [];
for (let i = 0; i < CONFIG.ANIMATION.PARTICLE_COUNT; i++) {
this.particles.push(new Particle(this.canvas));
}
},
/**
* Main animation loop
*/
animate() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Update and draw particles
this.particles.forEach(particle => {
particle.update(this.canvas);
particle.draw(this.ctx);
});
// Draw connections between particles
this.drawConnections();
requestAnimationFrame(() => this.animate());
},
/**
* Draw connections between nearby particles
*/
drawConnections() {
for (let a = 0; a < this.particles.length; a++) {
for (let b = a + 1; b < this.particles.length; b++) {
const distance = Math.sqrt(
Math.pow(this.particles[a].x - this.particles[b].x, 2) +
Math.pow(this.particles[a].y - this.particles[b].y, 2)
);
if (distance < CONFIG.ANIMATION.CONNECTION_DISTANCE) {
const opacity = 0.15 * (1 - distance / CONFIG.ANIMATION.CONNECTION_DISTANCE);
// Use darker blue for better visibility on light background
this.ctx.strokeStyle = `rgba(15, 114, 181, ${opacity})`;
this.ctx.lineWidth = 1;
this.ctx.beginPath();
this.ctx.moveTo(this.particles[a].x, this.particles[a].y);
this.ctx.lineTo(this.particles[b].x, this.particles[b].y);
this.ctx.stroke();
}
}
}
}
};
/**
* Particle class for animation
*/
class Particle {
constructor(canvas) {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * (CONFIG.ANIMATION.PARTICLE_SIZE_MAX - CONFIG.ANIMATION.PARTICLE_SIZE_MIN) + CONFIG.ANIMATION.PARTICLE_SIZE_MIN;
this.speedX = (Math.random() - 0.5) * CONFIG.ANIMATION.PARTICLE_SPEED;
this.speedY = (Math.random() - 0.5) * CONFIG.ANIMATION.PARTICLE_SPEED;
this.opacity = Math.random() * 0.5 + 0.2;
}
update(canvas) {
this.x += this.speedX;
this.y += this.speedY;
// Wrap around screen edges
if (this.x > canvas.width) this.x = 0;
else if (this.x < 0) this.x = canvas.width;
if (this.y > canvas.height) this.y = 0;
else if (this.y < 0) this.y = canvas.height;
}
draw(ctx) {
// Use darker blue for better visibility on light background
ctx.fillStyle = `rgba(15, 114, 181, ${this.opacity * 0.7})`;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
// Counter Animation
const CounterAnimation = {
/**
* Animate all counter elements
*/
animateAll() {
const counters = document.querySelectorAll(SELECTORS.INDICATOR_VALUE);
counters.forEach(counter => this.animateCounter(counter));
},
/**
* Animate a single counter
* @param {HTMLElement} counter - Counter element to animate
*/
animateCounter(counter) {
const target = parseFloat(counter.getAttribute(DATA_ATTRS.TARGET));
const increment = target / CONFIG.ANIMATION.COUNTER_SPEED;
let current = 0;
const updateCounter = () => {
current += increment;
if (current < target) {
counter.innerText = Math.ceil(current);
setTimeout(updateCounter, CONFIG.TIMING.COUNTER_UPDATE_INTERVAL);
} else {
// Set final value with proper formatting
if (target === CONFIG.TRUST_INDICATORS.AVAILABILITY) {
counter.innerText = target + '%';
} else if (target === CONFIG.TRUST_INDICATORS.AUTHORITIES_COUNT) {
counter.innerText = target + '+';
} else if (target === CONFIG.TRUST_INDICATORS.SUPPORT_HOURS) {
counter.innerText = target + '/7';
}
}
};
updateCounter();
}
};
// Scroll Animations
const ScrollAnimations = {
scrollIndicator: null,
/**
* Initialize scroll-based animations
*/
init() {
this.scrollIndicator = document.querySelector(SELECTORS.SCROLL_INDICATOR);
this.setupScrollIndicator();
this.setupIntersectionObserver();
},
/**
* Setup scroll indicator behavior
*/
setupScrollIndicator() {
if (!this.scrollIndicator) return;
// Click to scroll to about section
this.scrollIndicator.addEventListener('click', () => {
const aboutSection = document.querySelector('#about');
if (aboutSection) {
aboutSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
// Hide/show based on scroll position
let scrollTimeout;
window.addEventListener('scroll', () => {
const hero = document.querySelector(SELECTORS.HERO);
if (window.scrollY > CONFIG.ANIMATION.SCROLL_THRESHOLD) {
if (hero) hero.classList.add(CLASSES.SCROLLED);
if (this.scrollIndicator) this.scrollIndicator.style.opacity = '0';
} else {
if (hero) hero.classList.remove(CLASSES.SCROLLED);
if (this.scrollIndicator) this.scrollIndicator.style.opacity = '1';
}
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (window.scrollY > CONFIG.ANIMATION.SCROLL_THRESHOLD && this.scrollIndicator) {
this.scrollIndicator.style.display = 'none';
} else if (this.scrollIndicator) {
this.scrollIndicator.style.display = 'flex';
}
}, CONFIG.TIMING.SCROLL_HIDE_DELAY);
});
},
/**
* Setup intersection observer for scroll-triggered animations
*/
setupIntersectionObserver() {
const observerOptions = {
threshold: CONFIG.OBSERVER.THRESHOLD,
rootMargin: CONFIG.OBSERVER.ROOT_MARGIN
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Trust indicators animation
if (entry.target.classList.contains('trust-indicators')) {
CounterAnimation.animateAll();
observer.unobserve(entry.target);
}
// Timeline animation
if (entry.target.classList.contains('timeline')) {
const items = entry.target.querySelectorAll('.timeline-item');
items.forEach((item, index) => {
setTimeout(() => {
item.classList.add(CLASSES.VISIBLE);
}, index * 300);
});
observer.unobserve(entry.target);
}
// Feature nodes animation
if (entry.target.classList.contains('feature-nodes')) {
const nodes = entry.target.querySelectorAll('.node');
nodes.forEach((node, index) => {
setTimeout(() => {
node.style.opacity = '1';
node.style.transform = 'translateY(0)';
}, index * 150);
});
observer.unobserve(entry.target);
}
}
});
}, observerOptions);
// Observe elements
const trustIndicators = document.querySelector(SELECTORS.TRUST_INDICATORS);
if (trustIndicators) {
trustIndicators.style.opacity = '0';
observer.observe(trustIndicators);
}
const timeline = document.querySelector('.timeline');
if (timeline) observer.observe(timeline);
const featureNodes = document.querySelector('.feature-nodes');
if (featureNodes) {
document.querySelectorAll('.node').forEach(node => {
node.style.opacity = '0';
node.style.transform = 'translateY(30px)';
node.style.transition = 'all 0.6s ease';
});
observer.observe(featureNodes);
}
}
};
// Glitch Effect
const GlitchEffect = {
/**
* Apply glitch effect to element on hover
* @param {HTMLElement} element - Element to apply effect to
*/
apply(element) {
if (!element) return;
let glitchInterval;
element.addEventListener('mouseenter', () => {
let count = 0;
glitchInterval = setInterval(() => {
element.style.textShadow = `
${Math.random() * 5}px ${Math.random() * 5}px 0 rgba(0, 212, 255, 0.5),
${Math.random() * -5}px ${Math.random() * 5}px 0 rgba(255, 0, 128, 0.5)
`;
count++;
if (count > CONFIG.ANIMATION.GLITCH_ITERATIONS) {
clearInterval(glitchInterval);
element.style.textShadow = 'none';
}
}, CONFIG.ANIMATION.GLITCH_INTERVAL);
});
}
};
// Interactive Elements
const InteractiveElements = {
/**
* Initialize all interactive element animations
*/
init() {
this.setupNodeHoverEffects();
this.setupWidgetHoverEffects();
this.setupInteractiveIcon();
},
/**
* Setup hover effects for node elements
*/
setupNodeHoverEffects() {
document.querySelectorAll('.node').forEach(node => {
node.addEventListener('mouseenter', function() {
const icon = this.querySelector('.node-icon');
if (icon) icon.style.transform = 'scale(1.2) rotate(5deg)';
});
node.addEventListener('mouseleave', function() {
const icon = this.querySelector('.node-icon');
if (icon) icon.style.transform = 'scale(1) rotate(0deg)';
});
});
},
/**
* Setup hover effects for widget elements
*/
setupWidgetHoverEffects() {
document.querySelectorAll('.widget').forEach(widget => {
widget.addEventListener('mouseenter', function() {
this.style.boxShadow = '0 5px 20px rgba(0, 212, 255, 0.3)';
});
widget.addEventListener('mouseleave', function() {
this.style.boxShadow = 'none';
});
});
},
/**
* Setup 3D interactive icon effect
*/
setupInteractiveIcon() {
const icon = document.querySelector(SELECTORS.INTERACTIVE_ICON);
if (!icon) return;
document.addEventListener('mousemove', (e) => {
const rect = icon.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const mouseX = (e.clientX - centerX) / 20;
const mouseY = (e.clientY - centerY) / 20;
icon.style.transform = `perspective(1000px) rotateY(${mouseX}deg) rotateX(${-mouseY}deg)`;
});
}
};
// Initialize all animations
const Animations = {
/**
* Initialize all animation systems
*/
init() {
// Core animations
ParticleAnimation.init();
ScrollAnimations.init();
InteractiveElements.init();
// Apply glitch effect to main title
const mainTitle = document.querySelector('.main-title');
if (mainTitle) {
GlitchEffect.apply(mainTitle);
}
// Page load animations
window.addEventListener('load', () => {
document.body.classList.add(CLASSES.LOADED);
// Fade in hero content
setTimeout(() => {
const heroContent = document.querySelector(SELECTORS.HERO_CONTENT);
if (heroContent) {
heroContent.style.opacity = '1';
heroContent.style.transform = 'translateY(0)';
}
}, 100);
});
}
};