178 lines
5.4 KiB
JavaScript
178 lines
5.4 KiB
JavaScript
// 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)
|
|
});
|
|
});
|
|
}
|