anitimejs
Version:
Thư viện xử lý chuỗi số và thời gian trong JavaScript/Typescript
1,014 lines (1,013 loc) • 41.1 kB
JavaScript
;
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.effects = exports.createTransform = exports.sequence = exports.physics = exports.spring = exports.stagger = exports.timeline = exports.animate = exports.easingFunctions = void 0;
// Enhanced easing functions
exports.easingFunctions = {
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),
easeInCubic: (t) => t * t * t,
easeOutCubic: (t) => --t * t * t + 1,
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
easeInQuart: (t) => t * t * t * t,
easeOutQuart: (t) => 1 - --t * t * t * t,
easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t,
easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * (t - 1))),
easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
easeInOutExpo: (t) => {
if (t === 0)
return 0;
if (t === 1)
return 1;
if (t < 0.5)
return Math.pow(2, 10 * (2 * t - 1)) / 2;
return (2 - Math.pow(2, -10 * (2 * t - 1))) / 2;
},
easeOutBounce: (t) => {
const n1 = 7.5625, d1 = 2.75;
if (t < 1 / d1)
return n1 * t * t;
if (t < 2 / d1)
return n1 * (t -= 1.5 / d1) * t + 0.75;
if (t < 2.5 / d1)
return n1 * (t -= 2.25 / d1) * t + 0.9375;
return n1 * (t -= 2.625 / d1) * t + 0.984375;
},
easeInBounce: (t) => 1 - exports.easingFunctions.easeOutBounce(1 - t),
easeInOutBounce: (t) => t < 0.5
? exports.easingFunctions.easeInBounce(t * 2) * 0.5
: exports.easingFunctions.easeOutBounce(t * 2 - 1) * 0.5 + 0.5,
easeInElastic: (t) => {
if (t === 0 || t === 1)
return t;
return -Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI);
},
easeOutElastic: (t) => {
if (t === 0 || t === 1)
return t;
return 1 + Math.pow(2, -10 * t) * Math.sin((t - 0.1) * 5 * Math.PI);
},
easeInOutElastic: (t) => {
if (t === 0 || t === 1)
return t;
if (t < 0.5) {
return -0.5 * Math.pow(2, 10 * (2 * t - 1)) * Math.sin((2 * t - 1.1) * 5 * Math.PI);
}
return 0.5 * Math.pow(2, -10 * (2 * t - 1)) * Math.sin((2 * t - 1.1) * 5 * Math.PI) + 1;
},
spring: (t) => {
const s = 1.70158;
const s2 = 2.5949095;
return t < 0.5
? 0.5 * (2 * t) * (2 * t) * ((s2 + 1) * 2 * t - s2)
: 0.5 * (2 * t - 2) * (2 * t - 2) * ((s2 + 1) * (2 * t - 2) + s2) + 1;
}
};
// Transform helpers
const transformFunctions = [
'translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY',
'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY'
];
// Color helpers
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
const rgbToArray = (rgb) => {
const result = rgb.match(/\d+/g);
return result ? result.map(Number) : [0, 0, 0];
};
const interpolateColor = (color1, color2, progress) => {
let rgb1 = [], rgb2 = [];
if (color1.startsWith('#')) {
const parsed = hexToRgb(color1);
if (parsed)
rgb1 = [parsed.r, parsed.g, parsed.b];
}
else if (color1.startsWith('rgb')) {
rgb1 = rgbToArray(color1);
}
if (color2.startsWith('#')) {
const parsed = hexToRgb(color2);
if (parsed)
rgb2 = [parsed.r, parsed.g, parsed.b];
}
else if (color2.startsWith('rgb')) {
rgb2 = rgbToArray(color2);
}
if (rgb1.length === 3 && rgb2.length === 3) {
const r = Math.round(rgb1[0] + (rgb2[0] - rgb1[0]) * progress);
const g = Math.round(rgb1[1] + (rgb2[1] - rgb1[1]) * progress);
const b = Math.round(rgb1[2] + (rgb2[2] - rgb1[2]) * progress);
return `rgb(${r}, ${g}, ${b})`;
}
return color2;
};
// Enhanced CSS value parser
const parseCSSValue = (value) => {
const match = value.match(/^(-?\d*\.?\d+)(.*)$/);
if (match) {
return {
value: parseFloat(match[1]),
unit: match[2] || ''
};
}
return { value: 0, unit: '' };
};
// Parse transform
const parseTransform = (transformString) => {
const transforms = {};
const regex = /(\w+)\(([^)]*)\)/g;
let matches;
while ((matches = regex.exec(transformString)) !== null) {
const name = matches[1];
const args = matches[2].split(/,\s*/);
transforms[name] = { values: [], units: [] };
args.forEach(arg => {
const { value, unit } = parseCSSValue(arg.trim());
transforms[name].values.push(value);
transforms[name].units.push(unit);
});
}
return transforms;
};
// Interpolate transforms
const interpolateTransform = (start, end, progress) => {
const startTransforms = parseTransform(start);
const endTransforms = parseTransform(end);
const allTransformTypes = new Set([
...Object.keys(startTransforms),
...Object.keys(endTransforms)
]);
const result = [];
for (const type of allTransformTypes) {
const isStartDefined = type in startTransforms;
const isEndDefined = type in endTransforms;
if (!isEndDefined)
continue;
const endData = endTransforms[type];
let startData = isStartDefined
? startTransforms[type]
: {
values: Array(endData.values.length).fill(type.startsWith('scale') ? 1 : 0),
units: [...endData.units]
};
const values = endData.values.map((endVal, i) => {
const startVal = i < startData.values.length ? startData.values[i] : (type.startsWith('scale') ? 1 : 0);
let interpolated = startVal + (endVal - startVal) * progress;
return `${interpolated}${endData.units[i]}`;
});
result.push(`${type}(${values.join(', ')})`);
}
return result.join(' ');
};
// Process keyframes for a property
const processKeyframes = (propName, keyframes, initialValue, duration) => {
// Convert percentage keys to absolute time
const timePoints = Object.entries(keyframes)
.map(([keyPercent, value]) => {
const percent = keyPercent === 'from' ? 0 : keyPercent === 'to' ? 100 : parseFloat(keyPercent);
return [percent / 100 * duration, value];
})
.sort((a, b) => a[0] - b[0]);
// Ensure we have a starting point (0%)
if (timePoints[0][0] > 0) {
timePoints.unshift([0, initialValue !== null && initialValue !== void 0 ? initialValue : timePoints[0][1]]);
}
// Return a function that calculates the value at a given time
return (time) => {
// Find the two keyframe points that surround the current time
let startIdx = 0;
for (let i = 0; i < timePoints.length - 1; i++) {
if (time >= timePoints[i][0] && time <= timePoints[i + 1][0]) {
startIdx = i;
break;
}
}
if (startIdx === timePoints.length - 1 || time >= timePoints[timePoints.length - 1][0]) {
return timePoints[timePoints.length - 1][1];
}
const [t0, v0] = timePoints[startIdx];
const [t1, v1] = timePoints[startIdx + 1];
// Calculate local progress between these two points
const segmentProgress = (time - t0) / (t1 - t0);
// Special case for colors
if (typeof v0 === 'string' && (v0.startsWith('#') || v0.startsWith('rgb')) &&
typeof v1 === 'string' && (v1.startsWith('#') || v1.startsWith('rgb'))) {
return interpolateColor(v0, v1, segmentProgress);
}
// Special case for transforms
if (propName === 'transform' && typeof v0 === 'string' && typeof v1 === 'string') {
return interpolateTransform(v0, v1, segmentProgress);
}
// Regular numeric interpolation
if (typeof v0 === 'number' && typeof v1 === 'number') {
return v0 + (v1 - v0) * segmentProgress;
}
// Default to just returning the next value
return v1;
};
};
/**
* Enhanced animate function
*/
const animate = (config) => {
const { targets, props, duration, delay = 0, endDelay = 0, easing = "easeOutQuad", round = false, loop = false, direction = "normal", update, begin, complete, autoplay = true, } = config;
const isObjectTarget = typeof targets === "object" &&
!Array.isArray(targets) &&
!(targets instanceof HTMLElement) &&
!(targets instanceof NodeList);
// Normalize targets to array
const elements = !isObjectTarget
? typeof targets === "string"
? Array.from(document.querySelectorAll(targets))
: targets instanceof NodeList
? Array.from(targets)
: Array.isArray(targets)
? targets
: [targets]
: [];
// Setup animation properties
const loopCount = typeof loop === "boolean" ? (loop ? Infinity : 0) : loop;
let currentLoop = 0;
let currentDirection = direction;
let isPaused = !autoplay;
let startTime = null;
let pauseTime = null;
let pausedDuration = 0;
let animationProgress = 0;
let easingFunc = typeof easing === "function" ? easing : exports.easingFunctions[easing] || exports.easingFunctions.linear;
// Gather initial values
const initialStates = isObjectTarget
? [Object.assign({}, targets)]
: elements.map((el) => {
const state = {};
Object.keys(props).forEach((key) => {
// Handle special properties
if (key === "transform") {
state[key] = window.getComputedStyle(el).transform === "none"
? ""
: window.getComputedStyle(el).transform;
}
// Handle standard CSS properties
else if (key in el.style) {
state[key] = window.getComputedStyle(el)[key];
}
// Handle SVG attributes
else if (el instanceof SVGElement && el.hasAttribute(key)) {
state[key] = el.getAttribute(key) || "";
}
// Handle direct properties
else if (key in el) {
state[key] = el[key];
}
// Handle special cases like scroll
else if (key === "scroll") {
state[key] = el.scrollTop;
}
else if (key === "scrollX") {
state[key] = el.scrollLeft;
}
});
return state;
});
const processedProps = {};
Object.entries(props).forEach(([propName, propValue]) => {
var _a;
const isKeyframeObject = propValue !== null &&
typeof propValue === "object" &&
!Array.isArray(propValue) &&
(Object.keys(propValue).some(key => key === "from" || key === "to" || /^\d+%$/.test(key)));
if (isKeyframeObject) {
const initialValue = isObjectTarget
? initialStates[0][propName]
: (_a = initialStates[0]) === null || _a === void 0 ? void 0 : _a[propName];
const valueAtTime = processKeyframes(propName, propValue, initialValue, duration);
processedProps[propName] = {
isKeyframes: true,
valueAtTime,
endValue: propValue.to || propValue["100%"] || Object.values(propValue).pop()
};
}
else {
processedProps[propName] = {
isKeyframes: false,
valueAtTime: () => propValue,
endValue: propValue
};
}
});
// Animation control
let rafId = null;
let canceled = false;
// Create a promise that resolves when animation completes
let resolveAnimation;
const animationCompletePromise = new Promise((resolve) => {
resolveAnimation = resolve;
});
// Calculate delay for element
const getDelay = (el, index, total) => {
if (typeof delay === "function") {
return delay(el, index, total);
}
return delay;
};
// Apply values to targets based on progress
const applyValues = (rawProgress) => {
// Get actual progress depending on direction
let progress = rawProgress;
if (currentDirection === "reverse") {
progress = 1 - rawProgress;
}
else if (currentDirection === "alternate") {
progress = currentLoop % 2 === 0 ? rawProgress : 1 - rawProgress;
}
else if (currentDirection === "alternate-reverse") {
progress = currentLoop % 2 === 0 ? 1 - rawProgress : rawProgress;
}
// Apply easing
const easedProgress = easingFunc(Math.min(1, Math.max(0, progress)));
if (isObjectTarget) {
// Apply to object target
Object.keys(processedProps).forEach((key) => {
const prop = processedProps[key];
let value;
if (prop.isKeyframes) {
value = prop.valueAtTime(progress * duration);
}
else {
const initial = initialStates[0][key];
const target = prop.endValue;
if (typeof initial === 'string' && typeof target === 'string') {
// Handle colors
if ((initial.startsWith('#') || initial.startsWith('rgb')) &&
(target.startsWith('#') || target.startsWith('rgb'))) {
value = interpolateColor(initial, target, easedProgress);
}
// Handle transforms
else if (key === 'transform') {
value = interpolateTransform(initial || '', target, easedProgress);
}
// Handle other string values (try to parse numbers)
else {
const initialVal = parseCSSValue(initial);
const targetVal = parseCSSValue(target);
if (!isNaN(initialVal.value) && !isNaN(targetVal.value)) {
let interpolated = initialVal.value + (targetVal.value - initialVal.value) * easedProgress;
if (round) {
interpolated = typeof round === 'number'
? parseFloat(interpolated.toFixed(round))
: Math.round(interpolated);
}
value = `${interpolated}${targetVal.unit || initialVal.unit}`;
}
else {
value = easedProgress >= 0.5 ? target : initial;
}
}
}
// Handle numeric values
else if (typeof initial === 'number' && typeof target === 'number') {
let interpolated = initial + (target - initial) * easedProgress;
if (round) {
interpolated = typeof round === 'number'
? parseFloat(interpolated.toFixed(round))
: Math.round(interpolated);
}
value = interpolated;
}
else {
value = easedProgress >= 0.5 ? target : initial;
}
}
targets[key] = value;
});
// Call update callback
update === null || update === void 0 ? void 0 : update(targets, {
completed: progress * 100,
remaining: (1 - progress) * 100
});
}
else {
// Apply to DOM elements
elements.forEach((el, index) => {
const initialValues = initialStates[index];
Object.keys(processedProps).forEach((key) => {
const prop = processedProps[key];
let value;
if (prop.isKeyframes) {
value = prop.valueAtTime(progress * duration);
}
else {
const initial = initialValues[key];
const target = prop.endValue;
// Handle transform property specially
if (key === "transform") {
value = interpolateTransform(initial || "", target, easedProgress);
}
// Handle SVG points attribute
else if (key === "points" && el instanceof SVGElement) {
const startPoints = (initial || "").split(" ").map((p) => p.split(",").map(parseFloat));
const endPoints = target.split(" ").map(p => p.split(",").map(parseFloat));
value = startPoints.map((startPoint, i) => {
if (!endPoints[i])
return startPoint.join(",");
return startPoint.map((v, j) => {
if (isNaN(v))
v = 0;
const endValue = endPoints[i][j] || 0;
return v + (endValue - v) * easedProgress;
}).join(",");
}).join(" ");
}
// Handle color values
else if (typeof initial === 'string' && typeof target === 'string' &&
((initial.startsWith('#') || initial.startsWith('rgb')) &&
(target.startsWith('#') || target.startsWith('rgb')))) {
value = interpolateColor(initial, target, easedProgress);
}
// Handle scroll properties
else if (key === "scroll") {
const start = parseFloat(initial || "0");
const end = parseFloat(target);
const scrollValue = start + (end - start) * easedProgress;
el.scrollTop = scrollValue;
return; // Skip normal assignment below
}
else if (key === "scrollX") {
const start = parseFloat(initial || "0");
const end = parseFloat(target);
const scrollValue = start + (end - start) * easedProgress;
el.scrollLeft = scrollValue;
return; // Skip normal assignment below
}
// Handle standard numeric properties
else {
const initialVal = parseCSSValue(initial || "0");
const targetVal = parseCSSValue(target || "0");
let interpolated = initialVal.value + (targetVal.value - initialVal.value) * easedProgress;
if (round) {
interpolated = typeof round === 'number'
? parseFloat(interpolated.toFixed(round))
: Math.round(interpolated);
}
value = `${interpolated}${targetVal.unit || initialVal.unit}`;
}
}
// Apply the calculated value
if (key in el.style) {
el.style[key] = value;
}
else if (el instanceof SVGElement && el.hasAttribute(key)) {
el.setAttribute(key, value);
}
else if (key in el) {
el[key] = value;
}
});
});
// Call update callback
update === null || update === void 0 ? void 0 : update(elements, {
completed: progress * 100,
remaining: (1 - progress) * 100
});
}
return easedProgress;
};
// Create animation timeline function
const runAnimation = () => {
if (canceled) {
resolveAnimation();
return;
}
if (isPaused) {
pauseTime = pauseTime || performance.now();
rafId = requestAnimationFrame(runAnimation);
return;
}
const now = performance.now();
// Initialize or resume animation
if (startTime === null) {
startTime = now;
begin === null || begin === void 0 ? void 0 : begin();
}
else if (pauseTime !== null) {
// Adjust for pause duration
pausedDuration += now - pauseTime;
pauseTime = null;
}
// Calculate elapsed time considering pauses
const elapsed = now - startTime - pausedDuration;
const totalDuration = duration + (typeof delay === "function" ? 0 : delay);
// Calculate raw progress (0-1)
let progress = Math.min(Math.max(0, (elapsed - (typeof delay === "function" ? getDelay(elements[0], 0, elements.length) : delay)) / duration), 1);
animationProgress = progress;
// Apply animation values based on progress
applyValues(progress);
// Continue animation if not complete
if (progress < 1) {
rafId = requestAnimationFrame(runAnimation);
}
else {
// Animation completed
currentLoop++;
// Check if we need to loop
if (currentLoop < loopCount || loopCount === Infinity) {
// Reset for next loop
startTime = performance.now();
pausedDuration = 0;
// Toggle direction if needed
if (direction === "alternate") {
currentDirection = currentDirection === "normal" ? "reverse" : "normal";
}
else if (direction === "alternate-reverse") {
currentDirection = currentDirection === "reverse" ? "normal" : "reverse";
}
rafId = requestAnimationFrame(runAnimation);
}
else {
// All loops completed
complete === null || complete === void 0 ? void 0 : complete();
resolveAnimation();
}
}
};
// Control methods
const animation = {
play: () => {
if (canceled)
return animation;
isPaused = false;
if (rafId === null) {
rafId = requestAnimationFrame(runAnimation);
}
return animation;
},
pause: () => {
isPaused = true;
return animation;
},
restart: () => {
if (canceled)
return animation;
startTime = null;
pauseTime = null;
pausedDuration = 0;
currentLoop = 0;
currentDirection = direction;
isPaused = false;
if (rafId === null) {
rafId = requestAnimationFrame(runAnimation);
}
return animation;
},
seek: (seekProgress) => {
if (canceled)
return animation;
animationProgress = Math.min(1, Math.max(0, seekProgress));
applyValues(animationProgress);
return animation;
},
reverse: () => {
if (currentDirection === "normal") {
currentDirection = "reverse";
}
else if (currentDirection === "reverse") {
currentDirection = "normal";
}
else if (currentDirection === "alternate") {
currentDirection = "alternate-reverse";
}
else {
currentDirection = "alternate";
}
return animation;
},
cancel: () => {
canceled = true;
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
resolveAnimation();
},
completed: animationCompletePromise,
get progress() {
return animationProgress;
}
};
// Start animation if autoplay is true
if (autoplay && !isPaused) {
animation.play();
}
return animation;
};
exports.animate = animate;
/**
* Timeline for sequencing animations
*/
const timeline = (config = {}) => {
const { autoplay = true, direction = "normal", loop = false, update, complete } = config;
let animations = [];
let timelineLength = 0;
let currentProgress = 0;
let isPaused = !autoplay;
let isReversed = direction === "reverse" || direction === "alternate-reverse";
let currentLoop = 0;
let loopCount = typeof loop === "boolean" ? (loop ? Infinity : 0) : loop;
let rafId = null;
let canceled = false;
// Promise for completion
let resolveTimeline;
const timelineCompletePromise = new Promise((resolve) => {
resolveTimeline = resolve;
});
// Calculate relative position
const parseTimePosition = (pos, refLength) => {
if (typeof pos === "number") {
return pos;
}
// Handle relative positioning like "+=100" or "-=50"
if (pos.startsWith("+=")) {
return refLength + parseFloat(pos.substring(2));
}
else if (pos.startsWith("-=")) {
return refLength - parseFloat(pos.substring(2));
}
else {
return parseFloat(pos);
}
};
// Sort animations by position
const sortAnimations = () => {
animations.sort((a, b) => a.position - b.position);
};
// Run the timeline
const runTimeline = (currentTime = 0) => {
if (canceled) {
resolveTimeline();
return;
}
if (isPaused) {
rafId = requestAnimationFrame(() => runTimeline(currentTime));
return;
}
// Update progress and trigger animations
animations.forEach(anim => {
const { config, position } = anim;
// Ensure animation has the necessary properties
config.autoplay = false; // We control playback
// Create animation instance if not already
if (!anim.animation) {
anim.animation = (0, exports.animate)(config);
}
// Calculate if this animation should be active
const animEndTime = position + config.duration + (config.endDelay || 0);
if (currentTime >= position && currentTime <= animEndTime) {
// Animation is active, calculate local progress
const localProgress = (currentTime - position) / config.duration;
anim.animation.seek(isReversed ? 1 - localProgress : localProgress);
}
else if (currentTime < position) {
// Animation hasn't started yet
anim.animation.seek(isReversed ? 1 : 0);
}
else if (currentTime > animEndTime) {
// Animation has completed
anim.animation.seek(isReversed ? 0 : 1);
}
});
// Calculate overall timeline progress
currentProgress = timelineLength > 0 ? currentTime / timelineLength : 1;
// Update callback
update === null || update === void 0 ? void 0 : update({
completed: currentProgress * 100,
remaining: (1 - currentProgress) * 100
});
// Check if timeline is complete
if (currentTime >= timelineLength) {
currentLoop++;
if (currentLoop < loopCount || loopCount === Infinity) {
// Handle loop and direction changes
if (direction === "alternate" || direction === "alternate-reverse") {
isReversed = !isReversed;
}
// Reset for next loop
runTimeline(0);
}
else {
// All done
complete === null || complete === void 0 ? void 0 : complete();
resolveTimeline();
}
}
else {
// Continue timeline
rafId = requestAnimationFrame(() => runTimeline(currentTime + (isReversed ? -1 : 1)));
}
};
// Create timeline object
const timelineObj = {
add: (animConfig, timePosition = "+=0") => {
const position = parseTimePosition(timePosition, timelineLength);
animations.push({
config: Object.assign({}, animConfig),
position
});
// Update timeline length
const animEndTime = position + animConfig.duration + (animConfig.endDelay || 0);
if (animEndTime > timelineLength) {
timelineLength = animEndTime;
}
sortAnimations();
return timelineObj;
},
play: () => {
if (canceled)
return timelineObj;
isPaused = false;
if (rafId === null) {
runTimeline(isReversed ? timelineLength : 0);
}
return timelineObj;
},
pause: () => {
isPaused = true;
return timelineObj;
},
restart: () => {
if (canceled)
return timelineObj;
// Reset all animations
animations.forEach(anim => {
if (anim.animation) {
anim.animation.seek(isReversed ? 1 : 0);
}
});
currentLoop = 0;
isPaused = false;
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
runTimeline(isReversed ? timelineLength : 0);
return timelineObj;
},
seek: (progress) => {
const targetTime = timelineLength * Math.min(1, Math.max(0, progress));
runTimeline(targetTime);
return timelineObj;
},
reverse: () => {
isReversed = !isReversed;
return timelineObj;
},
cancel: () => {
canceled = true;
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
// Cancel all animations
animations.forEach(anim => {
var _a;
(_a = anim.animation) === null || _a === void 0 ? void 0 : _a.cancel();
});
resolveTimeline();
},
completed: timelineCompletePromise
};
// Auto-start if required
if (autoplay) {
setTimeout(() => timelineObj.play(), 0);
}
return timelineObj;
};
exports.timeline = timeline;
/**
* Helper function to create staggered animations
*/
const stagger = (value, options = {}) => {
const { start = 0, from = 0, direction = 'normal', grid, axis, easing = exports.easingFunctions.linear } = options;
return (el, i, total) => {
let index = i;
// Handle grid positioning
if (grid) {
const [rows, cols] = grid;
const fromX = from === 'first' ? 0 : from === 'last' ? cols - 1 : from === 'center' ? (cols - 1) / 2 : from;
const fromY = from === 'first' ? 0 : from === 'last' ? rows - 1 : from === 'center' ? (rows - 1) / 2 : from;
const row = Math.floor(i / cols);
const col = i % cols;
// Calculate distance based on axis
if (axis === 'x') {
index = col;
}
else if (axis === 'y') {
index = row;
}
else {
// Calculate distance based on position from origin
const distX = typeof fromX === 'number' ? Math.abs(col - fromX) : 0;
const distY = typeof fromY === 'number' ? Math.abs(row - fromY) : 0;
index = Math.sqrt(distX * distX + distY * distY);
}
}
else {
// Handle linear positioning
if (from === 'first') {
index = i;
}
else if (from === 'last') {
index = total - 1 - i;
}
else if (from === 'center') {
index = Math.abs((total - 1) / 2 - i);
}
else if (from === 'edges') {
index = Math.abs(i - (total - 1) * (i / (total - 1) < 0.5 ? 0 : 1));
}
else if (typeof from === 'number') {
index = Math.abs(from - i);
}
}
// Apply direction
if (direction === 'reverse') {
index = total - 1 - index;
}
// Apply easing to the index for non-linear distribution
const progress = index / Math.max(total - 1, 1);
const easedProgress = easing(progress);
return start + value * easedProgress;
};
};
exports.stagger = stagger;
/**
* Spring animation helper
*/
const spring = (config = {}) => {
const { mass = 1, stiffness = 100, damping = 10, velocity = 0 } = config;
return (t) => {
// Approximation of a spring function using damped sine wave
const omega = Math.sqrt(stiffness / mass);
const zeta = damping / (2 * Math.sqrt(stiffness * mass));
if (zeta < 1) {
// Underdamped
const omega_d = omega * Math.sqrt(1 - zeta * zeta);
return 1 - Math.exp(-zeta * omega * t) *
(Math.cos(omega_d * t) + (zeta * omega * velocity) / omega_d * Math.sin(omega_d * t));
}
else {
// Critically damped (zeta = 1)
return 1 - (1 + omega * t) * Math.exp(-omega * t);
}
};
};
exports.spring = spring;
/**
* Create a physics-based animation
*/
const physics = (config) => {
const { physics = {} } = config, animConfig = __rest(config, ["physics"]);
const springEasing = (0, exports.spring)(physics);
return (0, exports.animate)(Object.assign(Object.assign({}, animConfig), { easing: springEasing }));
};
exports.physics = physics;
/**
* Utility to create a sequence of animations
*/
const sequence = (animations) => {
const tl = (0, exports.timeline)();
animations.forEach((anim, index) => {
tl.add(anim, index === 0 ? 0 : "+=0");
});
return tl;
};
exports.sequence = sequence;
/**
* Create transform string from individual transform properties
*/
const createTransform = (transforms) => {
return Object.entries(transforms)
.map(([key, value]) => {
// Handle special cases
if (key === 'rotate' || key === 'rotateX' || key === 'rotateY' || key === 'rotateZ') {
const val = typeof value === 'number' ? `${value}deg` : value;
return `${key}(${val})`;
}
if (key === 'scale' || key === 'scaleX' || key === 'scaleY' || key === 'scaleZ') {
return `${key}(${value})`;
}
if (key === 'translateX' || key === 'translateY' || key === 'translateZ') {
const val = typeof value === 'number' ? `${value}px` : value;
return `${key}(${val})`;
}
if (key === 'skew' || key === 'skewX' || key === 'skewY') {
const val = typeof value === 'number' ? `${value}deg` : value;
return `${key}(${val})`;
}
return `${key}(${value})`;
})
.join(' ');
};
exports.createTransform = createTransform;
/**
* Quick one-shot animations for common use cases
*/
exports.effects = {
fadeIn: (targets, duration = 500, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: { opacity: [0, 1] }, duration, easing: 'easeOutQuad' }, options));
},
fadeOut: (targets, duration = 500, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: { opacity: [1, 0] }, duration, easing: 'easeOutQuad' }, options));
},
slideIn: (targets, direction = 'left', distance = '100%', duration = 500, options = {}) => {
const prop = direction === 'left' || direction === 'right' ? 'translateX' : 'translateY';
const start = direction === 'left' || direction === 'up' ? distance : `-${distance}`;
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
`${prop}(${start})`,
`${prop}(0)`
]
}, duration, easing: 'easeOutQuad' }, options));
},
slideOut: (targets, direction = 'left', distance = '100%', duration = 500, options = {}) => {
const prop = direction === 'left' || direction === 'right' ? 'translateX' : 'translateY';
const end = direction === 'right' || direction === 'down' ? distance : `-${distance}`;
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
`${prop}(0)`,
`${prop}(${end})`
]
}, duration, easing: 'easeOutQuad' }, options));
},
zoom: (targets, start = 0.5, end = 1, duration = 500, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
`scale(${start})`,
`scale(${end})`
]
}, duration, easing: 'easeOutQuad' }, options));
},
pulse: (targets, scale = 1.05, duration = 600, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
'scale(1)',
`scale(${scale})`,
'scale(1)'
]
}, duration, easing: 'easeInOutQuad' }, options));
},
shake: (targets, intensity = 10, duration = 800, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
'translateX(0)',
`translateX(${intensity}px)`,
`translateX(-${intensity}px)`,
`translateX(${intensity * 0.8}px)`,
`translateX(-${intensity * 0.8}px)`,
`translateX(${intensity * 0.5}px)`,
`translateX(-${intensity * 0.5}px)`,
'translateX(0)'
]
}, duration, easing: 'easeOutQuad' }, options));
},
flipIn: (targets, axis = 'X', duration = 800, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
`rotate${axis}(90deg)`,
`rotate${axis}(0deg)`
],
opacity: [0, 1]
}, duration, easing: 'easeOutQuad' }, options));
},
bounce: (targets, height = '20px', duration = 1000, options = {}) => {
return (0, exports.animate)(Object.assign({ targets, props: {
transform: [
'translateY(0)',
`translateY(-${height})`,
'translateY(0)',
`translateY(-${parseInt(height) * 0.6}px)`,
'translateY(0)',
`translateY(-${parseInt(height) * 0.3}px)`,
'translateY(0)'
]
}, duration, easing: 'easeOutQuad' }, options));
}
};