-
Notifications
You must be signed in to change notification settings - Fork 642
Expand file tree
/
Copy pathAnimate.js
More file actions
133 lines (116 loc) · 3.37 KB
/
Animate.js
File metadata and controls
133 lines (116 loc) · 3.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/* Animate
Modern animation using Web Animations API with CSS transitions fallback
Provides a simpler, more performant alternative to the legacy Morpheus-based approach
================================================== */
/**
* Animate element properties using Web Animations API or CSS transitions
* @param {HTMLElement|HTMLElement[]} el - Element(s) to animate
* @param {Object} options - Animation options
* @param {number} [options.duration=1000] - Duration in milliseconds
* @param {string|function} [options.easing='ease'] - Easing function
* @param {function} [options.complete] - Callback when animation completes
* @returns {Object} Animation controller with stop() method
*/
export function Animate(el, options) {
return animate(el, options);
}
function animate(elements, options) {
// Normalize to array
const els = Array.isArray(elements) ? elements : (elements.length !== undefined ? Array.from(elements) : [elements]);
const {
duration = 1000,
easing = 'ease',
complete,
...properties
} = options;
// Convert easing function to CSS easing string if needed
const easingStr = typeof easing === 'function' ? 'ease-out' : easing;
const animations = [];
const useWebAnimations = 'animate' in HTMLElement.prototype;
els.forEach(el => {
if (!el || !el.style) return;
if (useWebAnimations) {
// Use Web Animations API for better performance and control
const keyframes = {};
for (const prop in properties) {
if (properties.hasOwnProperty(prop)) {
keyframes[prop] = properties[prop];
}
}
try {
const animation = el.animate(keyframes, {
duration,
easing: easingStr,
fill: 'forwards'
});
animation.onfinish = () => {
// Apply final styles
for (const prop in properties) {
if (properties.hasOwnProperty(prop)) {
el.style[prop] = properties[prop];
}
}
};
animations.push(animation);
} catch (e) {
// Fallback to CSS transitions if Web Animations fails
useCSSTransition(el, properties, duration, easingStr);
}
} else {
// Fallback to CSS transitions
useCSSTransition(el, properties, duration, easingStr);
}
});
let stopped = false;
// Return controller object
return {
stop(jump = false) {
stopped = true;
animations.forEach(anim => {
if (anim && anim.cancel) {
if (jump) {
anim.finish();
} else {
anim.cancel();
}
}
});
if (!jump) {
// Don't call complete callback if stopped without jumping
return;
}
if (complete) {
complete();
}
}
};
function useCSSTransition(el, props, dur, ease) {
const propNames = Object.keys(props).map(camelToKebab).join(', ');
el.style.transition = `${propNames} ${dur}ms ${ease}`;
// Apply properties after a microtask to ensure transition triggers
requestAnimationFrame(() => {
for (const prop in props) {
if (props.hasOwnProperty(prop)) {
el.style[prop] = props[prop];
}
}
});
// Call complete callback
const handleTransitionEnd = (e) => {
if (e.target === el && !stopped) {
el.removeEventListener('transitionend', handleTransitionEnd);
el.style.transition = '';
if (complete) {
complete();
}
}
};
el.addEventListener('transitionend', handleTransitionEnd);
}
}
/**
* Convert camelCase to kebab-case for CSS properties
*/
function camelToKebab(str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}