656 lines
23 KiB
JavaScript
656 lines
23 KiB
JavaScript
/**
|
|
* Tech-Onepager JavaScript - Interactive Elements and Animations
|
|
* System being built aesthetic with smooth micro-animations
|
|
*/
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// ===== GLOBAL VARIABLES =====
|
|
let currentTooltip = null;
|
|
|
|
// ===== SYSTEM GRAPHIC ANIMATIONS =====
|
|
const systemGraphic = document.getElementById('systemGraphic');
|
|
const connections = document.getElementById('connections');
|
|
const dataPoints = document.getElementById('dataPoints');
|
|
|
|
if (systemGraphic && connections) {
|
|
initializeSystemGraphic();
|
|
}
|
|
|
|
function initializeSystemGraphic() {
|
|
const nodes = systemGraphic.querySelectorAll('.node');
|
|
const centralNode = systemGraphic.querySelector('.central-node');
|
|
|
|
// Draw connection lines after layout is complete
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(drawConnections);
|
|
});
|
|
|
|
// Also redraw after a short delay for CSS transitions
|
|
setTimeout(drawConnections, 300);
|
|
setTimeout(drawConnections, 600);
|
|
setTimeout(drawConnections, 1000);
|
|
|
|
// Redraw on resize with multiple delayed calls to handle CSS transitions
|
|
let resizeTimeout;
|
|
let resizeRAF;
|
|
window.addEventListener('resize', function() {
|
|
// Cancel any pending animation frame
|
|
if (resizeRAF) cancelAnimationFrame(resizeRAF);
|
|
clearTimeout(resizeTimeout);
|
|
|
|
// Immediate redraw
|
|
resizeRAF = requestAnimationFrame(drawConnections);
|
|
|
|
// Delayed redraws to catch CSS transition completion
|
|
resizeTimeout = setTimeout(() => {
|
|
drawConnections();
|
|
setTimeout(drawConnections, 200);
|
|
setTimeout(drawConnections, 400);
|
|
}, 100);
|
|
});
|
|
|
|
// Redraw on orientation change (mobile)
|
|
window.addEventListener('orientationchange', function() {
|
|
setTimeout(drawConnections, 300);
|
|
setTimeout(drawConnections, 600);
|
|
setTimeout(drawConnections, 1000);
|
|
});
|
|
|
|
// Also listen for transitionend on the system graphic
|
|
systemGraphic.addEventListener('transitionend', function() {
|
|
requestAnimationFrame(drawConnections);
|
|
});
|
|
|
|
// Initialize tooltip system
|
|
initializeTooltips();
|
|
|
|
// Add node interactions
|
|
nodes.forEach(node => {
|
|
node.addEventListener('mouseenter', function() {
|
|
activateConnection(this);
|
|
});
|
|
|
|
node.addEventListener('mouseleave', function() {
|
|
deactivateConnections();
|
|
});
|
|
|
|
// Special handling for central node
|
|
if (node.classList.contains('central-node')) {
|
|
node.addEventListener('click', function() {
|
|
triggerCentralNode(this);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Scroll-based activation
|
|
setupScrollActivation();
|
|
}
|
|
|
|
// ===== TOOLTIP SYSTEM =====
|
|
function initializeTooltips() {
|
|
const nodes = systemGraphic.querySelectorAll('.node[data-tooltip]');
|
|
|
|
nodes.forEach(node => {
|
|
let hoverTimeout;
|
|
|
|
node.addEventListener('mouseenter', function() {
|
|
const tooltip = this.getAttribute('data-tooltip');
|
|
if (!tooltip) return;
|
|
|
|
// Clear any existing timeout
|
|
clearTimeout(hoverTimeout);
|
|
|
|
// Set 1-second delay before showing tooltip
|
|
hoverTimeout = setTimeout(() => {
|
|
showTooltip(this, tooltip);
|
|
}, 1000);
|
|
});
|
|
|
|
node.addEventListener('mouseleave', function() {
|
|
// Clear the timeout if mouse leaves before 1 second
|
|
clearTimeout(hoverTimeout);
|
|
|
|
// Hide any visible tooltip
|
|
hideTooltip();
|
|
});
|
|
});
|
|
}
|
|
|
|
function showTooltip(node, text) {
|
|
// Remove any existing tooltip
|
|
hideTooltip();
|
|
|
|
// Create tooltip element
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'node-tooltip';
|
|
tooltip.textContent = text;
|
|
|
|
// Position the tooltip
|
|
const nodeRect = node.getBoundingClientRect();
|
|
const systemGraphicRect = systemGraphic.getBoundingClientRect();
|
|
|
|
// Calculate position relative to system graphic
|
|
let left = nodeRect.left - systemGraphicRect.left + (nodeRect.width / 2) - 140; // Center horizontally
|
|
let top = nodeRect.top - systemGraphicRect.top - 60; // Position above node
|
|
|
|
// Adjust if tooltip goes outside bounds
|
|
if (left < 10) left = 10;
|
|
if (left + 280 > systemGraphicRect.width - 10) left = systemGraphicRect.width - 290;
|
|
if (top < 10) top = nodeRect.top - systemGraphicRect.top + nodeRect.height + 10; // Show below if not enough space above
|
|
|
|
tooltip.style.left = left + 'px';
|
|
tooltip.style.top = top + 'px';
|
|
|
|
// Add to system graphic
|
|
systemGraphic.appendChild(tooltip);
|
|
|
|
// Trigger show animation
|
|
setTimeout(() => {
|
|
tooltip.classList.add('show');
|
|
}, 10);
|
|
|
|
currentTooltip = tooltip;
|
|
}
|
|
|
|
function hideTooltip() {
|
|
if (currentTooltip) {
|
|
currentTooltip.classList.remove('show');
|
|
setTimeout(() => {
|
|
if (currentTooltip && currentTooltip.parentNode) {
|
|
currentTooltip.parentNode.removeChild(currentTooltip);
|
|
}
|
|
currentTooltip = null;
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
// Cleanup function
|
|
function cleanupTooltips() {
|
|
hideTooltip();
|
|
}
|
|
|
|
// Add cleanup on page unload
|
|
window.addEventListener('beforeunload', cleanupTooltips);
|
|
|
|
function drawConnections() {
|
|
const centralNode = systemGraphic.querySelector('.central-node');
|
|
const otherNodes = systemGraphic.querySelectorAll('.node:not(.central-node)');
|
|
|
|
if (!centralNode || !systemGraphic || !connections) return;
|
|
|
|
// Get the computed transform scale of the system graphic
|
|
const computedStyle = window.getComputedStyle(systemGraphic);
|
|
const transform = computedStyle.transform;
|
|
let scale = 1;
|
|
if (transform && transform !== 'none') {
|
|
const matrix = new DOMMatrix(transform);
|
|
scale = matrix.a; // Get the X scale factor
|
|
}
|
|
|
|
const graphicRect = systemGraphic.getBoundingClientRect();
|
|
const centralRect = centralNode.getBoundingClientRect();
|
|
|
|
// Calculate center position relative to the graphic container, accounting for scale
|
|
const centerX = (centralRect.left - graphicRect.left + centralRect.width / 2) / scale;
|
|
const centerY = (centralRect.top - graphicRect.top + centralRect.height / 2) / scale;
|
|
|
|
// Set SVG viewBox to match container (unscaled dimensions)
|
|
const viewBoxWidth = graphicRect.width / scale;
|
|
const viewBoxHeight = graphicRect.height / scale;
|
|
connections.setAttribute('viewBox', `0 0 ${viewBoxWidth} ${viewBoxHeight}`);
|
|
connections.style.width = '100%';
|
|
connections.style.height = '100%';
|
|
|
|
connections.innerHTML = '';
|
|
|
|
otherNodes.forEach(node => {
|
|
const nodeRect = node.getBoundingClientRect();
|
|
|
|
// Calculate node center position relative to the graphic container, accounting for scale
|
|
const nodeX = (nodeRect.left - graphicRect.left + nodeRect.width / 2) / scale;
|
|
const nodeY = (nodeRect.top - graphicRect.top + nodeRect.height / 2) / scale;
|
|
|
|
// Create dashed line for better visibility
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', centerX);
|
|
line.setAttribute('y1', centerY);
|
|
line.setAttribute('x2', nodeX);
|
|
line.setAttribute('y2', nodeY);
|
|
line.setAttribute('class', 'connection-line');
|
|
line.setAttribute('data-target', node.dataset.node);
|
|
|
|
connections.appendChild(line);
|
|
});
|
|
}
|
|
|
|
|
|
function triggerCentralNode(node) {
|
|
// Add triggered class for growth animation
|
|
node.classList.add('triggered');
|
|
|
|
// Activate all connections
|
|
const lines = connections.querySelectorAll('.connection-line');
|
|
lines.forEach((line, index) => {
|
|
setTimeout(() => {
|
|
line.classList.add('active');
|
|
}, index * 100);
|
|
});
|
|
|
|
// Remove triggered class after animation
|
|
setTimeout(() => {
|
|
node.classList.remove('triggered');
|
|
|
|
// Deactivate connections
|
|
setTimeout(() => {
|
|
lines.forEach(line => line.classList.remove('active'));
|
|
}, 500);
|
|
}, 2000);
|
|
}
|
|
|
|
function activateConnection(node) {
|
|
const targetNode = node.dataset.node;
|
|
const line = connections.querySelector(`[data-target="${targetNode}"]`);
|
|
if (line) {
|
|
line.classList.add('active');
|
|
}
|
|
}
|
|
|
|
function deactivateConnections() {
|
|
const lines = connections.querySelectorAll('.connection-line');
|
|
lines.forEach(line => line.classList.remove('active'));
|
|
}
|
|
|
|
function setupScrollActivation() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const lines = connections.querySelectorAll('.connection-line');
|
|
lines.forEach((line, index) => {
|
|
setTimeout(() => {
|
|
line.classList.add('active');
|
|
setTimeout(() => {
|
|
line.classList.remove('active');
|
|
}, 2000);
|
|
}, index * 200);
|
|
});
|
|
}
|
|
});
|
|
}, { threshold: 0.5 });
|
|
|
|
observer.observe(systemGraphic);
|
|
}
|
|
|
|
// ===== PROCESS LINE ANIMATION =====
|
|
const processLine = document.getElementById('processLine');
|
|
const processSteps = document.querySelectorAll('.process-step');
|
|
const processConnectors = document.querySelectorAll('.process-connector');
|
|
const stepDetails = document.querySelectorAll('.step-detail');
|
|
|
|
if (processLine) {
|
|
setupProcessAnimation();
|
|
}
|
|
|
|
function setupProcessAnimation() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
animateProcessLine();
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.3 });
|
|
|
|
observer.observe(processLine);
|
|
}
|
|
|
|
function animateProcessLine() {
|
|
// Animate connectors sequentially
|
|
processConnectors.forEach((connector, index) => {
|
|
setTimeout(() => {
|
|
connector.classList.add('active');
|
|
}, 500 + (index * 500));
|
|
});
|
|
|
|
// Activate steps sequentially
|
|
processSteps.forEach((step, index) => {
|
|
setTimeout(() => {
|
|
step.classList.add('active');
|
|
activateStepDetail(index + 1);
|
|
}, 200 + (index * 500));
|
|
});
|
|
}
|
|
|
|
function activateStepDetail(stepNumber) {
|
|
const detail = document.querySelector(`[data-step-detail="${stepNumber}"]`);
|
|
if (detail) {
|
|
setTimeout(() => {
|
|
detail.classList.add('active');
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
// Step click interactions
|
|
processSteps.forEach((step, index) => {
|
|
step.addEventListener('click', () => {
|
|
// Reset all steps
|
|
processSteps.forEach(s => s.classList.remove('active'));
|
|
processConnectors.forEach(c => c.classList.remove('active'));
|
|
stepDetails.forEach(d => d.classList.remove('active'));
|
|
|
|
// Activate up to clicked step
|
|
for (let i = 0; i <= index; i++) {
|
|
processSteps[i].classList.add('active');
|
|
activateStepDetail(i + 1);
|
|
|
|
if (i < processConnectors.length) {
|
|
processConnectors[i].classList.add('active');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// ===== INTERACTION CARD ANIMATIONS =====
|
|
const phoneInteraction = document.getElementById('phoneInteraction');
|
|
const chatInteraction = document.getElementById('chatInteraction');
|
|
|
|
if (phoneInteraction) {
|
|
setupPhoneInteraction();
|
|
}
|
|
|
|
if (chatInteraction) {
|
|
setupChatInteraction();
|
|
}
|
|
|
|
function setupPhoneInteraction() {
|
|
const microphone = phoneInteraction.querySelector('.microphone');
|
|
const pulseRing = phoneInteraction.querySelector('.pulse-ring');
|
|
const micIcon = phoneInteraction.querySelector('.mic-icon');
|
|
|
|
if (microphone && pulseRing && micIcon) {
|
|
microphone.addEventListener('mouseenter', () => {
|
|
pulseRing.style.animation = 'pulse 0.8s infinite';
|
|
micIcon.style.transform = 'scale(1.1)';
|
|
});
|
|
|
|
microphone.addEventListener('mouseleave', () => {
|
|
pulseRing.style.animation = 'pulse 2s infinite';
|
|
micIcon.style.transform = 'scale(1)';
|
|
});
|
|
|
|
microphone.addEventListener('click', () => {
|
|
// Enhanced click animation
|
|
microphone.style.transform = 'scale(0.9)';
|
|
micIcon.style.transform = 'scale(0.8)';
|
|
|
|
// Create ripple effect
|
|
const ripple = document.createElement('div');
|
|
ripple.style.cssText = `
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 20px;
|
|
height: 20px;
|
|
background: rgba(255, 255, 255, 0.5);
|
|
border-radius: 50%;
|
|
animation: rippleEffect 0.6s ease-out;
|
|
pointer-events: none;
|
|
`;
|
|
microphone.appendChild(ripple);
|
|
|
|
setTimeout(() => {
|
|
microphone.style.transform = 'scale(1.15)';
|
|
micIcon.style.transform = 'scale(1.1)';
|
|
setTimeout(() => {
|
|
microphone.style.transform = 'scale(1)';
|
|
micIcon.style.transform = 'scale(1)';
|
|
ripple.remove();
|
|
}, 200);
|
|
}, 100);
|
|
|
|
// Show feedback
|
|
showInteractionFeedback('KI Telefon wird verbunden...');
|
|
});
|
|
}
|
|
}
|
|
|
|
function setupChatInteraction() {
|
|
const chatWindow = chatInteraction.querySelector('.chat-window');
|
|
const typingIndicator = chatInteraction.querySelector('.typing-indicator');
|
|
const chatHeader = chatInteraction.querySelector('.chat-header');
|
|
|
|
if (chatWindow && typingIndicator && chatHeader) {
|
|
chatInteraction.addEventListener('mouseenter', () => {
|
|
// Enhanced typing animation
|
|
const spans = typingIndicator.querySelectorAll('span');
|
|
spans.forEach((span, index) => {
|
|
span.style.animation = 'typing 1.2s infinite ease-in-out';
|
|
span.style.animationDelay = `${-0.32 + (index * 0.16)}s`;
|
|
span.style.background = 'var(--accent-teal)';
|
|
});
|
|
|
|
// Animate chat window
|
|
chatWindow.style.transform = 'scale(1.02)';
|
|
chatHeader.style.background = 'linear-gradient(135deg, var(--accent-green), var(--accent-teal))';
|
|
});
|
|
|
|
chatInteraction.addEventListener('mouseleave', () => {
|
|
const spans = typingIndicator.querySelectorAll('span');
|
|
spans.forEach(span => {
|
|
span.style.background = 'var(--primary-mid)';
|
|
});
|
|
chatWindow.style.transform = 'scale(1)';
|
|
chatHeader.style.background = 'linear-gradient(135deg, var(--accent-teal), var(--accent-green))';
|
|
});
|
|
|
|
const chatBtn = chatInteraction.querySelector('.interaction-btn.secondary');
|
|
if (chatBtn) {
|
|
chatBtn.addEventListener('click', () => {
|
|
// Enhanced click animation
|
|
chatWindow.style.transform = 'scale(0.95)';
|
|
setTimeout(() => {
|
|
chatWindow.style.transform = 'scale(1.05)';
|
|
setTimeout(() => {
|
|
chatWindow.style.transform = 'scale(1)';
|
|
}, 200);
|
|
}, 100);
|
|
|
|
showInteractionFeedback('KI Chat wird gestartet...');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function showInteractionFeedback(message) {
|
|
// Create temporary feedback element
|
|
const feedback = document.createElement('div');
|
|
feedback.style.cssText = `
|
|
position: fixed;
|
|
top: 100px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: var(--accent-teal);
|
|
color: white;
|
|
padding: 16px 24px;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
z-index: 10000;
|
|
box-shadow: 0 8px 30px rgba(38, 166, 154, 0.4);
|
|
animation: slideDown 0.3s ease;
|
|
`;
|
|
feedback.textContent = message;
|
|
|
|
document.body.appendChild(feedback);
|
|
|
|
setTimeout(() => {
|
|
feedback.style.animation = 'slideUp 0.3s ease';
|
|
setTimeout(() => {
|
|
feedback.remove();
|
|
}, 300);
|
|
}, 2000);
|
|
}
|
|
|
|
// ===== HERO BUTTON INTERACTIONS =====
|
|
const kiPhoneBtn = document.getElementById('kiPhoneBtn');
|
|
const chatBtn = document.getElementById('chatBtn');
|
|
|
|
if (kiPhoneBtn) {
|
|
kiPhoneBtn.addEventListener('click', () => {
|
|
showInteractionFeedback('KI Telefon wird verbunden...');
|
|
// Scroll to interaction section
|
|
document.getElementById('interaction')?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
});
|
|
}
|
|
|
|
if (chatBtn) {
|
|
chatBtn.addEventListener('click', () => {
|
|
showInteractionFeedback('KI Chat wird gestartet...');
|
|
// Scroll to interaction section
|
|
document.getElementById('interaction')?.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===== SYSTEM CARD HOVER EFFECTS =====
|
|
const systemCards = document.querySelectorAll('.system-card');
|
|
|
|
systemCards.forEach(card => {
|
|
card.addEventListener('mouseenter', function() {
|
|
// Add subtle glow effect
|
|
this.style.boxShadow = '0 25px 50px rgba(38, 166, 154, 0.2)';
|
|
|
|
// Animate internal components
|
|
const flowItems = this.querySelectorAll('.flow-item, .phone-icon, .ki-processor, .crm-output');
|
|
flowItems.forEach((item, index) => {
|
|
setTimeout(() => {
|
|
item.style.transform = 'translateY(-2px)';
|
|
setTimeout(() => {
|
|
item.style.transform = 'translateY(0)';
|
|
}, 200);
|
|
}, index * 100);
|
|
});
|
|
});
|
|
|
|
card.addEventListener('mouseleave', function() {
|
|
this.style.boxShadow = '0 12px 30px rgba(79, 71, 71, 0.1)';
|
|
});
|
|
});
|
|
|
|
// ===== DATA CARD ANIMATIONS =====
|
|
const dataCards = document.querySelectorAll('.data-card');
|
|
|
|
const dataCardObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry, index) => {
|
|
if (entry.isIntersecting) {
|
|
setTimeout(() => {
|
|
entry.target.style.animation = 'fadeInUp 0.6s ease forwards';
|
|
}, index * 100);
|
|
dataCardObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.1 });
|
|
|
|
dataCards.forEach(card => {
|
|
dataCardObserver.observe(card);
|
|
});
|
|
|
|
// ===== SMOOTH SCROLL FOR INTERNAL LINKS =====
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
const target = document.querySelector(this.getAttribute('href'));
|
|
if (target) {
|
|
target.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// ===== UTILITY ANIMATIONS =====
|
|
// Add CSS animations dynamically
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translate(-50%, -20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translate(-50%, 0);
|
|
}
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
opacity: 1;
|
|
transform: translate(-50%, 0);
|
|
}
|
|
to {
|
|
opacity: 0;
|
|
transform: translate(-50%, -20px);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes rippleEffect {
|
|
from {
|
|
width: 20px;
|
|
height: 20px;
|
|
opacity: 0.5;
|
|
}
|
|
to {
|
|
width: 100px;
|
|
height: 100px;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); opacity: 0; }
|
|
50% { opacity: 1; }
|
|
100% { transform: translateX(100%) translateY(100%) rotate(45deg); opacity: 0; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// ===== PERFORMANCE OPTIMIZATION =====
|
|
// Throttle scroll events
|
|
let ticking = false;
|
|
function requestTick() {
|
|
if (!ticking) {
|
|
requestAnimationFrame(updateScrollEffects);
|
|
ticking = true;
|
|
setTimeout(() => { ticking = false; }, 16);
|
|
}
|
|
}
|
|
|
|
function updateScrollEffects() {
|
|
// Add scroll-based effects here if needed
|
|
// Parallax, fade-ins, etc.
|
|
}
|
|
|
|
window.addEventListener('scroll', requestTick, { passive: true });
|
|
|
|
// ===== INITIALIZATION COMPLETE =====
|
|
console.log('Tech-Onepager initialized successfully');
|
|
});
|