@vune-ui/animation 0.1.20

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.
Files changed (68) hide show
  1. package/ARCHITECTURE.md +470 -0
  2. package/CHANGELOG.md +88 -0
  3. package/LICENSE +21 -0
  4. package/PERFORMANCE.md +151 -0
  5. package/README.md +630 -0
  6. package/dist/index.d.ts +474 -0
  7. package/dist/src/canvas/index.d.ts +15 -0
  8. package/dist/src/canvas/index.js +67 -0
  9. package/dist/src/constraints/index.d.ts +33 -0
  10. package/dist/src/constraints/index.js +346 -0
  11. package/dist/src/core/bezier.js +51 -0
  12. package/dist/src/core/composition.js +17 -0
  13. package/dist/src/core/controls.js +22 -0
  14. package/dist/src/core/default-engine.js +20 -0
  15. package/dist/src/core/easing.js +58 -0
  16. package/dist/src/core/engine.js +1031 -0
  17. package/dist/src/core/frame-budget.js +30 -0
  18. package/dist/src/core/index.d.ts +43 -0
  19. package/dist/src/core/index.js +17 -0
  20. package/dist/src/core/js-spring-batch.js +57 -0
  21. package/dist/src/core/kinetics.js +140 -0
  22. package/dist/src/core/math.js +20 -0
  23. package/dist/src/core/motion-value.js +53 -0
  24. package/dist/src/core/planner.js +72 -0
  25. package/dist/src/core/specs.js +70 -0
  26. package/dist/src/dom/index.d.ts +41 -0
  27. package/dist/src/dom/index.js +364 -0
  28. package/dist/src/gesture/index.d.ts +66 -0
  29. package/dist/src/gesture/index.js +376 -0
  30. package/dist/src/index.js +53 -0
  31. package/dist/src/interpolate/color.js +223 -0
  32. package/dist/src/interpolate/css.d.ts +13 -0
  33. package/dist/src/interpolate/css.js +34 -0
  34. package/dist/src/interpolate/index.d.ts +13 -0
  35. package/dist/src/interpolate/index.js +55 -0
  36. package/dist/src/interpolate/transform.js +247 -0
  37. package/dist/src/layout/index.d.ts +56 -0
  38. package/dist/src/layout/index.js +485 -0
  39. package/dist/src/material/index.d.ts +9 -0
  40. package/dist/src/material/index.js +70 -0
  41. package/dist/src/path/index.d.ts +37 -0
  42. package/dist/src/path/index.js +527 -0
  43. package/dist/src/render/frame-batcher.js +52 -0
  44. package/dist/src/scroll/index.d.ts +55 -0
  45. package/dist/src/scroll/index.js +233 -0
  46. package/dist/src/timeline/index.d.ts +147 -0
  47. package/dist/src/timeline/index.js +849 -0
  48. package/dist/src/transition/index.d.ts +88 -0
  49. package/dist/src/transition/index.js +369 -0
  50. package/dist/src/wasm/index.d.ts +29 -0
  51. package/dist/src/wasm/index.js +8 -0
  52. package/dist/src/wasm/loader.js +55 -0
  53. package/dist/src/wasm/shared-wasm-spring-batch.js +52 -0
  54. package/dist/src/wasm/wasm-spring-batch.js +52 -0
  55. package/dist/src/webgl/index.d.ts +22 -0
  56. package/dist/src/webgl/index.js +94 -0
  57. package/dist/src/webgpu/index.d.ts +35 -0
  58. package/dist/src/webgpu/index.js +73 -0
  59. package/dist/src/webgpu/spring-batch.js +218 -0
  60. package/dist/src/worker/index.d.ts +17 -0
  61. package/dist/src/worker/index.js +1 -0
  62. package/dist/src/worker/shared-spring-worker.js +218 -0
  63. package/dist/src/worker/shared-worker.js +75 -0
  64. package/dist/wasm/kernel-scalar.wasm +0 -0
  65. package/dist/wasm/kernel-shared-scalar.wasm +0 -0
  66. package/dist/wasm/kernel-shared-simd.wasm +0 -0
  67. package/dist/wasm/kernel-simd.wasm +0 -0
  68. package/package.json +113 -0
