Files
website/website-mockups/js/animations.js

349 lines
10 KiB
JavaScript
Raw Normal View History

2025-09-18 22:20:01 +02:00
// HarborSmith - Animation Scripts
// ================================
// Scroll Animation Observer
const scrollAnimations = {
init() {
this.observeElements();
this.initProgressBars();
this.initTextAnimations();
},
observeElements() {
const options = {
threshold: 0.1,
rootMargin: '0px 0px -100px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
// Trigger specific animations
if (entry.target.classList.contains('counter-animate')) {
this.animateValue(entry.target);
}
// Only animate once
if (!entry.target.classList.contains('repeat-animation')) {
observer.unobserve(entry.target);
}
}
});
}, options);
// Observe all animated elements
const animatedElements = document.querySelectorAll('.scroll-animate, .scroll-animate-left, .scroll-animate-right, .scroll-animate-scale, .counter-animate');
animatedElements.forEach(el => observer.observe(el));
},
animateValue(element) {
const target = parseInt(element.dataset.target);
const duration = parseInt(element.dataset.duration) || 2000;
const start = 0;
const increment = target / (duration / 16);
let current = start;
const updateValue = () => {
current += increment;
if (current < target) {
element.textContent = Math.floor(current);
requestAnimationFrame(updateValue);
} else {
element.textContent = target;
}
};
updateValue();
},
initProgressBars() {
const progressBars = document.querySelectorAll('.progress-bar-fill');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const progressBar = entry.target;
const targetWidth = progressBar.dataset.progress || '100';
progressBar.style.setProperty('--progress', targetWidth + '%');
progressBar.classList.add('animate');
observer.unobserve(progressBar);
}
});
}, { threshold: 0.5 });
progressBars.forEach(bar => observer.observe(bar));
},
initTextAnimations() {
const textElements = document.querySelectorAll('.text-animate');
textElements.forEach(element => {
const text = element.textContent;
element.textContent = '';
[...text].forEach((char, index) => {
const span = document.createElement('span');
span.textContent = char === ' ' ? '\u00A0' : char;
span.style.animationDelay = `${index * 0.05}s`;
span.classList.add('char-animate');
element.appendChild(span);
});
});
}
};
// Cursor Effects
const cursorEffects = {
init() {
this.cursor = document.createElement('div');
this.cursor.className = 'custom-cursor';
document.body.appendChild(this.cursor);
this.follower = document.createElement('div');
this.follower.className = 'cursor-follower';
document.body.appendChild(this.follower);
this.initEventListeners();
},
initEventListeners() {
document.addEventListener('mousemove', (e) => {
this.cursor.style.left = e.clientX + 'px';
this.cursor.style.top = e.clientY + 'px';
setTimeout(() => {
this.follower.style.left = e.clientX + 'px';
this.follower.style.top = e.clientY + 'px';
}, 100);
});
// Add hover effects
const interactiveElements = document.querySelectorAll('a, button, .clickable');
interactiveElements.forEach(el => {
el.addEventListener('mouseenter', () => {
this.cursor.classList.add('hover');
this.follower.classList.add('hover');
});
el.addEventListener('mouseleave', () => {
this.cursor.classList.remove('hover');
this.follower.classList.remove('hover');
});
});
}
};
// Ripple Effect
const rippleEffect = {
init() {
const buttons = document.querySelectorAll('.btn, .ripple');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
const ripple = document.createElement('span');
ripple.className = 'ripple-effect';
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
button.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
});
}
};
// Magnetic Buttons
const magneticButtons = {
init() {
const magneticElements = document.querySelectorAll('.magnetic');
magneticElements.forEach(element => {
element.addEventListener('mousemove', (e) => {
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
element.style.transform = `translate(${x * 0.2}px, ${y * 0.2}px)`;
});
element.addEventListener('mouseleave', () => {
element.style.transform = 'translate(0, 0)';
});
});
}
};
// Page Loader
const pageLoader = {
init() {
const loader = document.createElement('div');
loader.className = 'page-loader';
loader.innerHTML = `
<div class="loader-content">
<div class="loader-ship">
<svg width="60" height="60" viewBox="0 0 60 60">
<path d="M30 10 L45 40 L15 40 Z" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M30 40 L30 50" stroke="currentColor" stroke-width="2"/>
<path d="M20 50 L40 50" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="loader-text">Setting Sail...</div>
</div>
`;
document.body.appendChild(loader);
window.addEventListener('load', () => {
setTimeout(() => {
loader.classList.add('fade-out');
setTimeout(() => {
loader.remove();
}, 500);
}, 1000);
});
}
};
// Smooth Page Transitions
const pageTransitions = {
init() {
const links = document.querySelectorAll('a[href^="/"], a[href^="./"], a[href$=".html"]');
links.forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
// Skip if it's an anchor link or external link
if (href.startsWith('#') || href.startsWith('http')) return;
e.preventDefault();
document.body.classList.add('page-transition');
setTimeout(() => {
window.location.href = href;
}, 500);
});
});
}
};
// Initialize animations
document.addEventListener('DOMContentLoaded', () => {
scrollAnimations.init();
rippleEffect.init();
magneticButtons.init();
// Uncomment these for production
// cursorEffects.init();
// pageLoader.init();
// pageTransitions.init();
});
// Add some CSS for the loader (normally would be in a separate file)
const loaderStyles = `
<style>
.page-loader {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.5s;
}
.page-loader.fade-out {
opacity: 0;
pointer-events: none;
}
.loader-content {
text-align: center;
color: white;
}
.loader-ship {
animation: float 2s ease-in-out infinite;
margin-bottom: 20px;
}
.loader-text {
font-size: 1.5rem;
font-weight: 600;
animation: pulse 2s ease-in-out infinite;
}
.custom-cursor {
width: 10px;
height: 10px;
background: var(--primary);
border-radius: 50%;
position: fixed;
pointer-events: none;
z-index: 9998;
transform: translate(-50%, -50%);
transition: transform 0.2s, background 0.2s;
}
.cursor-follower {
width: 30px;
height: 30px;
border: 2px solid var(--primary);
border-radius: 50%;
position: fixed;
pointer-events: none;
z-index: 9997;
transform: translate(-50%, -50%);
transition: transform 0.3s, border-color 0.2s;
}
.custom-cursor.hover,
.cursor-follower.hover {
transform: translate(-50%, -50%) scale(1.5);
}
.ripple-effect {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
animation: ripple 0.6s ease-out;
pointer-events: none;
}
.char-animate {
display: inline-block;
opacity: 0;
animation: fadeInUp 0.5s ease-out forwards;
}
.page-transition {
animation: fadeOut 0.5s ease-out;
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateY(20px);
}
}
</style>
`;
// Inject loader styles
document.head.insertAdjacentHTML('beforeend', loaderStyles);