// resources/js/scrollfx/Tween.js export const Easing = { linear: t => t, easeInQuad: t => t * t, easeOutQuad: t => t * (2 - t), easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), }; function interpolate(start, end, t) { return start + (end - start) * t; } function parseTransform(transform) { const result = { translateY: 0, scale: 1, rotate: 0 }; if (!transform || transform === 'none') return result; const translateMatch = transform.match(/translateY\((-?\d+(?:\.\d+)?)px\)/); const scaleMatch = transform.match(/scale\((-?\d+(?:\.\d+)?)\)/); const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/); if (translateMatch) result.translateY = parseFloat(translateMatch[1]); if (scaleMatch) result.scale = parseFloat(scaleMatch[1]); if (rotateMatch) result.rotate = parseFloat(rotateMatch[1]); return result; } export function tweenTo(el, props = {}, duration = 300, easing = Easing.linear) { const start = performance.now(); const initial = {}; const currentTransform = parseTransform(getComputedStyle(el).transform); for (const key in props) { if (['translateY', 'scale', 'rotate'].includes(key)) { initial[key] = currentTransform[key]; } else { const current = parseFloat(getComputedStyle(el)[key]) || 0; initial[key] = current; } } function animate(now) { const t = Math.min((now - start) / duration, 1); const eased = easing(t); let transformParts = []; for (const key in props) { const startValue = initial[key]; const endValue = parseFloat(props[key]); const current = interpolate(startValue, endValue, eased); if (key === 'translateY') transformParts.push(`translateY(${current}px)`); else if (key === 'scale') transformParts.push(`scale(${current})`); else if (key === 'rotate') transformParts.push(`rotate(${current}deg)`); else el.style[key] = current + (key === 'opacity' ? '' : 'px'); } if (transformParts.length > 0) { el.style.transform = transformParts.join(' '); } if (t < 1) requestAnimationFrame(animate); } requestAnimationFrame(animate); } export function timeline(steps = []) { let index = 0; function runNext() { if (index >= steps.length) return; const { el, props, duration, easing = Easing.linear, delay = 0 } = steps[index++]; setTimeout(() => { tweenTo(el, props, duration, easing); setTimeout(runNext, duration); }, delay); } runNext(); } export function tweenFromTo(el, from = {}, to = {}, duration = 300, easing = Easing.linear) { for (const key in from) { if (['translateY', 'scale', 'rotate'].includes(key)) { el.style.transform = `${key}(${from[key]}${key === 'rotate' ? 'deg' : key === 'translateY' ? 'px' : ''})`; } else { el.style[key] = from[key] + (key === 'opacity' ? '' : 'px'); } } tweenTo(el, to, duration, easing); } // === Utility Animations === export function fadeIn(el, duration = 400, easing = Easing.easeOutQuad) { el.classList.remove('fade-out'); el.classList.add('fade-in'); tweenTo(el, { opacity: 1 }, duration, easing); } export function fadeOut(el, duration = 400, easing = Easing.easeInQuad) { el.classList.remove('fade-in'); el.classList.add('fade-out'); tweenTo(el, { opacity: 0 }, duration, easing); } export function zoomIn(el, duration = 500, easing = Easing.easeOutQuad) { el.classList.add('zoom-in'); tweenTo(el, { opacity: 1, scale: 1 }, duration, easing); } export function zoomOut(el, duration = 500, easing = Easing.easeInQuad) { el.classList.remove('zoom-in'); tweenTo(el, { opacity: 0, scale: 0.8 }, duration, easing); } export function triggerCssAnimation(el, className, duration = 1000) { el.classList.add(className); setTimeout(() => { el.classList.remove(className); }, duration); } // === ScrollTrigger Presets === import { createTrigger } from './index.js'; export function fadeScrollTrigger(selector, options = {}) { const elements = document.querySelectorAll(selector); elements.forEach(el => { createTrigger({ element: el, start: options.start || 'top 80%', end: options.end || 'bottom 20%', onEnter: () => fadeIn(el), onLeave: () => fadeOut(el) }); }); } export function zoomScrollTrigger(selector, options = {}) { const elements = document.querySelectorAll(selector); elements.forEach(el => { createTrigger({ element: el, start: options.start || 'top 80%', end: options.end || 'bottom 20%', onEnter: () => zoomIn(el), onLeave: () => zoomOut(el) }); }); } export function fixedZoomScrollTrigger(selector, options = {}) { const elements = document.querySelectorAll(selector); elements.forEach(el => { el.style.willChange = 'transform, opacity'; el.style.opacity = 0; el.style.transform = 'scale(0.8)'; createTrigger({ element: el, start: options.start || 'top 100%', end: options.end || 'top 40%', onEnter: () => zoomIn(el, 600), onLeave: () => zoomOut(el, 400) }); }); }