@@ -0,0 +1,233 @@
1
+ import { motionValue } from '../core/motion-value.js';
2
+ import { VelocityTracker } from '../gesture/index.js';
3
+
4
+ function nowMs() {
5
+ return globalThis.performance?.now?.() ?? Date.now();
6
+ }
7
+
8
+ function clamp(value, min, max) {
9
+ return Math.min(max, Math.max(min, value));
10
+ }
11
+
12
+ function finite(value, fallback = 0) {
13
+ return Number.isFinite(value) ? Number(value) : fallback;
14
+ }
15
+
16
+ function resolveBound(bound, context) {
17
+ const value = typeof bound === 'function' ? bound(context) : bound;
18
+ return finite(value);
19
+ }
20
+
21
+ export class ScrollTracker {
22
+ constructor({
23
+ start = 0,
24
+ end = 1,
25
+ clamp: shouldClamp = true,
26
+ velocity = {},
27
+ initialOffset = 0,
28
+ } = {}) {
29
+ this.startSource = start;
30
+ this.endSource = end;
31
+ this.clamp = shouldClamp !== false;
32
+ this.offset = motionValue(finite(initialOffset));
33
+ this.progress = motionValue(0);
34
+ this.velocityTracker = new VelocityTracker(velocity);
35
+ this.velocityTracker.reset(this.offset.get(), nowMs());
36
+ this.lastRange = { start: 0, end: 1, span: 1 };
37
+ this.sampleContext = {};
38
+ this.sample(this.offset.get(), nowMs(), { resetVelocity: true });
39
+ }
40
+
41
+ setRange(start, end) {
42
+ this.startSource = start;
43
+ this.endSource = end;
44
+ this.sample(this.offset.get(), nowMs(), { resetVelocity: true });
45
+ return this;
46
+ }
47
+
48
+ resolveRange(context = {}) {
49
+ const start = resolveBound(this.startSource, context);
50
+ const end = resolveBound(this.endSource, context);
51
+ const span = end - start;
52
+ this.lastRange.start = start;
53
+ this.lastRange.end = end;
54
+ this.lastRange.span = span;
55
+ return this.lastRange;
56
+ }
57
+
58
+ sample(offset, time = nowMs(), {
59
+ context = {},
60
+ resetVelocity = false,
61
+ } = {}) {
62
+ const nextOffset = finite(offset, this.offset.get());
63
+ if (resetVelocity) this.velocityTracker.reset(nextOffset, time);
64
+ else this.velocityTracker.add(nextOffset, time);
65
+ const velocity = resetVelocity ? 0 : this.velocityTracker.velocity;
66
+ const resolvedContext = context && typeof context === 'object' ? context : this.sampleContext;
67
+ resolvedContext.offset = nextOffset;
68
+ const range = this.resolveRange(resolvedContext);
69
+ const raw = Math.abs(range.span) <= 1e-12 ? 0 : (nextOffset - range.start) / range.span;
70
+ const progress = this.clamp ? clamp(raw, 0, 1) : raw;
71
+ const progressVelocity = Math.abs(range.span) <= 1e-12 ? 0 : velocity / range.span;
72
+ this.offset.set(nextOffset, velocity);
73
+ this.progress.set(progress, this.clamp && (raw < 0 || raw > 1) ? 0 : progressVelocity);
74
+ return progress;
75
+ }
76
+
77
+ reset(offset = this.offset.get(), time = nowMs(), context = {}) {
78
+ return this.sample(offset, time, { context, resetVelocity: true });
79
+ }
80
+
81
+ getState() {
82
+ return {
83
+ offset: this.offset.get(),
84
+ velocity: this.offset.getVelocity(),
85
+ progress: this.progress.get(),
86
+ progressVelocity: this.progress.getVelocity(),
87
+ start: this.lastRange.start,
88
+ end: this.lastRange.end,
89
+ };
90
+ }
91
+ }
92
+
93
+ export function createScrollTracker(options) {
94
+ return new ScrollTracker(options);
95
+ }
96
+
97
+ function isWindowLike(target) {
98
+ return target && (target === globalThis.window || ('document' in target && ('scrollX' in target || 'scrollY' in target)));
99
+ }
100
+
101
+ export function readScrollMetrics(target, axis = 'y') {
102
+ if (!target) throw new TypeError('readScrollMetrics() requires a scroll target.');
103
+ if (axis !== 'x' && axis !== 'y') throw new TypeError("scroll axis must be 'x' or 'y'.");
104
+ if (isWindowLike(target)) {
105
+ const documentElement = target.document?.documentElement;
106
+ if (axis === 'x') {
107
+ const offset = finite(target.scrollX ?? target.pageXOffset);
108
+ const viewport = finite(target.innerWidth ?? documentElement?.clientWidth);
109
+ const extent = Math.max(finite(documentElement?.scrollWidth), viewport);
110
+ return { offset, viewport, extent, max: Math.max(0, extent - viewport) };
111
+ }
112
+ const offset = finite(target.scrollY ?? target.pageYOffset);
113
+ const viewport = finite(target.innerHeight ?? documentElement?.clientHeight);
114
+ const extent = Math.max(finite(documentElement?.scrollHeight), viewport);
115
+ return { offset, viewport, extent, max: Math.max(0, extent - viewport) };
116
+ }
117
+ if (axis === 'x') {
118
+ const offset = finite(target.scrollLeft);
119
+ const viewport = finite(target.clientWidth);
120
+ const extent = Math.max(finite(target.scrollWidth), viewport);
121
+ return { offset, viewport, extent, max: Math.max(0, extent - viewport) };
122
+ }
123
+ const offset = finite(target.scrollTop);
124
+ const viewport = finite(target.clientHeight);
125
+ const extent = Math.max(finite(target.scrollHeight), viewport);
126
+ return { offset, viewport, extent, max: Math.max(0, extent - viewport) };
127
+ }
128
+
129
+ function defaultRequestFrame(callback) {
130
+ if (typeof globalThis.requestAnimationFrame === 'function') return globalThis.requestAnimationFrame(callback);
131
+ return setTimeout(() => callback(nowMs()), 16);
132
+ }
133
+
134
+ function defaultCancelFrame(id) {
135
+ if (typeof globalThis.cancelAnimationFrame === 'function') globalThis.cancelAnimationFrame(id);
136
+ else clearTimeout(id);
137
+ }
138
+
139
+ /**
140
+ * DOM/window adapter that turns arbitrarily many scroll events into one metric
141
+ * read and one ScrollTracker sample per animation frame.
142
+ */
143
+ export class ScrollObserver {
144
+ constructor(target, {
145
+ tracker,
146
+ axis = 'y',
147
+ start = 0,
148
+ end = (metrics) => metrics.max,
149
+ clamp: shouldClamp = true,
150
+ requestFrame = defaultRequestFrame,
151
+ cancelFrame = defaultCancelFrame,
152
+ passive = true,
153
+ velocity,
154
+ autoStart = true,
155
+ } = {}) {
156
+ if (!target?.addEventListener || !target?.removeEventListener) throw new TypeError('ScrollObserver requires an EventTarget-like scroll source.');
157
+ if (axis !== 'x' && axis !== 'y') throw new TypeError("scroll axis must be 'x' or 'y'.");
158
+ this.target = target;
159
+ this.axis = axis;
160
+ this.requestFrame = requestFrame;
161
+ this.cancelFrame = cancelFrame;
162
+ this.frameId = null;
163
+ this.pending = false;
164
+ this.disposed = false;
165
+ this.metrics = { offset: 0, viewport: 0, extent: 0, max: 0 };
166
+ this.context = { metrics: this.metrics, target: this.target, axis: this.axis, offset: 0 };
167
+ this.tracker = tracker ?? new ScrollTracker({
168
+ start: typeof start === 'function' ? (context) => start(context.metrics ?? context) : start,
169
+ end: typeof end === 'function' ? (context) => end(context.metrics ?? context) : end,
170
+ clamp: shouldClamp,
171
+ velocity,
172
+ });
173
+ this.onScroll = () => this.schedule();
174
+ target.addEventListener('scroll', this.onScroll, { passive });
175
+ if (autoStart) this.update(nowMs(), { resetVelocity: true });
176
+ }
177
+
178
+ get offset() { return this.tracker.offset; }
179
+ get progress() { return this.tracker.progress; }
180
+
181
+ schedule() {
182
+ if (this.disposed || this.pending) return;
183
+ this.pending = true;
184
+ this.frameId = this.requestFrame((time) => {
185
+ this.pending = false;
186
+ this.frameId = null;
187
+ this.update(time);
188
+ });
189
+ }
190
+
191
+ update(time = nowMs(), { resetVelocity = false } = {}) {
192
+ if (this.disposed) return this.tracker.getState();
193
+ this.metrics = readScrollMetrics(this.target, this.axis);
194
+ this.context.metrics = this.metrics;
195
+ this.context.offset = this.metrics.offset;
196
+ this.tracker.sample(this.metrics.offset, time, { resetVelocity, context: this.context });
197
+ return this.tracker.getState();
198
+ }
199
+
200
+ dispose() {
201
+ if (this.disposed) return;
202
+ this.disposed = true;
203
+ this.target.removeEventListener('scroll', this.onScroll);
204
+ if (this.pending && this.frameId != null) this.cancelFrame(this.frameId);
205
+ this.pending = false;
206
+ this.frameId = null;
207
+ }
208
+ }
209
+
210
+ export function observeScroll(target, options) {
211
+ return new ScrollObserver(target, options);
212
+ }
213
+
214
+ export class ScrollTimelineLink {
215
+ constructor(player, source, { pause = true } = {}) {
216
+ if (!player?.seekProgress) throw new TypeError('ScrollTimelineLink requires a TimelinePlayer-like target.');
217
+ const progress = source?.progress ?? source;
218
+ if (!progress?.subscribeValue && !progress?.subscribe) throw new TypeError('ScrollTimelineLink requires a ScrollTracker/Observer or MotionValue-like progress source.');
219
+ this.player = player;
220
+ this.source = source;
221
+ if (pause) player.pause?.();
222
+ this.unsubscribe = (progress.subscribeValue ?? progress.subscribe).call(progress, (value) => player.seekProgress(value));
223
+ }
224
+
225
+ dispose() {
226
+ this.unsubscribe?.();
227
+ this.unsubscribe = null;
228
+ }
229
+ }
230
+
231
+ export function bindScrollTimeline(player, source, options) {
232
+ return new ScrollTimelineLink(player, source, options);
233
+ }
@@ -0,0 +1,147 @@
1
+ import type { BezierCurve, MotionEngine, MotionValue, ColorSpace, InterpolatorOptions } from '../../index.js';
2
+
3
+ export type TimelineEasing = BezierCurve | ((progress: number) => number);
4
+ export type TimelineKeyframe<T> = {
5
+ at?: number;
6
+ time?: number;
7
+ offset?: number;
8
+ value: T;
9
+ easing?: TimelineEasing;
10
+ };
11
+ export type TimelineTrackOptions = InterpolatorOptions & {
12
+ duration?: number;
13
+ easing?: TimelineEasing;
14
+ };
15
+ export type TimelineDirection = 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
16
+ export type TimelineFill = 'none' | 'forwards' | 'backwards' | 'both';
17
+ export type TimelineStatus = 'finished' | 'cancelled' | 'interrupted';
18
+ export type TimelineResult = {
19
+ status: TimelineStatus;
20
+ currentTime: number;
21
+ elapsedTime: number;
22
+ progress: number;
23
+ iteration: number;
24
+ };
25
+ export type TimelinePlayerOptions = {
26
+ engine?: MotionEngine;
27
+ autoplay?: boolean;
28
+ playbackRate?: number;
29
+ iterations?: number;
30
+ direction?: TimelineDirection;
31
+ onUpdate?: (player: TimelinePlayer) => void;
32
+ onRepeat?: (iteration: number, player: TimelinePlayer, crossedIterations: number) => void;
33
+ onComplete?: (player: TimelinePlayer) => void;
34
+ };
35
+
36
+ export class Timeline {
37
+ constructor(options?: { duration?: number; easing?: TimelineEasing });
38
+ readonly duration: number;
39
+ track(target: MotionValue, frames: Array<number | TimelineKeyframe<number>>, options?: { duration?: number; easing?: TimelineEasing }): this;
40
+ track<T>(target: ((value: T, velocity?: number) => void) | { set(value: T, velocity?: number): void }, frames: Array<T | TimelineKeyframe<T>>, options?: TimelineTrackOptions): this;
41
+ keyframes(target: MotionValue, frames: Array<number | TimelineKeyframe<number>>, options?: { duration?: number; easing?: TimelineEasing }): this;
42
+ keyframes<T>(target: ((value: T, velocity?: number) => void) | { set(value: T, velocity?: number): void }, frames: Array<T | TimelineKeyframe<T>>, options?: TimelineTrackOptions): this;
43
+ fromTo(target: MotionValue, from: number, to: number, options?: { at?: number; duration?: number; easing?: TimelineEasing }): this;
44
+ fromTo<T>(target: ((value: T, velocity?: number) => void) | { set(value: T, velocity?: number): void }, from: T, to: T, options?: TimelineTrackOptions & { at?: number }): this;
45
+ to(target: MotionValue, to: number, options?: { at?: number; duration?: number; easing?: TimelineEasing; from?: number }): this;
46
+ to<T>(target: ((value: T, velocity?: number) => void) | { get?(): T; set(value: T, velocity?: number): void }, to: T, options?: TimelineTrackOptions & { at?: number; from?: T }): this;
47
+ add(child: Timeline, options?: { at?: number; speed?: number; fill?: TimelineFill }): this;
48
+ sample(time: number, options?: { velocityScale?: number }): number;
49
+ zeroVelocities(): void;
50
+ hasMotionValue(value: MotionValue): boolean;
51
+ stopConflicts(engine: MotionEngine): void;
52
+ player(options?: TimelinePlayerOptions): TimelinePlayer;
53
+ }
54
+
55
+ export class TimelinePlayer {
56
+ constructor(timeline: Timeline, options?: TimelinePlayerOptions);
57
+ readonly timeline: Timeline;
58
+ readonly engine: MotionEngine;
59
+ readonly duration: number;
60
+ readonly totalDuration: number;
61
+ readonly finished: Promise<TimelineResult>;
62
+ readonly running: boolean;
63
+ state: 'idle' | 'running' | 'paused' | TimelineStatus;
64
+ playbackRate: number;
65
+ readonly iterations: number;
66
+ readonly direction: TimelineDirection;
67
+ elapsedTime: number;
68
+ currentTime: number;
69
+ progress: number;
70
+ iteration: number;
71
+ play(): this;
72
+ pause(): this;
73
+ cancel(): this;
74
+ finish(): this;
75
+ reverse(): this;
76
+ setPlaybackRate(rate: number): this;
77
+ seek(timeSeconds: number, options?: { iteration?: number }): this;
78
+ seekProgress(progress: number, options?: { iteration?: number }): this;
79
+ scrub(progress: number, options?: { iteration?: number }): this;
80
+ seekElapsed(elapsedSeconds: number): this;
81
+ step(dtMs: number): boolean;
82
+ owns(value: MotionValue): boolean;
83
+ interruptValue(value: MotionValue, status?: 'cancelled' | 'interrupted'): boolean;
84
+ }
85
+
86
+ export type PhaseTarget<T = unknown> =
87
+ | MotionValue
88
+ | ((value: T) => void)
89
+ | { set(value: T): void }
90
+ | ({ target: MotionValue | ((value: T) => void) | { set(value: T): void } } & TimelineTrackOptions);
91
+ export type PhaseDefinition = {
92
+ name?: string;
93
+ duration?: number;
94
+ hold?: number;
95
+ easing?: TimelineEasing;
96
+ values?: Record<string, unknown>;
97
+ };
98
+
99
+ export class PhaseTimeline {
100
+ constructor(
101
+ targets: Record<string, PhaseTarget>,
102
+ phases: PhaseDefinition[],
103
+ options?: { defaultDuration?: number; easing?: TimelineEasing },
104
+ );
105
+ readonly names: string[];
106
+ readonly arrivals: Float64Array;
107
+ readonly timeline: Timeline;
108
+ readonly duration: number;
109
+ phaseAt(timeSeconds: number): string;
110
+ player(options?: TimelinePlayerOptions): TimelinePlayer;
111
+ sample(time: number, options?: { velocityScale?: number }): number;
112
+ }
113
+
114
+ export function timeline(options?: { duration?: number; easing?: TimelineEasing }): Timeline;
115
+ export function createPhaseTimeline(
116
+ targets: Record<string, PhaseTarget>,
117
+ phases: PhaseDefinition[],
118
+ options?: { defaultDuration?: number; easing?: TimelineEasing },
119
+ ): PhaseTimeline;
120
+ export function stagger(
121
+ interval: number,
122
+ options?: { start?: number; from?: 'first' | 'last' | 'center' | number; easing?: TimelineEasing },
123
+ ): (index: number, total: number) => number;
124
+
125
+ export class TimelineScrubber {
126
+ constructor(player: TimelinePlayer, options?: {
127
+ progress?: MotionValue;
128
+ engine?: MotionEngine;
129
+ min?: number;
130
+ max?: number;
131
+ snapPoints?: number[];
132
+ pauseOnBind?: boolean;
133
+ inertiaOptions?: import('../../index.js').InertiaOptions;
134
+ });
135
+ readonly player: TimelinePlayer;
136
+ readonly engine: MotionEngine;
137
+ readonly progress: MotionValue;
138
+ readonly min: number;
139
+ readonly max: number;
140
+ controls: import('../../index.js').AnimationControls | null;
141
+ set(value: number, velocity?: number): this;
142
+ seekProgress(progress: number, velocity?: number): this;
143
+ release(options?: import('../../index.js').InertiaOptions & { velocity?: number; snapPoints?: number[] }): import('../../index.js').AnimationControls;
144
+ play(options?: { direction?: 'forward' | 'reverse' }): TimelinePlayer;
145
+ dispose(): void;
146
+ }
147
+ export function createTimelineScrubber(player: TimelinePlayer, options?: ConstructorParameters<typeof TimelineScrubber>[1]): TimelineScrubber;