elementdrawing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Utilities
|
|
3
|
+
* ElementDrawing Framework - requestAnimationFrame wrapper, easing functions,
|
|
4
|
+
* tween engine, spring physics, keyframes, scroll/intersection animation,
|
|
5
|
+
* sequencing, and CSS animation helpers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// ─── requestAnimationFrame Wrapper ────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let rafId = 0;
|
|
13
|
+
const rafCallbacks = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Request an animation frame with a cancel function.
|
|
17
|
+
* @param {Function} callback
|
|
18
|
+
* @returns {number} RAF ID
|
|
19
|
+
*/
|
|
20
|
+
function requestFrame(callback) {
|
|
21
|
+
const id = ++rafId;
|
|
22
|
+
const wrapped = (time) => {
|
|
23
|
+
rafCallbacks.delete(id);
|
|
24
|
+
callback(time);
|
|
25
|
+
};
|
|
26
|
+
rafCallbacks.set(id, wrapped);
|
|
27
|
+
return window.requestAnimationFrame(wrapped);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cancel a requested animation frame.
|
|
32
|
+
* @param {number} id
|
|
33
|
+
*/
|
|
34
|
+
function cancelFrame(id) {
|
|
35
|
+
window.cancelAnimationFrame(id);
|
|
36
|
+
rafCallbacks.delete(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cancel all pending animation frames.
|
|
41
|
+
*/
|
|
42
|
+
function cancelAllFrames() {
|
|
43
|
+
rafCallbacks.forEach((cb, id) => window.cancelAnimationFrame(id));
|
|
44
|
+
rafCallbacks.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Easing Functions ─────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const easing = {
|
|
50
|
+
linear: (t) => t,
|
|
51
|
+
|
|
52
|
+
easeIn: (t) => t * t * t,
|
|
53
|
+
easeOut: (t) => 1 - Math.pow(1 - t, 3),
|
|
54
|
+
easeInOut: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
55
|
+
|
|
56
|
+
easeInQuad: (t) => t * t,
|
|
57
|
+
easeOutQuad: (t) => 1 - (1 - t) * (1 - t),
|
|
58
|
+
easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
|
|
59
|
+
|
|
60
|
+
easeInCubic: (t) => t * t * t,
|
|
61
|
+
easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
|
|
62
|
+
easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
63
|
+
|
|
64
|
+
easeInQuart: (t) => t * t * t * t,
|
|
65
|
+
easeOutQuart: (t) => 1 - Math.pow(1 - t, 4),
|
|
66
|
+
easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2,
|
|
67
|
+
|
|
68
|
+
bounce: (t) => {
|
|
69
|
+
const n1 = 7.5625, d1 = 2.75;
|
|
70
|
+
if (t < 1 / d1) return n1 * t * t;
|
|
71
|
+
if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
|
|
72
|
+
if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
|
73
|
+
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
elastic: (t) => {
|
|
77
|
+
if (t === 0 || t === 1) return t;
|
|
78
|
+
return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ((2 * Math.PI) / 3));
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
back: (t) => {
|
|
82
|
+
const c1 = 1.70158, c3 = c1 + 1;
|
|
83
|
+
return c3 * t * t * t - c1 * t * t;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
expo: (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
|
|
87
|
+
circ: (t) => 1 - Math.sqrt(1 - t * t),
|
|
88
|
+
|
|
89
|
+
spring: (t, stiffness, damping) => {
|
|
90
|
+
stiffness = stiffness || 100;
|
|
91
|
+
damping = damping || 10;
|
|
92
|
+
const omega = Math.sqrt(stiffness);
|
|
93
|
+
const zeta = damping / (2 * omega);
|
|
94
|
+
if (zeta < 1) {
|
|
95
|
+
const omegaD = omega * Math.sqrt(1 - zeta * zeta);
|
|
96
|
+
return 1 - Math.exp(-zeta * omega * t) * (Math.cos(omegaD * t) + (zeta * omega / omegaD) * Math.sin(omegaD * t));
|
|
97
|
+
}
|
|
98
|
+
return 1 - (1 + omega * t) * Math.exp(-omega * t);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ─── Animation Timeline ───────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create an animation timeline for sequencing animations.
|
|
106
|
+
* @returns {Object} Timeline controller
|
|
107
|
+
*/
|
|
108
|
+
function createTimeline() {
|
|
109
|
+
const animations = [];
|
|
110
|
+
let startTime = null;
|
|
111
|
+
let isPlaying = false;
|
|
112
|
+
let rafId = null;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
/**
|
|
116
|
+
* Add an animation to the timeline.
|
|
117
|
+
* @param {Object} anim
|
|
118
|
+
* @param {number} anim.delay - Start delay in ms
|
|
119
|
+
* @param {number} anim.duration - Duration in ms
|
|
120
|
+
* @param {Function} anim.onUpdate - Called with (progress) each frame
|
|
121
|
+
* @param {Function} [anim.onComplete] - Called when animation completes
|
|
122
|
+
* @param {string} [anim.easing='linear'] - Easing function name
|
|
123
|
+
*/
|
|
124
|
+
add: function (anim) {
|
|
125
|
+
animations.push(Object.assign({ delay: 0, duration: 1000, easing: 'linear' }, anim));
|
|
126
|
+
return this;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Start playing the timeline.
|
|
131
|
+
*/
|
|
132
|
+
play: function () {
|
|
133
|
+
if (isPlaying) return;
|
|
134
|
+
isPlaying = true;
|
|
135
|
+
startTime = performance.now();
|
|
136
|
+
|
|
137
|
+
function tick(now) {
|
|
138
|
+
if (!isPlaying) return;
|
|
139
|
+
const elapsed = now - startTime;
|
|
140
|
+
|
|
141
|
+
let allComplete = true;
|
|
142
|
+
animations.forEach((anim) => {
|
|
143
|
+
const localTime = elapsed - anim.delay;
|
|
144
|
+
if (localTime < 0) { allComplete = false; return; }
|
|
145
|
+
|
|
146
|
+
const rawProgress = Math.min(localTime / anim.duration, 1);
|
|
147
|
+
if (rawProgress < 1) allComplete = false;
|
|
148
|
+
|
|
149
|
+
const easeFn = easing[anim.easing] || easing.linear;
|
|
150
|
+
const progress = easeFn(rawProgress);
|
|
151
|
+
anim.onUpdate(progress, rawProgress);
|
|
152
|
+
|
|
153
|
+
if (rawProgress >= 1 && anim.onComplete) {
|
|
154
|
+
anim.onComplete();
|
|
155
|
+
anim.onComplete = null;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!allComplete) {
|
|
160
|
+
rafId = requestFrame(tick);
|
|
161
|
+
} else {
|
|
162
|
+
isPlaying = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
rafId = requestFrame(tick);
|
|
167
|
+
return this;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Pause the timeline.
|
|
172
|
+
*/
|
|
173
|
+
pause: function () {
|
|
174
|
+
isPlaying = false;
|
|
175
|
+
if (rafId) cancelFrame(rafId);
|
|
176
|
+
return this;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reset the timeline.
|
|
181
|
+
*/
|
|
182
|
+
reset: function () {
|
|
183
|
+
this.pause();
|
|
184
|
+
startTime = null;
|
|
185
|
+
return this;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Tween Engine ─────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a tween animation from a start value to an end value.
|
|
194
|
+
* @param {Object} options
|
|
195
|
+
* @param {*} options.from - Starting value
|
|
196
|
+
* @param {*} options.to - Ending value
|
|
197
|
+
* @param {number} options.duration - Duration in ms
|
|
198
|
+
* @param {string} [options.easing='easeInOut'] - Easing function
|
|
199
|
+
* @param {Function} options.onUpdate - Called with (currentValue) each frame
|
|
200
|
+
* @param {Function} [options.onComplete] - Called when complete
|
|
201
|
+
* @returns {Object} Tween controller with play, pause, reset
|
|
202
|
+
*/
|
|
203
|
+
function tween(options) {
|
|
204
|
+
const from = options.from;
|
|
205
|
+
const to = options.to;
|
|
206
|
+
const duration = options.duration || 1000;
|
|
207
|
+
const easeFn = easing[options.easing || 'easeInOut'] || easing.easeInOut;
|
|
208
|
+
const onUpdate = options.onUpdate;
|
|
209
|
+
const onComplete = options.onComplete;
|
|
210
|
+
|
|
211
|
+
let startTime = null;
|
|
212
|
+
let isPlaying = false;
|
|
213
|
+
let rafId = null;
|
|
214
|
+
let currentValue = from;
|
|
215
|
+
|
|
216
|
+
function tick(now) {
|
|
217
|
+
if (!isPlaying) return;
|
|
218
|
+
if (!startTime) startTime = now;
|
|
219
|
+
|
|
220
|
+
const elapsed = now - startTime;
|
|
221
|
+
const rawProgress = Math.min(elapsed / duration, 1);
|
|
222
|
+
const progress = easeFn(rawProgress);
|
|
223
|
+
|
|
224
|
+
currentValue = from + (to - from) * progress;
|
|
225
|
+
onUpdate(currentValue, progress, rawProgress);
|
|
226
|
+
|
|
227
|
+
if (rawProgress < 1) {
|
|
228
|
+
rafId = requestFrame(tick);
|
|
229
|
+
} else {
|
|
230
|
+
isPlaying = false;
|
|
231
|
+
if (onComplete) onComplete(currentValue);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
play: function () {
|
|
237
|
+
if (isPlaying) return this;
|
|
238
|
+
isPlaying = true;
|
|
239
|
+
startTime = null;
|
|
240
|
+
rafId = requestFrame(tick);
|
|
241
|
+
return this;
|
|
242
|
+
},
|
|
243
|
+
pause: function () {
|
|
244
|
+
isPlaying = false;
|
|
245
|
+
if (rafId) cancelFrame(rafId);
|
|
246
|
+
return this;
|
|
247
|
+
},
|
|
248
|
+
reset: function () {
|
|
249
|
+
this.pause();
|
|
250
|
+
startTime = null;
|
|
251
|
+
currentValue = from;
|
|
252
|
+
onUpdate(from, 0, 0);
|
|
253
|
+
return this;
|
|
254
|
+
},
|
|
255
|
+
getCurrentValue: function () { return currentValue; },
|
|
256
|
+
isPlaying: function () { return isPlaying; },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Create a tween from a starting value.
|
|
262
|
+
* @param {*} from
|
|
263
|
+
* @param {*} to
|
|
264
|
+
* @param {number} duration
|
|
265
|
+
* @returns {Object} Tween builder
|
|
266
|
+
*/
|
|
267
|
+
function fromTo(from, to, duration) {
|
|
268
|
+
return tween({ from, to, duration });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a tween from current value to a target.
|
|
273
|
+
* @param {*} from
|
|
274
|
+
* @param {*} to
|
|
275
|
+
* @param {number} duration
|
|
276
|
+
* @returns {Object} Tween builder
|
|
277
|
+
*/
|
|
278
|
+
function to(from, to, duration) {
|
|
279
|
+
return tween({ from, to, duration });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create staggered tweens for multiple targets.
|
|
284
|
+
* @param {Array} targets
|
|
285
|
+
* @param {Object} options
|
|
286
|
+
* @param {number} options.stagger - Delay between each target in ms
|
|
287
|
+
* @param {*} options.from
|
|
288
|
+
* @param {*} options.to
|
|
289
|
+
* @param {number} options.duration
|
|
290
|
+
* @param {Function} options.onUpdate
|
|
291
|
+
* @returns {Array} Array of tween controllers
|
|
292
|
+
*/
|
|
293
|
+
function stagger(targets, options) {
|
|
294
|
+
const staggerDelay = options.stagger || 50;
|
|
295
|
+
return targets.map((target, i) => {
|
|
296
|
+
return tween({
|
|
297
|
+
from: options.from,
|
|
298
|
+
to: options.to,
|
|
299
|
+
duration: options.duration || 1000,
|
|
300
|
+
easing: options.easing || 'easeInOut',
|
|
301
|
+
onUpdate: function (value) {
|
|
302
|
+
if (options.onUpdate) options.onUpdate(value, target, i);
|
|
303
|
+
},
|
|
304
|
+
onComplete: options.onComplete ? () => options.onComplete(target, i) : undefined,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Spring Physics ───────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create a spring-based animation.
|
|
313
|
+
* @param {Object} options
|
|
314
|
+
* @param {number} [options.stiffness=100]
|
|
315
|
+
* @param {number} [options.damping=10]
|
|
316
|
+
* @param {number} [options.mass=1]
|
|
317
|
+
* @param {number} [options.initialVelocity=0]
|
|
318
|
+
* @param {number} options.from
|
|
319
|
+
* @param {number} options.to
|
|
320
|
+
* @param {Function} options.onUpdate
|
|
321
|
+
* @param {Function} [options.onComplete]
|
|
322
|
+
* @returns {Object} Spring controller
|
|
323
|
+
*/
|
|
324
|
+
function spring(options) {
|
|
325
|
+
const stiffness = options.stiffness || 100;
|
|
326
|
+
const damping = options.damping || 10;
|
|
327
|
+
const mass = options.mass || 1;
|
|
328
|
+
let velocity = options.initialVelocity || 0;
|
|
329
|
+
const from = options.from;
|
|
330
|
+
const to = options.to;
|
|
331
|
+
const onUpdate = options.onUpdate;
|
|
332
|
+
const onComplete = options.onComplete;
|
|
333
|
+
|
|
334
|
+
let currentValue = from;
|
|
335
|
+
let isPlaying = false;
|
|
336
|
+
let rafId = null;
|
|
337
|
+
let lastTime = null;
|
|
338
|
+
const precision = 0.01;
|
|
339
|
+
|
|
340
|
+
function step(now) {
|
|
341
|
+
if (!isPlaying) return;
|
|
342
|
+
if (!lastTime) lastTime = now;
|
|
343
|
+
const dt = Math.min((now - lastTime) / 1000, 0.064);
|
|
344
|
+
lastTime = now;
|
|
345
|
+
|
|
346
|
+
const displacement = currentValue - to;
|
|
347
|
+
const springForce = -stiffness * displacement;
|
|
348
|
+
const dampingForce = -damping * velocity;
|
|
349
|
+
const acceleration = (springForce + dampingForce) / mass;
|
|
350
|
+
|
|
351
|
+
velocity += acceleration * dt;
|
|
352
|
+
currentValue += velocity * dt;
|
|
353
|
+
|
|
354
|
+
onUpdate(currentValue);
|
|
355
|
+
|
|
356
|
+
// Check if settled
|
|
357
|
+
if (Math.abs(velocity) < precision && Math.abs(displacement) < precision) {
|
|
358
|
+
currentValue = to;
|
|
359
|
+
onUpdate(to);
|
|
360
|
+
isPlaying = false;
|
|
361
|
+
if (onComplete) onComplete(to);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
rafId = requestFrame(step);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
play: function () {
|
|
370
|
+
if (isPlaying) return this;
|
|
371
|
+
isPlaying = true;
|
|
372
|
+
lastTime = null;
|
|
373
|
+
rafId = requestFrame(step);
|
|
374
|
+
return this;
|
|
375
|
+
},
|
|
376
|
+
pause: function () {
|
|
377
|
+
isPlaying = false;
|
|
378
|
+
if (rafId) cancelFrame(rafId);
|
|
379
|
+
return this;
|
|
380
|
+
},
|
|
381
|
+
reset: function () {
|
|
382
|
+
this.pause();
|
|
383
|
+
currentValue = from;
|
|
384
|
+
velocity = options.initialVelocity || 0;
|
|
385
|
+
lastTime = null;
|
|
386
|
+
onUpdate(from);
|
|
387
|
+
return this;
|
|
388
|
+
},
|
|
389
|
+
setVelocity: function (v) { velocity = v; return this; },
|
|
390
|
+
getCurrentValue: function () { return currentValue; },
|
|
391
|
+
isPlaying: function () { return isPlaying; },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ─── Keyframe Animation ───────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Create a keyframe animation with multiple steps.
|
|
399
|
+
* @param {Object} options
|
|
400
|
+
* @param {Array<{offset: number, value: *}>} options.keyframes
|
|
401
|
+
* @param {number} options.duration
|
|
402
|
+
* @param {string} [options.easing]
|
|
403
|
+
* @param {Function} options.onUpdate
|
|
404
|
+
* @param {Function} [options.onComplete]
|
|
405
|
+
* @returns {Object} Keyframe controller
|
|
406
|
+
*/
|
|
407
|
+
function keyframe(options) {
|
|
408
|
+
const frames = options.keyframes.sort((a, b) => a.offset - b.offset);
|
|
409
|
+
const duration = options.duration || 1000;
|
|
410
|
+
const easeFn = easing[options.easing || 'linear'] || easing.linear;
|
|
411
|
+
const onUpdate = options.onUpdate;
|
|
412
|
+
const onComplete = options.onComplete;
|
|
413
|
+
|
|
414
|
+
let startTime = null;
|
|
415
|
+
let isPlaying = false;
|
|
416
|
+
let rafId = null;
|
|
417
|
+
|
|
418
|
+
function interpolate(progress) {
|
|
419
|
+
if (frames.length === 0) return 0;
|
|
420
|
+
if (frames.length === 1) return frames[0].value;
|
|
421
|
+
|
|
422
|
+
// Find surrounding keyframes
|
|
423
|
+
let lower = frames[0], upper = frames[frames.length - 1];
|
|
424
|
+
for (let i = 0; i < frames.length - 1; i++) {
|
|
425
|
+
if (progress >= frames[i].offset && progress <= frames[i + 1].offset) {
|
|
426
|
+
lower = frames[i];
|
|
427
|
+
upper = frames[i + 1];
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const range = upper.offset - lower.offset;
|
|
433
|
+
const localProgress = range === 0 ? 1 : (progress - lower.offset) / range;
|
|
434
|
+
return lower.value + (upper.value - lower.value) * localProgress;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function tick(now) {
|
|
438
|
+
if (!isPlaying) return;
|
|
439
|
+
if (!startTime) startTime = now;
|
|
440
|
+
|
|
441
|
+
const elapsed = now - startTime;
|
|
442
|
+
const rawProgress = Math.min(elapsed / duration, 1);
|
|
443
|
+
const easedProgress = easeFn(rawProgress);
|
|
444
|
+
const value = interpolate(easedProgress);
|
|
445
|
+
|
|
446
|
+
onUpdate(value, easedProgress, rawProgress);
|
|
447
|
+
|
|
448
|
+
if (rawProgress < 1) {
|
|
449
|
+
rafId = requestFrame(tick);
|
|
450
|
+
} else {
|
|
451
|
+
isPlaying = false;
|
|
452
|
+
if (onComplete) onComplete(value);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
play: function () { isPlaying = true; startTime = null; rafId = requestFrame(tick); return this; },
|
|
458
|
+
pause: function () { isPlaying = false; if (rafId) cancelFrame(rafId); return this; },
|
|
459
|
+
reset: function () { this.pause(); startTime = null; onUpdate(interpolate(0), 0, 0); return this; },
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ─── Scroll Animation ─────────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Animate elements based on scroll position.
|
|
467
|
+
* @param {HTMLElement} el - Element to observe
|
|
468
|
+
* @param {Object} options
|
|
469
|
+
* @param {Function} options.onProgress - Called with (progress: 0-1) on scroll
|
|
470
|
+
* @param {number} [options.start=0] - Start offset in viewport (0=top, 1=bottom)
|
|
471
|
+
* @param {number} [options.end=1] - End offset
|
|
472
|
+
* @returns {Function} Cleanup function
|
|
473
|
+
*/
|
|
474
|
+
function scrollAnimate(el, options) {
|
|
475
|
+
const onProgress = options.onProgress;
|
|
476
|
+
const start = options.start || 0;
|
|
477
|
+
const end = options.end || 1;
|
|
478
|
+
|
|
479
|
+
function update() {
|
|
480
|
+
const rect = el.getBoundingClientRect();
|
|
481
|
+
const viewportHeight = window.innerHeight;
|
|
482
|
+
const elementProgress = (viewportHeight - rect.top) / (viewportHeight + rect.height);
|
|
483
|
+
const progress = Math.max(0, Math.min(1, (elementProgress - start) / (end - start)));
|
|
484
|
+
onProgress(progress);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
window.addEventListener('scroll', update, { passive: true });
|
|
488
|
+
update();
|
|
489
|
+
|
|
490
|
+
return function cleanup() {
|
|
491
|
+
window.removeEventListener('scroll', update);
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ─── Intersection Observer Animation ──────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Trigger animation when element enters viewport.
|
|
499
|
+
* @param {HTMLElement} el
|
|
500
|
+
* @param {Object} options
|
|
501
|
+
* @param {Function} options.onEnter - Called when element enters viewport
|
|
502
|
+
* @param {Function} [options.onExit] - Called when element exits viewport
|
|
503
|
+
* @param {number} [options.threshold=0.1]
|
|
504
|
+
* @returns {Function} Cleanup function
|
|
505
|
+
*/
|
|
506
|
+
function intersectionAnimate(el, options) {
|
|
507
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
508
|
+
options.onEnter(el);
|
|
509
|
+
return function () {};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const observer = new IntersectionObserver(
|
|
513
|
+
(entries) => {
|
|
514
|
+
entries.forEach((entry) => {
|
|
515
|
+
if (entry.isIntersecting) {
|
|
516
|
+
options.onEnter(el, entry);
|
|
517
|
+
} else if (options.onExit) {
|
|
518
|
+
options.onExit(el, entry);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
{ threshold: options.threshold || 0.1 }
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
observer.observe(el);
|
|
526
|
+
return function cleanup() { observer.disconnect(); };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ─── Animation Queue & Sequencing ─────────────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create an animation queue that runs animations sequentially.
|
|
533
|
+
* @returns {Object} Queue controller
|
|
534
|
+
*/
|
|
535
|
+
function createQueue() {
|
|
536
|
+
const queue = [];
|
|
537
|
+
let isRunning = false;
|
|
538
|
+
|
|
539
|
+
function runNext() {
|
|
540
|
+
if (queue.length === 0) { isRunning = false; return; }
|
|
541
|
+
isRunning = true;
|
|
542
|
+
const anim = queue.shift();
|
|
543
|
+
anim(function onComplete() {
|
|
544
|
+
runNext();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
add: function (animFn) { queue.push(animFn); if (!isRunning) runNext(); return this; },
|
|
550
|
+
clear: function () { queue.length = 0; isRunning = false; return this; },
|
|
551
|
+
isRunning: function () { return isRunning; },
|
|
552
|
+
length: function () { return queue.length; },
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Run animations in sequence.
|
|
558
|
+
* @param {...Object} anims - Tween/keyframe controllers
|
|
559
|
+
* @returns {Promise} Resolves when all animations complete
|
|
560
|
+
*/
|
|
561
|
+
function sequence(anims) {
|
|
562
|
+
return anims.reduce((promise, anim) => {
|
|
563
|
+
return promise.then(() => new Promise((resolve) => {
|
|
564
|
+
anim.play();
|
|
565
|
+
const originalComplete = anim.onComplete;
|
|
566
|
+
anim.onComplete = function () {
|
|
567
|
+
if (originalComplete) originalComplete();
|
|
568
|
+
resolve();
|
|
569
|
+
};
|
|
570
|
+
}));
|
|
571
|
+
}, Promise.resolve());
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Run animations in parallel.
|
|
576
|
+
* @param {Array} anims
|
|
577
|
+
* @returns {Promise} Resolves when all animations complete
|
|
578
|
+
*/
|
|
579
|
+
function parallel(anims) {
|
|
580
|
+
return Promise.all(anims.map((anim) => new Promise((resolve) => {
|
|
581
|
+
anim.play();
|
|
582
|
+
const originalComplete = anim.onComplete;
|
|
583
|
+
anim.onComplete = function () {
|
|
584
|
+
if (originalComplete) originalComplete();
|
|
585
|
+
resolve();
|
|
586
|
+
};
|
|
587
|
+
})));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ─── CSS Animation Helpers ────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Apply a CSS transition to an element.
|
|
594
|
+
* @param {HTMLElement} el
|
|
595
|
+
* @param {string} property
|
|
596
|
+
* @param {number} [duration=300]
|
|
597
|
+
* @param {string} [easing='ease']
|
|
598
|
+
* @param {number} [delay=0]
|
|
599
|
+
*/
|
|
600
|
+
function cssTransition(el, property, duration, easingName, delay) {
|
|
601
|
+
duration = duration || 300;
|
|
602
|
+
easingName = easingName || 'ease';
|
|
603
|
+
delay = delay || 0;
|
|
604
|
+
el.style.transition = property + ' ' + duration + 'ms ' + easingName + ' ' + delay + 'ms';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Apply a CSS keyframe animation.
|
|
609
|
+
* @param {HTMLElement} el
|
|
610
|
+
* @param {string} name - Animation name
|
|
611
|
+
* @param {number} [duration=300]
|
|
612
|
+
* @param {string} [easing='ease']
|
|
613
|
+
* @param {string} [fill='forwards']
|
|
614
|
+
* @returns {Promise}
|
|
615
|
+
*/
|
|
616
|
+
function cssAnimate(el, name, duration, easingName, fill) {
|
|
617
|
+
duration = duration || 300;
|
|
618
|
+
easingName = easingName || 'ease';
|
|
619
|
+
fill = fill || 'forwards';
|
|
620
|
+
|
|
621
|
+
el.style.animation = name + ' ' + duration + 'ms ' + easingName + ' ' + fill;
|
|
622
|
+
|
|
623
|
+
return new Promise((resolve) => {
|
|
624
|
+
function handler(e) {
|
|
625
|
+
if (e.animationName === name) {
|
|
626
|
+
el.removeEventListener('animationend', handler);
|
|
627
|
+
resolve(el);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
el.addEventListener('animationend', handler);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Remove CSS animation from element.
|
|
636
|
+
* @param {HTMLElement} el
|
|
637
|
+
*/
|
|
638
|
+
function cssAnimateStop(el) {
|
|
639
|
+
el.style.animation = '';
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
module.exports = {
|
|
645
|
+
requestFrame, cancelFrame, cancelAllFrames,
|
|
646
|
+
easing,
|
|
647
|
+
createTimeline,
|
|
648
|
+
tween, fromTo, to, stagger,
|
|
649
|
+
spring,
|
|
650
|
+
keyframe,
|
|
651
|
+
scrollAnimate,
|
|
652
|
+
intersectionAnimate,
|
|
653
|
+
createQueue, sequence, parallel,
|
|
654
|
+
cssTransition, cssAnimate, cssAnimateStop,
|
|
655
|
+
};
|