gsap-dattebayo
Version:
The ultimate GSAP-powered scroll animation library - Simple as AOS, powerful as GSAP. Modern animations for 2025-2026 web trends.
465 lines (459 loc) • 14.9 kB
JavaScript
/*!
* GSAP Dattebayo v0.1.0-alpha.1
* The ultimate GSAP-powered scroll animation library
* https://github.com/serdjan/gsap-dattebayo
*
* Copyright 2025 GSAP Dattebayo
* Released under the MIT License
*/
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
import 'gsap/SplitText';
/**
* Helper utilities for GSAP Dattebayo
*/
/**
* Convert selector or element to array of HTMLElements
*/
function toArray(target) {
if (typeof target === 'string') {
return Array.from(document.querySelectorAll(target));
}
if (target instanceof HTMLElement) {
return [target];
}
if (target instanceof NodeList) {
return Array.from(target);
}
if (Array.isArray(target)) {
return target;
}
return [];
}
/**
* Parallax scroll effects
* Uses GSAP ScrollTrigger for smooth parallax animations
*/
/**
* Basic parallax - Elements move at different speeds while scrolling
*/
function parallax(target, options = {}) {
const { speed = 0.5, direction = 'vertical', scrub = true, start = 'top bottom', end = 'bottom top', markers = false } = options;
const elements = toArray(target);
const triggers = [];
elements.forEach(element => {
const yMove = direction === 'vertical' ? (speed - 1) * 100 : 0;
const xMove = direction === 'horizontal' ? (speed - 1) * 100 : 0;
const trigger = ScrollTrigger.create({
trigger: element,
start,
end,
scrub,
markers,
onUpdate: self => {
gsap.to(element, {
y: yMove * self.progress,
x: xMove * self.progress,
force3D: true,
overwrite: 'auto'
});
}
});
triggers.push(trigger);
});
return triggers;
}
/**
* Parallax speed - Simple speed-based parallax (like Locomotive Scroll)
*/
function parallaxSpeed(target, options = {}) {
const { speed = 1, scrub = 1, markers = false } = options;
const elements = toArray(target);
const triggers = [];
elements.forEach(element => {
gsap.to(element, {
y: () => -(element.offsetHeight * (1 - speed)),
ease: 'none',
scrollTrigger: {
trigger: element,
start: 'top bottom',
end: 'bottom top',
scrub,
markers,
invalidateOnRefresh: true
}
});
const st = ScrollTrigger.getById(element.dataset.scrollTriggerId || '');
if (st)
triggers.push(st);
});
return triggers;
}
/**
* Parallax layers - Multi-layer parallax effect
*/
function parallaxLayers(container, options = {}) {
const { scrub = 1, markers = false } = options;
const containerElement = typeof container === 'string'
? document.querySelector(container)
: container;
if (!containerElement)
return [];
const layers = toArray(containerElement.querySelectorAll('[data-speed]'));
const triggers = [];
layers.forEach(layer => {
const speed = parseFloat(layer.dataset.speed || '1');
gsap.to(layer, {
y: () => -(layer.clientHeight * (1 - speed)),
ease: 'none',
scrollTrigger: {
trigger: containerElement,
start: 'top bottom',
end: 'bottom top',
scrub,
markers,
invalidateOnRefresh: true
}
});
const st = ScrollTrigger.getById(layer.dataset.scrollTriggerId || '');
if (st)
triggers.push(st);
});
return triggers;
}
/**
* Parallax 3D - Perspective-based parallax
*/
function parallax3D(target, options = {}) {
const { speed = 0.5, scrub = 1, markers = false } = options;
const elements = toArray(target);
const triggers = [];
elements.forEach(element => {
gsap.set(element, {
transformPerspective: 1000,
transformStyle: 'preserve-3d'
});
gsap.to(element, {
z: () => 200 * (1 - speed),
rotationX: () => 10 * (1 - speed),
ease: 'none',
scrollTrigger: {
trigger: element,
start: 'top bottom',
end: 'bottom top',
scrub,
markers
}
});
const st = ScrollTrigger.getById(element.dataset.scrollTriggerId || '');
if (st)
triggers.push(st);
});
return triggers;
}
/**
* Parallax rotate - Rotation-based parallax
*/
function parallaxRotate(target, options = {}) {
const { speed = 1, scrub = 1, markers = false } = options;
const elements = toArray(target);
const triggers = [];
elements.forEach(element => {
gsap.to(element, {
rotation: () => 360 * speed,
ease: 'none',
scrollTrigger: {
trigger: element,
start: 'top bottom',
end: 'bottom top',
scrub,
markers
}
});
const st = ScrollTrigger.getById(element.dataset.scrollTriggerId || '');
if (st)
triggers.push(st);
});
return triggers;
}
/**
* Scroll reveal animations
* Elements animate when they enter the viewport
*/
/**
* Scroll reveal - Animate elements as they enter viewport
*/
function scrollReveal(target, options = {}) {
const { animation = 'fadeUp', duration = 1, ease = 'power2.out', start = 'top 80%', end = 'bottom 20%', once = false, markers = false, distance = 50, stagger = 0 } = options;
const elements = toArray(target);
const triggers = [];
// Animation presets
const animations = {
fade: { opacity: 0 },
fadeUp: { opacity: 0, y: distance },
fadeDown: { opacity: 0, y: -distance },
fadeLeft: { opacity: 0, x: distance },
fadeRight: { opacity: 0, x: -distance },
zoom: { opacity: 0, scale: 0.5 },
slide: { x: -100 }
};
const fromVars = animations[animation] || animations.fadeUp;
elements.forEach((element, i) => {
gsap.from(element, {
...fromVars,
duration,
ease,
delay: i * stagger,
scrollTrigger: {
trigger: element,
start,
end,
markers,
once,
toggleActions: once ? 'play none none none' : 'play none none reverse'
}
});
const st = ScrollTrigger.getById(element.dataset.scrollTriggerId || '');
if (st)
triggers.push(st);
});
return triggers;
}
/**
* Batch scroll reveal - Optimized for many elements
*/
function batchScrollReveal(target, options = {}) {
const { animation = 'fadeUp', duration = 1, ease = 'power2.out', start = 'top 80%', stagger = 0.1, distance = 50, once = false } = options;
const animations = {
fade: { opacity: 0 },
fadeUp: { opacity: 0, y: distance },
fadeDown: { opacity: 0, y: -distance },
fadeLeft: { opacity: 0, x: distance },
fadeRight: { opacity: 0, x: -distance },
zoom: { opacity: 0, scale: 0.5 }
};
const fromVars = animations[animation] || animations.fadeUp;
return ScrollTrigger.batch(target, {
onEnter: batch => {
gsap.from(batch, {
...fromVars,
duration,
ease,
stagger,
overwrite: 'auto'
});
},
onLeaveBack: !once ? batch => {
gsap.to(batch, {
...fromVars,
duration: duration * 0.5,
overwrite: 'auto'
});
} : undefined,
start,
once
});
}
/**
* Pin section - Pin element while scrolling
*/
function pinSection(target, options = {}) {
const { start = 'top top', end = '+=100%', pin = true, scrub = false, markers = false } = options;
const element = typeof target === 'string'
? document.querySelector(target)
: target;
if (!element)
throw new Error('Pin target not found');
return ScrollTrigger.create({
trigger: element,
start,
end,
pin,
scrub,
markers
});
}
/**
* Scrub animation - Animation tied to scroll position
*/
function scrubAnimation(target, animationVars, options = {}) {
const { start = 'top bottom', end = 'bottom top', scrub = 1, markers = false } = options;
const elements = toArray(target);
const tweens = [];
elements.forEach(element => {
const tween = gsap.to(element, {
...animationVars,
ease: 'none',
scrollTrigger: {
trigger: element,
start,
end,
scrub,
markers
}
});
tweens.push(tween);
});
return tweens;
}
/**
* Horizontal scroll - Horizontal scrolling section
*/
function horizontalScroll(container, options = {}) {
const { scrub = 1, pin = true, markers = false } = options;
const containerElement = typeof container === 'string'
? document.querySelector(container)
: container;
if (!containerElement)
throw new Error('Horizontal scroll container not found');
const sections = containerElement.querySelectorAll('[data-scroll-section]');
const totalWidth = Array.from(sections).reduce((acc, section) => acc + section.offsetWidth, 0);
return gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: 'none',
scrollTrigger: {
trigger: containerElement,
pin,
scrub,
markers,
end: () => `+=${totalWidth}`,
invalidateOnRefresh: true
}
}).scrollTrigger;
}
/**
* Scroll progress indicators
* Visual feedback for scroll position
*/
/**
* Scroll progress bar - Page-level progress indicator
*/
function scrollProgress(options = {}) {
const { direction = 'horizontal', thickness = 4, color = '#3b82f6', position = 'top' } = options;
// Create progress bar element
const progressBar = document.createElement('div');
progressBar.className = 'gsap-scroll-progress';
// Style the progress bar
const isHorizontal = direction === 'horizontal';
Object.assign(progressBar.style, {
position: 'fixed',
[position]: '0',
[isHorizontal ? 'left' : 'top']: '0',
[isHorizontal ? 'width' : 'height']: '0%',
[isHorizontal ? 'height' : 'width']: `${thickness}px`,
backgroundColor: color,
zIndex: '9999',
transformOrigin: '0 0'
});
document.body.appendChild(progressBar);
// Animate on scroll
gsap.to(progressBar, {
[isHorizontal ? 'width' : 'height']: '100%',
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: true
}
});
return progressBar;
}
/**
* Section progress - Progress for specific section
*/
function sectionProgress(target, options = {}) {
const { direction = 'vertical', thickness = 4, color = '#3b82f6', position = 'left' } = options;
const targetElement = typeof target === 'string'
? document.querySelector(target)
: target;
if (!targetElement)
throw new Error('Section progress target not found');
// Create progress indicator
const indicator = document.createElement('div');
indicator.className = 'gsap-section-progress';
const isVertical = direction === 'vertical';
Object.assign(indicator.style, {
position: 'absolute',
[position]: '0',
[isVertical ? 'top' : 'left']: '0',
[isVertical ? 'height' : 'width']: '0%',
[isVertical ? 'width' : 'height']: `${thickness}px`,
backgroundColor: color,
transformOrigin: '0 0'
});
targetElement.style.position = 'relative';
targetElement.appendChild(indicator);
// Animate based on section scroll
gsap.to(indicator, {
[isVertical ? 'height' : 'width']: '100%',
ease: 'none',
scrollTrigger: {
trigger: targetElement,
start: 'top top',
end: 'bottom bottom',
scrub: true
}
});
return indicator;
}
/**
* Circular progress - Circular scroll indicator
*/
function circularProgress(options = {}) {
const { size = 60, thickness = 4, color = '#3b82f6', position = 'bottom' } = options;
// Create SVG circle
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(size));
svg.setAttribute('height', String(size));
svg.style.position = 'fixed';
svg.style[position] = '20px';
svg.style.right = '20px';
svg.style.zIndex = '9999';
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
const radius = (size - thickness) / 2;
const circumference = 2 * Math.PI * radius;
circle.setAttribute('cx', String(size / 2));
circle.setAttribute('cy', String(size / 2));
circle.setAttribute('r', String(radius));
circle.setAttribute('fill', 'none');
circle.setAttribute('stroke', color);
circle.setAttribute('stroke-width', String(thickness));
circle.setAttribute('stroke-dasharray', String(circumference));
circle.setAttribute('stroke-dashoffset', String(circumference));
circle.setAttribute('transform', `rotate(-90 ${size / 2} ${size / 2})`);
svg.appendChild(circle);
document.body.appendChild(svg);
// Animate circle
gsap.to(circle, {
strokeDashoffset: 0,
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: true
}
});
return svg;
}
/**
* Scroll percentage - Update element with scroll percentage
*/
function scrollPercentage(target, options = {}) {
const targetElement = typeof target === 'string'
? document.querySelector(target)
: target;
if (!targetElement)
throw new Error('Scroll percentage target not found');
return ScrollTrigger.create({
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
onUpdate: self => {
const percentage = Math.round(self.progress * 100);
targetElement.textContent = `${percentage}%`;
}
});
}
export { batchScrollReveal, circularProgress, horizontalScroll, parallax, parallax3D, parallaxLayers, parallaxRotate, parallaxSpeed, pinSection, scrollPercentage, scrollProgress, scrollReveal, scrubAnimation, sectionProgress };