tractatus/public/js/scroll-animations.js
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

135 lines
4.6 KiB
JavaScript

/**
* Scroll Animations using Intersection Observer
* Tractatus Framework - Phase 3: Engagement & Interactive Features
*
* Provides smooth scroll-triggered animations for elements marked with .animate-on-scroll
* Supports multiple animation types via data-animation attribute
*/
class ScrollAnimations {
constructor(options = {}) {
this.observerOptions = {
threshold: options.threshold || 0.1,
rootMargin: options.rootMargin || '0px 0px -100px 0px'
};
this.animations = {
'fade-in': 'opacity-0 animate-fade-in',
'slide-up': 'opacity-0 translate-y-8 animate-slide-up',
'slide-down': 'opacity-0 -translate-y-8 animate-slide-down',
'slide-left': 'opacity-0 translate-x-8 animate-slide-left',
'slide-right': 'opacity-0 -translate-x-8 animate-slide-right',
'scale-in': 'opacity-0 scale-95 animate-scale-in',
'rotate-in': 'opacity-0 rotate-12 animate-rotate-in'
};
this.init();
}
init() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.observe());
} else {
this.observe();
}
console.log('[ScrollAnimations] Initialized');
}
observe() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.animateElement(entry.target);
// Optional: unobserve after animation to improve performance
// Only do this for elements without data-animate-repeat attribute
if (!entry.target.hasAttribute('data-animate-repeat')) {
observer.unobserve(entry.target);
}
} else if (entry.target.hasAttribute('data-animate-repeat')) {
// Reset animation for repeatable elements
this.resetElement(entry.target);
}
});
}, this.observerOptions);
// Find all elements with .animate-on-scroll class
const elements = document.querySelectorAll('.animate-on-scroll');
console.log(`[ScrollAnimations] Observing ${elements.length} elements`);
elements.forEach((el, index) => {
// Add stagger delay if data-stagger attribute is present
if (el.hasAttribute('data-stagger')) {
const delay = parseInt(el.getAttribute('data-stagger')) || (index * 100);
el.style.animationDelay = `${delay}ms`;
}
// Apply initial animation classes based on data-animation attribute
const animationType = el.getAttribute('data-animation') || 'fade-in';
if (this.animations[animationType]) {
// Remove any existing animation classes
Object.values(this.animations).forEach(classes => {
classes.split(' ').forEach(cls => el.classList.remove(cls));
});
// Add initial state classes (will be removed when visible)
const initialClasses = this.getInitialClasses(animationType);
initialClasses.forEach(cls => el.classList.add(cls));
}
observer.observe(el);
});
}
getInitialClasses(animationType) {
// Return classes that represent the "before animation" state
const map = {
'fade-in': ['opacity-0'],
'slide-up': ['opacity-0', 'translate-y-8'],
'slide-down': ['opacity-0', '-translate-y-8'],
'slide-left': ['opacity-0', 'translate-x-8'],
'slide-right': ['opacity-0', '-translate-x-8'],
'scale-in': ['opacity-0', 'scale-95'],
'rotate-in': ['opacity-0', 'rotate-12']
};
return map[animationType] || ['opacity-0'];
}
animateElement(element) {
// Remove initial state classes
element.classList.remove('opacity-0', 'translate-y-8', '-translate-y-8', 'translate-x-8', '-translate-x-8', 'scale-95', 'rotate-12');
// Add visible state
element.classList.add('is-visible');
// Trigger custom event for other components to listen to
element.dispatchEvent(new CustomEvent('scroll-animated', {
bubbles: true,
detail: { element }
}));
}
resetElement(element) {
// Remove visible state
element.classList.remove('is-visible');
// Re-apply initial animation classes
const animationType = element.getAttribute('data-animation') || 'fade-in';
const initialClasses = this.getInitialClasses(animationType);
initialClasses.forEach(cls => element.classList.add(cls));
}
}
// Auto-initialize when script loads
// Can be disabled by setting window.DISABLE_AUTO_SCROLL_ANIMATIONS = true before loading this script
if (typeof window !== 'undefined' && !window.DISABLE_AUTO_SCROLL_ANIMATIONS) {
window.scrollAnimations = new ScrollAnimations();
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = ScrollAnimations;
}