scrollcue.js
Version:
A lightweight scroll animation library using Intersection Observer API with advanced transform animations
302 lines (269 loc) • 8.23 kB
JavaScript
(function() {
'use strict';
// Animation definitions - all animations are defined here
const animations = {
'fade-in': {
css: `
.scrollcue.fade-in.is-inactive { opacity: 0; }
.scrollcue.fade-in.cue-in { animation-name: fadeIn; }
fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
`
},
'fade-up': {
css: `
.scrollcue.fade-up.is-inactive {
opacity: 0;
transform: translateY(20px);
}
.scrollcue.fade-up.cue-in { animation-name: fadeUp; }
fadeUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
`
},
'slide-up': {
css: `
.scrollcue.slide-up.is-inactive {
transform: translateY(100px);
opacity: 0;
}
.scrollcue.slide-up.cue-in { animation-name: slideUp; }
slideUp {
0% {
transform: translateY(100px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
`
},
'bounce-in': {
css: `
.scrollcue.bounce-in.is-inactive {
transform: scale(0.3);
opacity: 0;
}
.scrollcue.bounce-in.cue-in { animation-name: bounceIn; }
bounceIn {
0% { transform: scale(0.3); opacity: 0; }
50% { transform: scale(1.05); opacity: 0.8; }
70% { transform: scale(0.9); opacity: 0.9; }
100% { transform: scale(1); opacity: 1; }
}
`
},
'fade-split': {
css: `
.scrollcue.fade-split {
position: relative;
overflow: hidden;
}
.scrollcue.fade-split .split-left,
.scrollcue.fade-split .split-right {
position: absolute;
top: 0;
width: 50%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.scrollcue.fade-split .split-left {
left: 0;
justify-content: flex-end;
}
.scrollcue.fade-split .split-right {
right: 0;
justify-content: flex-start;
}
.scrollcue.fade-split.is-inactive .split-left {
transform: translateX(-100%);
opacity: 0;
}
.scrollcue.fade-split.is-inactive .split-right {
transform: translateX(100%);
opacity: 0;
}
.scrollcue.fade-split.cue-in .split-left {
animation: fadeSplitLeft 1s forwards;
}
.scrollcue.fade-split.cue-in .split-right {
animation: fadeSplitRight 1s forwards;
}
fadeSplitLeft {
0% {
transform: translateX(-100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
fadeSplitRight {
0% {
transform: translateX(100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
`
}
};
// Default options
const defaults = {
rootMargin: '0px',
threshold: 0.2,
duration: 800,
delay: 0,
easing: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
once: true,
useRAF: true
};
class ScrollCue {
constructor(options = {}) {
this.options = Object.assign({}, defaults, options);
this.elements = [];
this.observer = null;
this.initialized = false;
this.usedAnimations = new Set();
}
init() {
if (this.initialized) return;
// Find all elements with scrollcue class
const elements = document.querySelectorAll('.scrollcue');
// Collect used animations
elements.forEach(element => {
const animType = element.dataset.cue || 'fade-in';
this.usedAnimations.add(animType);
// Special handling for fade-split
if (animType === 'fade-split') {
const content = element.innerHTML;
element.innerHTML = `
<div class="split-left">${content}</div>
<div class="split-right">${content}</div>
`;
}
});
// Inject only used animations
this.injectAnimations();
// Initialize Intersection Observer
this.observer = new IntersectionObserver(
(entries) => this.handleIntersection(entries),
{
rootMargin: this.options.rootMargin,
threshold: this.options.threshold
}
);
// Add elements
elements.forEach(element => {
this.addElement(element);
});
this.initialized = true;
return this;
}
injectAnimations() {
const styleEl = document.createElement('style');
// Add only used animations
const css = Array.from(this.usedAnimations)
.map(anim => animations[anim]?.css)
.filter(Boolean)
.join('\n');
styleEl.textContent = css;
document.head.appendChild(styleEl);
}
addElement(element) {
if (!element.classList.contains('scrollcue')) {
element.classList.add('scrollcue');
}
if (!element.classList.contains('is-inactive')) {
element.classList.add('is-inactive');
}
this.elements.push(element);
this.observer.observe(element);
return this;
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.animateElement(entry.target);
if (this.options.once) {
this.observer.unobserve(entry.target);
}
}
});
}
animateElement(element) {
const animationType = element.dataset.cue || 'fade-in';
const delay = parseInt(element.dataset.delay || this.options.delay, 10);
const duration = parseInt(element.dataset.duration || this.options.duration, 10);
element.style.animationDuration = `${duration}ms`;
element.style.animationDelay = `${delay}ms`;
element.style.animationTimingFunction = this.options.easing;
if (this.options.useRAF) {
requestAnimationFrame(() => {
this.startElementAnimation(element, animationType);
});
} else {
setTimeout(() => {
this.startElementAnimation(element, animationType);
}, 10);
}
}
startElementAnimation(element, animationType) {
element.classList.remove('is-inactive');
element.classList.add('cue-in', animationType);
element.dispatchEvent(new CustomEvent('scrollcue:start', {
bubbles: true,
detail: { element }
}));
}
refresh() {
if (!this.initialized) return this;
this.elements.forEach(element => {
this.observer.unobserve(element);
});
this.elements = [];
this.init();
return this;
}
destroy() {
if (!this.initialized) return;
this.elements.forEach(element => {
this.observer.unobserve(element);
});
this.elements = [];
this.observer.disconnect();
this.initialized = false;
}
}
// Export for different module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = ScrollCue;
} else if (typeof define === 'function' && define.amd) {
define([], function() { return ScrollCue; });
} else {
window.ScrollCue = ScrollCue;
}
// Auto-initialize by default unless explicitly disabled
if (!document.currentScript?.hasAttribute('data-no-auto-init')) {
new ScrollCue().init();
}
})();