gsap-dattebayo
Version:
The ultimate GSAP-powered scroll animation library - Simple as AOS, powerful as GSAP. Modern animations for 2025-2026 web trends.
736 lines (652 loc) • 20 kB
text/typescript
/**
* Auto-initialization system
* Automatically detects and animates elements with data attributes
*/
import {
gsap,
ScrollTrigger,
initGSAP,
refreshScrollTriggers,
} from "./utils/gsap-config";
import {
getAnimatedElements,
parseAttributes,
isValidAnimation,
} from "./utils/data-attributes";
import {
ready,
debounce,
isBelowViewport,
getOptimalScrollTriggerStart,
} from "./utils/helpers";
import { mergeConfig, type DattebayoDefaults } from "./utils/defaults";
// Core animations
import * as coreAnimations from "./core";
// Text animations
import * as textAnimations from "./text";
// Scroll animations
import * as scrollAnimations from "./scroll";
let isAutoInitialized = false;
let globalConfig: Partial<DattebayoDefaults> = {};
/**
* Initialize auto-detection and animation
*/
export function autoInit(config: Partial<DattebayoDefaults> = {}): void {
if (isAutoInitialized) return;
// Initialize GSAP
initGSAP();
// Store global config
globalConfig = config;
// Wait for DOM ready
ready(() => {
animateElements();
setupObserver();
setupResizeHandler();
// Force ScrollTrigger to check current scroll position
// This ensures animations trigger even if page is reloaded while scrolled
requestAnimationFrame(() => {
ScrollTrigger.refresh(true);
});
});
isAutoInitialized = true;
}
/**
* Animate all elements with data attributes
*/
function animateElements(root: Document | HTMLElement = document): void {
const elements = getAnimatedElements(root);
elements.forEach((element) => {
const attrs = parseAttributes(element);
const animation = attrs.animation;
// Check if element has parallax attribute
const hasParallax = attrs.parallax !== undefined;
// For hover-only or trigger-only elements, we don't need a main animation
const hasHover = attrs.hoverAnimation;
const hasTriggerMode =
attrs.triggerMode === "hover" || attrs.triggerMode === "click";
// Skip validation if it's a hover-only, click-only, or parallax-only element
if (!animation && !hasHover && !hasTriggerMode && !hasParallax) {
return;
}
if (animation && !isValidAnimation(animation)) {
console.warn(`[GSAP Dattebayo] Invalid animation: ${animation}`);
return;
}
// Merge element config with global config
const config = mergeConfig({
...globalConfig,
...attrs,
});
// Execute animation
executeAnimation(element, animation || "", config);
});
}
/**
* Execute animation based on trigger mode
*/
function executeAnimation(
element: HTMLElement,
animationName: string,
config: DattebayoDefaults
): void {
// Special cases: animations that don't take a target element
const noTargetAnimations = ["scrollProgress", "circularProgress"];
if (noTargetAnimations.includes(animationName)) {
const specialAnimations: Record<string, (options: any) => any> = {
scrollProgress: scrollAnimations.scrollProgress,
circularProgress: scrollAnimations.circularProgress,
};
const animationFn = specialAnimations[animationName];
if (animationFn) {
try {
animationFn(config);
} catch (error) {
console.error(
`[GSAP Dattebayo] Error executing ${animationName}:`,
error
);
}
}
return;
}
// Check if it's hover-only (data-gsap-hover without data-gsap)
if (config.hoverAnimation && !animationName) {
executeOnHover(element, config.hoverAnimation, config);
return;
}
// Check if it's parallax-only (data-gsap-parallax without data-gsap)
if (config.parallax !== undefined && !animationName) {
executeParallax(element, config);
return;
}
// Route based on trigger mode
const triggerMode = config.triggerMode || "scroll";
switch (triggerMode) {
case "load":
executeImmediately(element, animationName, config);
break;
case "scroll":
executeOnScroll(element, animationName, config);
break;
case "hover":
executeOnHover(element, animationName, config);
break;
case "click":
executeOnClick(element, animationName, config);
break;
default:
executeOnScroll(element, animationName, config);
}
}
/**
* Execute animation immediately (load mode)
*/
function executeImmediately(
element: HTMLElement,
animationName: string,
config: DattebayoDefaults
): void {
const animationFn = getAnimationFunction(animationName);
if (!animationFn) return;
try {
animationFn(element, config);
} catch (error) {
console.error(`[GSAP Dattebayo] Error executing ${animationName}:`, error);
}
}
/**
* Execute animation on scroll (AOS-like behavior)
*/
function executeOnScroll(
element: HTMLElement,
animationName: string,
config: DattebayoDefaults
): void {
const animationFn = getAnimationFunction(animationName);
if (!animationFn) return;
// Get initial state based on animation type
const initialState = getInitialState(animationName, config);
// Get optimal scroll trigger start position (adjusts for bottom elements)
const optimalStart = getOptimalScrollTriggerStart(
element,
config.start || "top 100%"
);
// Check if option mode is enabled
if (config.option) {
const finalState = getFinalState(animationName, config);
const duration = config.duration || 1;
const ease = config.ease || "power2.out";
try {
switch (config.option) {
case "reverse":
// Reverse: ping-pong forward/backward
gsap.set(element, initialState);
ScrollTrigger.create({
trigger: element,
start: optimalStart,
end: config.end || "bottom 0%",
markers: config.markers || false,
onEnter: () => {
gsap.to(element, {
...finalState,
duration,
ease,
force3D: true,
});
},
onLeaveBack: () => {
gsap.to(element, {
...initialState,
duration,
ease,
force3D: true,
});
},
});
break;
case "scrub":
// Scrub: tied to scroll position with scrub
gsap
.timeline({
scrollTrigger: {
trigger: element,
start: optimalStart,
end: config.end || "bottom 0%",
markers: config.markers || false,
scrub: true,
},
})
.fromTo(element, initialState, {
...finalState,
ease,
force3D: true,
});
break;
case "replay":
// Replay: plays without reverse each time
ScrollTrigger.create({
trigger: element,
start: optimalStart,
end: config.end || "bottom 0%",
markers: config.markers || false,
onEnter: () => {
gsap.set(element, initialState);
gsap.to(element, {
...finalState,
duration,
ease,
force3D: true,
});
},
onEnterBack: () => {
gsap.set(element, initialState);
gsap.to(element, {
...finalState,
duration,
ease,
force3D: true,
});
},
});
break;
}
} catch (error) {
console.error(
`[GSAP Dattebayo] Error executing ${animationName} with option ${config.option}:`,
error
);
}
} else {
// AOS behavior: play once
// Set initial state for AOS mode
gsap.set(element, initialState);
// AOS behavior: play once
ScrollTrigger.create({
trigger: element,
start: optimalStart,
end: config.end || "bottom 0%",
markers: config.markers || false,
once: config.once !== false, // Default to true for AOS behavior
// Check if element is already scrolled past on page load
onRefresh: (self) => {
if (self.progress === 1) {
// Element already visible - set to final state immediately without animation
gsap.set(element, { clearProps: "all" });
}
},
onEnter: () => {
try {
// Clear the initial state properties so gsap.from() can animate properly
gsap.set(element, { clearProps: "all" });
// Now execute the animation
animationFn(element, config);
} catch (error) {
console.error(
`[GSAP Dattebayo] Error executing ${animationName}:`,
error
);
}
},
});
}
}
/**
* Create hover-specific animation (using gsap.to instead of gsap.from)
*/
function createHoverAnimation(
element: HTMLElement,
animationName: string,
config: DattebayoDefaults
): gsap.core.Tween | gsap.core.Timeline {
const duration = config.duration || 0.5;
const ease = config.ease || "power2.out";
// Map animation names to hover-friendly .to() animations
const hoverAnimations: Record<string, gsap.TweenVars> = {
// Zoom animations
zoomIn: { scale: 1.1, duration, ease },
zoomOut: { scale: 0.9, duration, ease },
elasticZoom: {
scale: 1.15,
duration: duration * 1.5,
ease: "elastic.out(1, 0.3)",
},
// Fade animations
fadeIn: { opacity: 1, duration, ease },
fadeOut: { opacity: 0.5, duration, ease },
// Rotate animations
rotateIn: { rotation: 5, duration, ease },
spinIn: { rotation: 360, duration: duration * 2, ease },
// Blur animations
blurToFocus: { filter: "blur(0px)", duration, ease },
focusToBlur: { filter: "blur(5px)", duration, ease },
};
const animProps = hoverAnimations[animationName];
if (animProps) {
return gsap.to(element, animProps);
}
// Fallback: slight scale up for unknown animations
return gsap.to(element, {
scale: 1.05,
duration,
ease,
});
}
/**
* Execute animation on hover
*/
function executeOnHover(
element: HTMLElement,
animationName: string,
config: DattebayoDefaults
): void {
// Use hoverAnimation if specified, otherwise use main animation
const hoverAnim = config.hoverAnimation || animationName;
const leaveAnim = config.hoverLeaveAnimation;
let timeline: any = null;
element.addEventListener("mouseenter", () => {
// Kill any existing animation
if (timeline) {
timeline.kill();
}
try {
// Create hover animation using gsap.to()
timeline = createHoverAnimation(element, hoverAnim, config);
} catch (error) {
console.error(`[GSAP Dattebayo] Error executing ${hoverAnim}:`, error);
}
});
element.addEventListener("mouseleave", () => {
// Kill any existing animation
if (timeline) {
timeline.kill();
}
// If hoverLeaveAnimation is specified, play it
if (leaveAnim) {
try {
timeline = createHoverAnimation(element, leaveAnim, config);
} catch (error) {
console.error(`[GSAP Dattebayo] Error executing ${leaveAnim}:`, error);
}
} else {
// Otherwise, animate back to normal state
timeline = gsap.to(element, {
scale: 1,
x: 0,
y: 0,
rotation: 0,
opacity: 1,
filter: "none",
duration: 0.3,
ease: "power2.out",
});
}
});
}
/**
* Execute animation on click
*/
function executeOnClick(
element: HTMLElement,
animationName: string,
config: DattebayoDefaults
): void {
const animationFn = getAnimationFunction(animationName);
if (!animationFn) return;
element.style.cursor = "pointer";
element.addEventListener("click", () => {
try {
// Always reset element to normal state before animating
gsap.set(element, { clearProps: "all" });
// Execute animation every time (ignore 'once' for click mode)
animationFn(element, config);
} catch (error) {
console.error(
`[GSAP Dattebayo] Error executing ${animationName}:`,
error
);
}
});
}
/**
* Execute parallax effect (data-gsap-parallax)
*/
function executeParallax(
element: HTMLElement,
config: DattebayoDefaults
): void {
try {
// Default to basic parallax function with the speed from data-gsap-parallax
scrollAnimations.parallax(element, {
speed: config.parallax,
scrub: config.scrub !== undefined ? config.scrub : true,
start: config.start,
end: config.end,
markers: config.markers,
});
} catch (error) {
console.error(
`[GSAP Dattebayo] Error executing parallax:`,
error
);
}
}
/**
* Get animation function by name
*/
function getAnimationFunction(
animationName: string
): ((target: HTMLElement, options: any) => any) | null {
// Animation mapping
const animationMap: Record<
string,
(target: HTMLElement, options: any) => any
> = {
// Core - Fade
fade: coreAnimations.fadeIn,
fadeIn: coreAnimations.fadeIn,
fadeOut: coreAnimations.fadeOut,
fadeUp: coreAnimations.fadeUp,
fadeDown: coreAnimations.fadeDown,
fadeLeft: coreAnimations.fadeLeft,
fadeRight: coreAnimations.fadeRight,
// Core - Slide
slide: coreAnimations.slideInUp,
slideInUp: coreAnimations.slideInUp,
slideInDown: coreAnimations.slideInDown,
slideInLeft: coreAnimations.slideInLeft,
slideInRight: coreAnimations.slideInRight,
slideOutUp: coreAnimations.slideOutUp,
slideOutDown: coreAnimations.slideOutDown,
// Core - Zoom
zoom: coreAnimations.zoomIn,
zoomIn: coreAnimations.zoomIn,
zoomOut: coreAnimations.zoomOut,
zoomInUp: coreAnimations.zoomInUp,
zoomInDown: coreAnimations.zoomInDown,
elasticZoom: coreAnimations.elasticZoom,
// Core - Rotate
rotate: coreAnimations.rotateIn,
rotateIn: coreAnimations.rotateIn,
rotateOut: coreAnimations.rotateOut,
flipInX: coreAnimations.flipInX,
flipInY: coreAnimations.flipInY,
spinIn: coreAnimations.spinIn,
// Core - Blur
blur: coreAnimations.blurToFocus,
blurIn: coreAnimations.blurToFocus,
blurToFocus: coreAnimations.blurToFocus,
focusToBlur: coreAnimations.focusToBlur,
blurInUp: coreAnimations.blurInUp,
blurZoom: coreAnimations.blurZoom,
// Text - Character
charReveal: textAnimations.charReveal,
charFadeUp: textAnimations.charFadeUp,
charWave: textAnimations.charWave,
charElastic: textAnimations.charElastic,
charBlur: textAnimations.charBlur,
// Text - Word
wordReveal: textAnimations.wordReveal,
wordScaleIn: textAnimations.wordScaleIn,
wordRotateIn: textAnimations.wordRotateIn,
wordSlideAlternate: textAnimations.wordSlideAlternate,
// Text - Line
lineReveal: textAnimations.lineReveal,
lineClipReveal: textAnimations.lineClipReveal,
lineSlideAlternate: textAnimations.lineSlideAlternate,
lineScaleReveal: textAnimations.lineScaleReveal,
// Text - Scramble
scramble: textAnimations.scrambleReveal,
scrambleReveal: textAnimations.scrambleReveal,
glitch: textAnimations.glitchReveal,
glitchReveal: textAnimations.glitchReveal,
matrix: textAnimations.matrixReveal,
matrixReveal: textAnimations.matrixReveal,
// Scroll - Parallax
parallax: scrollAnimations.parallax,
parallaxSpeed: scrollAnimations.parallaxSpeed,
parallaxLayers: scrollAnimations.parallaxLayers,
parallax3D: scrollAnimations.parallax3D,
parallaxRotate: scrollAnimations.parallaxRotate,
// Scroll - Reveal
scrollReveal: scrollAnimations.scrollReveal,
batchScrollReveal: scrollAnimations.batchScrollReveal,
pinSection: scrollAnimations.pinSection,
scrubAnimation: scrollAnimations.scrubAnimation,
horizontalScroll: scrollAnimations.horizontalScroll,
// Scroll - Progress (with target)
sectionProgress: scrollAnimations.sectionProgress,
scrollPercentage: scrollAnimations.scrollPercentage,
};
const animationFn = animationMap[animationName];
if (!animationFn) {
console.warn(
`[GSAP Dattebayo] Animation function not found: ${animationName}`
);
return null;
}
return animationFn;
}
/**
* Get initial state for animation (before scroll trigger)
*/
function getInitialState(
animationName: string,
config: DattebayoDefaults
): gsap.TweenVars {
const distance = config.distance || 50;
// Map animation types to their initial states
const stateMap: Record<string, gsap.TweenVars> = {
// Fade animations
fade: { opacity: 0 },
fadeIn: { opacity: 0 },
fadeUp: { opacity: 0, y: distance },
fadeDown: { opacity: 0, y: -distance },
fadeLeft: { opacity: 0, x: distance },
fadeRight: { opacity: 0, x: -distance },
// Slide animations
slideInUp: { opacity: 0, y: distance },
slideInDown: { opacity: 0, y: -distance },
slideInLeft: { opacity: 0, x: -distance },
slideInRight: { opacity: 0, x: distance },
// Zoom animations
zoom: { opacity: 0, scale: 0.5 },
zoomIn: { opacity: 0, scale: 0.5 },
zoomOut: { opacity: 0, scale: 1.5 },
zoomInUp: { opacity: 0, scale: 0.5, y: distance },
zoomInDown: { opacity: 0, scale: 0.5, y: -distance },
elasticZoom: { opacity: 0, scale: 0 },
// Rotate animations
rotate: { opacity: 0, rotation: -180 },
rotateIn: { opacity: 0, rotation: -180 },
rotateOut: { opacity: 1, rotation: 0 },
flipInX: { opacity: 0, rotationX: -90 },
flipInY: { opacity: 0, rotationY: -90 },
spinIn: { opacity: 0, rotation: 360 },
// Blur animations
blur: { opacity: 0, filter: "blur(20px)" },
blurIn: { opacity: 0, filter: "blur(20px)" },
blurToFocus: { opacity: 0, filter: "blur(20px)" },
blurInUp: { opacity: 0, filter: "blur(20px)", y: distance },
blurZoom: { opacity: 0, filter: "blur(20px)", scale: 0.5 },
// Text animations - just hide them
charReveal: { opacity: 0 },
charFadeUp: { opacity: 0 },
charWave: { opacity: 0 },
charElastic: { opacity: 0 },
charBlur: { opacity: 0 },
wordReveal: { opacity: 0 },
wordScaleIn: { opacity: 0 },
wordRotateIn: { opacity: 0 },
wordSlideAlternate: { opacity: 0 },
lineReveal: { opacity: 0 },
lineClipReveal: { opacity: 0 },
lineSlideAlternate: { opacity: 0 },
lineScaleReveal: { opacity: 0 },
scramble: { opacity: 0 },
scrambleReveal: { opacity: 0 },
glitch: { opacity: 0 },
glitchReveal: { opacity: 0 },
matrix: { opacity: 0 },
matrixReveal: { opacity: 0 },
};
return stateMap[animationName] || { opacity: 0 };
}
/**
* Get final state for animation (normal/visible state)
*/
function getFinalState(
animationName: string,
config: DattebayoDefaults
): gsap.TweenVars {
// Final state is always normal/visible - just properties, no timing
return {
opacity: 1,
x: 0,
y: 0,
scale: 1,
rotation: 0,
rotationX: 0,
rotationY: 0,
filter: "none",
};
}
/**
* Setup MutationObserver for dynamic content
*/
function setupObserver(): void {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
animateElements(node as HTMLElement);
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
/**
* Setup resize handler to refresh ScrollTriggers
*/
function setupResizeHandler(): void {
const handleResize = debounce(() => {
refreshScrollTriggers();
}, 250);
window.addEventListener("resize", handleResize);
}
/**
* Manually refresh all animations
*/
export function refresh(): void {
refreshScrollTriggers();
}
/**
* Destroy auto-init
*/
export function destroy(): void {
isAutoInitialized = false;
globalConfig = {};
}