@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.
- package/ARCHITECTURE.md +470 -0
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/PERFORMANCE.md +151 -0
- package/README.md +630 -0
- package/dist/index.d.ts +474 -0
- package/dist/src/canvas/index.d.ts +15 -0
- package/dist/src/canvas/index.js +67 -0
- package/dist/src/constraints/index.d.ts +33 -0
- package/dist/src/constraints/index.js +346 -0
- package/dist/src/core/bezier.js +51 -0
- package/dist/src/core/composition.js +17 -0
- package/dist/src/core/controls.js +22 -0
- package/dist/src/core/default-engine.js +20 -0
- package/dist/src/core/easing.js +58 -0
- package/dist/src/core/engine.js +1031 -0
- package/dist/src/core/frame-budget.js +30 -0
- package/dist/src/core/index.d.ts +43 -0
- package/dist/src/core/index.js +17 -0
- package/dist/src/core/js-spring-batch.js +57 -0
- package/dist/src/core/kinetics.js +140 -0
- package/dist/src/core/math.js +20 -0
- package/dist/src/core/motion-value.js +53 -0
- package/dist/src/core/planner.js +72 -0
- package/dist/src/core/specs.js +70 -0
- package/dist/src/dom/index.d.ts +41 -0
- package/dist/src/dom/index.js +364 -0
- package/dist/src/gesture/index.d.ts +66 -0
- package/dist/src/gesture/index.js +376 -0
- package/dist/src/index.js +53 -0
- package/dist/src/interpolate/color.js +223 -0
- package/dist/src/interpolate/css.d.ts +13 -0
- package/dist/src/interpolate/css.js +34 -0
- package/dist/src/interpolate/index.d.ts +13 -0
- package/dist/src/interpolate/index.js +55 -0
- package/dist/src/interpolate/transform.js +247 -0
- package/dist/src/layout/index.d.ts +56 -0
- package/dist/src/layout/index.js +485 -0
- package/dist/src/material/index.d.ts +9 -0
- package/dist/src/material/index.js +70 -0
- package/dist/src/path/index.d.ts +37 -0
- package/dist/src/path/index.js +527 -0
- package/dist/src/render/frame-batcher.js +52 -0
- package/dist/src/scroll/index.d.ts +55 -0
- package/dist/src/scroll/index.js +233 -0
- package/dist/src/timeline/index.d.ts +147 -0
- package/dist/src/timeline/index.js +849 -0
- package/dist/src/transition/index.d.ts +88 -0
- package/dist/src/transition/index.js +369 -0
- package/dist/src/wasm/index.d.ts +29 -0
- package/dist/src/wasm/index.js +8 -0
- package/dist/src/wasm/loader.js +55 -0
- package/dist/src/wasm/shared-wasm-spring-batch.js +52 -0
- package/dist/src/wasm/wasm-spring-batch.js +52 -0
- package/dist/src/webgl/index.d.ts +22 -0
- package/dist/src/webgl/index.js +94 -0
- package/dist/src/webgpu/index.d.ts +35 -0
- package/dist/src/webgpu/index.js +73 -0
- package/dist/src/webgpu/spring-batch.js +218 -0
- package/dist/src/worker/index.d.ts +17 -0
- package/dist/src/worker/index.js +1 -0
- package/dist/src/worker/shared-spring-worker.js +218 -0
- package/dist/src/worker/shared-worker.js +75 -0
- package/dist/wasm/kernel-scalar.wasm +0 -0
- package/dist/wasm/kernel-shared-scalar.wasm +0 -0
- package/dist/wasm/kernel-shared-simd.wasm +0 -0
- package/dist/wasm/kernel-simd.wasm +0 -0
- package/package.json +113 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
import { evaluateBezier } from '../core/bezier.js';
|
|
2
|
+
import { compileEasing, evaluateCompiledEasing, derivativeCompiledEasing } from '../core/easing.js';
|
|
3
|
+
import { defaultEngine } from '../core/default-engine.js';
|
|
4
|
+
import { motionValue } from '../core/motion-value.js';
|
|
5
|
+
import { inertia } from '../core/kinetics.js';
|
|
6
|
+
import { curves } from '../core/specs.js';
|
|
7
|
+
import { createInterpolator } from '../interpolate/index.js';
|
|
8
|
+
|
|
9
|
+
const EPSILON = 1e-9;
|
|
10
|
+
|
|
11
|
+
function clamp(value, min, max) {
|
|
12
|
+
return Math.min(max, Math.max(min, value));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isMotionValue(value) {
|
|
16
|
+
return value && typeof value.get === 'function' && typeof value._commit === 'function';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function easingValue(easing, progress) {
|
|
20
|
+
const p = clamp(progress, 0, 1);
|
|
21
|
+
if (typeof easing === 'function') {
|
|
22
|
+
const value = easing(p);
|
|
23
|
+
return Number.isFinite(value) ? value : p;
|
|
24
|
+
}
|
|
25
|
+
if (easing?.kind === 'bezier') return evaluateBezier(easing, p);
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function easingDerivative(easing, progress) {
|
|
30
|
+
const p = clamp(progress, 0, 1);
|
|
31
|
+
if (typeof easing === 'function') {
|
|
32
|
+
const epsilon = 1e-4;
|
|
33
|
+
const lo = Math.max(0, p - epsilon);
|
|
34
|
+
const hi = Math.min(1, p + epsilon);
|
|
35
|
+
if (hi - lo <= EPSILON) return 0;
|
|
36
|
+
const a = easing(lo);
|
|
37
|
+
const b = easing(hi);
|
|
38
|
+
return Number.isFinite(a) && Number.isFinite(b) ? (b - a) / (hi - lo) : 0;
|
|
39
|
+
}
|
|
40
|
+
if (easing?.kind === 'bezier') return evaluateBezierDerivative(easing, p);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeFrames(frames, { duration, easing = curves.linear } = {}) {
|
|
45
|
+
if (!Array.isArray(frames) || frames.length === 0) throw new TypeError('Timeline track requires at least one keyframe.');
|
|
46
|
+
|
|
47
|
+
let normalized;
|
|
48
|
+
const hasKeyframeMetadata = frames.some((frame) => frame && typeof frame === 'object' && !Array.isArray(frame)
|
|
49
|
+
&& ('value' in frame || 'at' in frame || 'time' in frame || 'offset' in frame));
|
|
50
|
+
if (!hasKeyframeMetadata) {
|
|
51
|
+
const total = Number(duration);
|
|
52
|
+
if (!Number.isFinite(total) || total < 0) throw new TypeError('Shorthand keyframes require a finite non-negative options.duration.');
|
|
53
|
+
const denominator = Math.max(1, frames.length - 1);
|
|
54
|
+
normalized = frames.map((value, index) => ({
|
|
55
|
+
at: total * index / denominator,
|
|
56
|
+
value,
|
|
57
|
+
easing,
|
|
58
|
+
order: index,
|
|
59
|
+
}));
|
|
60
|
+
} else {
|
|
61
|
+
normalized = frames.map((frame, index) => {
|
|
62
|
+
if (!frame || typeof frame !== 'object' || !('value' in frame)) throw new TypeError('Keyframes must contain a value.');
|
|
63
|
+
let at;
|
|
64
|
+
if (Number.isFinite(frame.at)) at = Number(frame.at);
|
|
65
|
+
else if (Number.isFinite(frame.time)) at = Number(frame.time);
|
|
66
|
+
else if (Number.isFinite(frame.offset) && Number.isFinite(duration)) at = Number(frame.offset) * Number(duration);
|
|
67
|
+
else throw new TypeError('Each keyframe requires at/time, or offset with options.duration.');
|
|
68
|
+
if (at < 0) throw new RangeError('Keyframe time cannot be negative.');
|
|
69
|
+
return { at, value: frame.value, easing: frame.easing ?? easing, order: index };
|
|
70
|
+
});
|
|
71
|
+
normalized.sort((a, b) => (a.at - b.at) || (a.order - b.order));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const collapsed = [];
|
|
75
|
+
for (const frame of normalized) {
|
|
76
|
+
const last = collapsed[collapsed.length - 1];
|
|
77
|
+
if (last && Math.abs(last.at - frame.at) <= EPSILON) collapsed[collapsed.length - 1] = frame;
|
|
78
|
+
else collapsed.push(frame);
|
|
79
|
+
}
|
|
80
|
+
return collapsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findSegment(times, time) {
|
|
84
|
+
let low = 0;
|
|
85
|
+
let high = times.length - 1;
|
|
86
|
+
while (low + 1 < high) {
|
|
87
|
+
const middle = (low + high) >> 1;
|
|
88
|
+
if (times[middle] <= time) low = middle;
|
|
89
|
+
else high = middle;
|
|
90
|
+
}
|
|
91
|
+
return low;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class NumericKeyframeTrack {
|
|
95
|
+
constructor(target, frames) {
|
|
96
|
+
this.target = target;
|
|
97
|
+
this.motionValue = isMotionValue(target) ? target : null;
|
|
98
|
+
this.writer = this.motionValue
|
|
99
|
+
? (value, velocity) => this.motionValue._commit(value, velocity)
|
|
100
|
+
: typeof target === 'function'
|
|
101
|
+
? target
|
|
102
|
+
: target && typeof target.set === 'function'
|
|
103
|
+
? (value, velocity) => target.set(value, velocity)
|
|
104
|
+
: null;
|
|
105
|
+
if (!this.writer) throw new TypeError('Numeric timeline target must be a MotionValue, callback, or settable object.');
|
|
106
|
+
|
|
107
|
+
this.times = new Float64Array(frames.length);
|
|
108
|
+
this.values = new Float64Array(frames.length);
|
|
109
|
+
this.easings = new Array(Math.max(0, frames.length - 1));
|
|
110
|
+
for (let i = 0; i < frames.length; i += 1) {
|
|
111
|
+
const value = Number(frames[i].value);
|
|
112
|
+
if (!Number.isFinite(value)) throw new TypeError('Numeric keyframes require finite values.');
|
|
113
|
+
this.times[i] = frames[i].at;
|
|
114
|
+
this.values[i] = value;
|
|
115
|
+
if (i < frames.length - 1) this.easings[i] = compileEasing(frames[i].easing);
|
|
116
|
+
}
|
|
117
|
+
this.duration = this.times[this.times.length - 1];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
sample(time, velocityScale = 0) {
|
|
121
|
+
const count = this.times.length;
|
|
122
|
+
if (count === 1 || time <= this.times[0]) {
|
|
123
|
+
this.writer(this.values[0], 0);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const last = count - 1;
|
|
127
|
+
if (time >= this.times[last]) {
|
|
128
|
+
this.writer(this.values[last], 0);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const index = findSegment(this.times, time);
|
|
133
|
+
const startTime = this.times[index];
|
|
134
|
+
const endTime = this.times[index + 1];
|
|
135
|
+
const span = endTime - startTime;
|
|
136
|
+
if (span <= EPSILON) {
|
|
137
|
+
this.writer(this.values[index + 1], 0);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const progress = (time - startTime) / span;
|
|
141
|
+
const easing = this.easings[index];
|
|
142
|
+
const eased = evaluateCompiledEasing(easing, progress);
|
|
143
|
+
const from = this.values[index];
|
|
144
|
+
const delta = this.values[index + 1] - from;
|
|
145
|
+
const value = from + delta * eased;
|
|
146
|
+
const velocity = velocityScale === 0 ? 0 : delta * derivativeCompiledEasing(easing, progress) / span * velocityScale;
|
|
147
|
+
this.writer(value, velocity);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
zeroVelocity() {
|
|
151
|
+
if (this.motionValue) this.motionValue._commit(this.motionValue.get(), 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
owns(value) {
|
|
155
|
+
return this.motionValue === value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
stopConflict(engine) {
|
|
159
|
+
if (this.motionValue) engine.stop(this.motionValue, 'interrupted');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class GenericKeyframeTrack {
|
|
164
|
+
constructor(target, frames, interpolationOptions) {
|
|
165
|
+
if (isMotionValue(target)) throw new TypeError('MotionValue timeline tracks require numeric keyframes. Use a callback for structured values.');
|
|
166
|
+
this.writer = typeof target === 'function'
|
|
167
|
+
? target
|
|
168
|
+
: target && typeof target.set === 'function'
|
|
169
|
+
? (value) => target.set(value)
|
|
170
|
+
: null;
|
|
171
|
+
if (!this.writer) throw new TypeError('Interpolated timeline target must be a callback or settable object.');
|
|
172
|
+
|
|
173
|
+
this.times = new Float64Array(frames.length);
|
|
174
|
+
this.values = frames.map((frame) => frame.value);
|
|
175
|
+
this.easings = new Array(Math.max(0, frames.length - 1));
|
|
176
|
+
this.mixers = new Array(Math.max(0, frames.length - 1));
|
|
177
|
+
for (let i = 0; i < frames.length; i += 1) {
|
|
178
|
+
this.times[i] = frames[i].at;
|
|
179
|
+
if (i < frames.length - 1) {
|
|
180
|
+
this.easings[i] = compileEasing(frames[i].easing);
|
|
181
|
+
this.mixers[i] = createInterpolator(frames[i].value, frames[i + 1].value, interpolationOptions);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.duration = this.times[this.times.length - 1];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
sample(time) {
|
|
188
|
+
const count = this.times.length;
|
|
189
|
+
if (count === 1 || time <= this.times[0]) {
|
|
190
|
+
this.writer(this.values[0]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const last = count - 1;
|
|
194
|
+
if (time >= this.times[last]) {
|
|
195
|
+
this.writer(this.values[last]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const index = findSegment(this.times, time);
|
|
199
|
+
const span = this.times[index + 1] - this.times[index];
|
|
200
|
+
const progress = span <= EPSILON ? 1 : (time - this.times[index]) / span;
|
|
201
|
+
this.writer(this.mixers[index](evaluateCompiledEasing(this.easings[index], progress)));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
zeroVelocity() {}
|
|
205
|
+
owns() { return false; }
|
|
206
|
+
stopConflict() {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
class TimelineClip {
|
|
210
|
+
constructor(timeline, { at = 0, speed = 1, fill = 'both' } = {}) {
|
|
211
|
+
if (!(timeline instanceof Timeline)) throw new TypeError('Timeline.add() requires another Timeline.');
|
|
212
|
+
if (!Number.isFinite(at) || at < 0) throw new RangeError('Clip start time must be finite and non-negative.');
|
|
213
|
+
if (!Number.isFinite(speed) || speed <= 0) throw new RangeError('Clip speed must be a finite positive number.');
|
|
214
|
+
if (!['none', 'forwards', 'backwards', 'both'].includes(fill)) throw new TypeError("Clip fill must be 'none', 'forwards', 'backwards', or 'both'.");
|
|
215
|
+
this.timeline = timeline;
|
|
216
|
+
this.at = at;
|
|
217
|
+
this.speed = speed;
|
|
218
|
+
this.fill = fill;
|
|
219
|
+
this.duration = timeline.duration / speed;
|
|
220
|
+
this.end = at + this.duration;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
sample(parentTime, velocityScale) {
|
|
224
|
+
if (parentTime < this.at) {
|
|
225
|
+
if (this.fill === 'backwards' || this.fill === 'both') this.timeline.sample(0, { velocityScale: 0 });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (parentTime > this.end) {
|
|
229
|
+
if (this.fill === 'forwards' || this.fill === 'both') this.timeline.sample(this.timeline.duration, { velocityScale: 0 });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.timeline.sample((parentTime - this.at) * this.speed, { velocityScale: velocityScale * this.speed });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
zeroVelocities() { this.timeline.zeroVelocities(); }
|
|
236
|
+
owns(value) { return this.timeline.hasMotionValue(value); }
|
|
237
|
+
stopConflicts(engine) { this.timeline.stopConflicts(engine); }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export class Timeline {
|
|
241
|
+
constructor({ duration = 0, easing = curves.linear } = {}) {
|
|
242
|
+
if (!Number.isFinite(duration) || duration < 0) throw new RangeError('Timeline duration must be finite and non-negative.');
|
|
243
|
+
this.defaultEasing = easing;
|
|
244
|
+
this.explicitDuration = duration;
|
|
245
|
+
this._duration = duration;
|
|
246
|
+
this.tracks = [];
|
|
247
|
+
this.clips = [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get duration() { return this._duration; }
|
|
251
|
+
|
|
252
|
+
track(target, frames, options = {}) {
|
|
253
|
+
const normalized = normalizeFrames(frames, { duration: options.duration, easing: options.easing ?? this.defaultEasing });
|
|
254
|
+
const numeric = normalized.every((frame) => typeof frame.value === 'number');
|
|
255
|
+
const fastNumeric = numeric && options.type == null && typeof options.interpolate !== 'function';
|
|
256
|
+
const track = fastNumeric && (isMotionValue(target) || typeof target === 'function' || typeof target?.set === 'function')
|
|
257
|
+
? new NumericKeyframeTrack(target, normalized)
|
|
258
|
+
: new GenericKeyframeTrack(target, normalized, options);
|
|
259
|
+
this.tracks.push(track);
|
|
260
|
+
this._duration = Math.max(this._duration, track.duration);
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
keyframes(target, frames, options = {}) {
|
|
265
|
+
return this.track(target, frames, options);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fromTo(target, from, to, { at = 0, duration = 0.3, easing = this.defaultEasing, ...options } = {}) {
|
|
269
|
+
const start = Number(at);
|
|
270
|
+
const span = Number(duration);
|
|
271
|
+
if (!Number.isFinite(start) || start < 0) throw new RangeError('Timeline fromTo() start must be finite and non-negative.');
|
|
272
|
+
if (!Number.isFinite(span) || span < 0) throw new RangeError('Timeline fromTo() duration must be finite and non-negative.');
|
|
273
|
+
return this.track(target, [
|
|
274
|
+
{ at: start, value: from, easing },
|
|
275
|
+
{ at: start + span, value: to },
|
|
276
|
+
], options);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
to(target, to, { from, ...options } = {}) {
|
|
280
|
+
let startValue = from;
|
|
281
|
+
if (startValue === undefined) {
|
|
282
|
+
if (isMotionValue(target)) startValue = target.get();
|
|
283
|
+
else if (target && typeof target.get === 'function') startValue = target.get();
|
|
284
|
+
else throw new TypeError('Timeline.to() requires options.from for callback-only targets.');
|
|
285
|
+
}
|
|
286
|
+
return this.fromTo(target, startValue, to, options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
add(child, options = {}) {
|
|
290
|
+
const clip = new TimelineClip(child, options);
|
|
291
|
+
this.clips.push(clip);
|
|
292
|
+
this._duration = Math.max(this._duration, clip.end);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
sample(time, { velocityScale = 0 } = {}) {
|
|
297
|
+
const local = clamp(Number.isFinite(time) ? time : 0, 0, this.duration);
|
|
298
|
+
for (const track of this.tracks) track.sample(local, velocityScale);
|
|
299
|
+
for (const clip of this.clips) clip.sample(local, velocityScale);
|
|
300
|
+
return local;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
zeroVelocities() {
|
|
304
|
+
for (const track of this.tracks) track.zeroVelocity();
|
|
305
|
+
for (const clip of this.clips) clip.zeroVelocities();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
hasMotionValue(value) {
|
|
309
|
+
for (const track of this.tracks) if (track.owns(value)) return true;
|
|
310
|
+
for (const clip of this.clips) if (clip.owns(value)) return true;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
stopConflicts(engine) {
|
|
315
|
+
for (const track of this.tracks) track.stopConflict(engine);
|
|
316
|
+
for (const clip of this.clips) clip.stopConflicts(engine);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
player(options = {}) {
|
|
320
|
+
return new TimelinePlayer(this, options);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function deferred() {
|
|
325
|
+
let resolve;
|
|
326
|
+
let settled = false;
|
|
327
|
+
const promise = new Promise((resolver) => { resolve = resolver; });
|
|
328
|
+
return {
|
|
329
|
+
promise,
|
|
330
|
+
settle(value) {
|
|
331
|
+
if (settled) return;
|
|
332
|
+
settled = true;
|
|
333
|
+
resolve(value);
|
|
334
|
+
},
|
|
335
|
+
get settled() { return settled; },
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function validateDirection(direction) {
|
|
340
|
+
if (!['normal', 'reverse', 'alternate', 'alternate-reverse'].includes(direction)) {
|
|
341
|
+
throw new TypeError("Timeline direction must be 'normal', 'reverse', 'alternate', or 'alternate-reverse'.");
|
|
342
|
+
}
|
|
343
|
+
return direction;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function directionSign(direction, iteration) {
|
|
347
|
+
switch (direction) {
|
|
348
|
+
case 'reverse': return -1;
|
|
349
|
+
case 'alternate': return iteration % 2 === 0 ? 1 : -1;
|
|
350
|
+
case 'alternate-reverse': return iteration % 2 === 0 ? -1 : 1;
|
|
351
|
+
default: return 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export class TimelinePlayer {
|
|
356
|
+
constructor(timeline, {
|
|
357
|
+
engine = defaultEngine,
|
|
358
|
+
autoplay = false,
|
|
359
|
+
playbackRate = 1,
|
|
360
|
+
iterations = 1,
|
|
361
|
+
direction = 'normal',
|
|
362
|
+
onUpdate,
|
|
363
|
+
onRepeat,
|
|
364
|
+
onComplete,
|
|
365
|
+
} = {}) {
|
|
366
|
+
if (!(timeline instanceof Timeline)) throw new TypeError('TimelinePlayer requires a Timeline.');
|
|
367
|
+
if (!engine || typeof engine.addDriver !== 'function') throw new TypeError('TimelinePlayer requires a MotionEngine-like engine.');
|
|
368
|
+
if (!Number.isFinite(playbackRate)) throw new TypeError('playbackRate must be finite.');
|
|
369
|
+
if (!(iterations === Infinity || (Number.isFinite(iterations) && iterations >= 1))) throw new RangeError('iterations must be >= 1 or Infinity.');
|
|
370
|
+
|
|
371
|
+
this.timeline = timeline;
|
|
372
|
+
this.engine = engine;
|
|
373
|
+
this.playbackRate = playbackRate;
|
|
374
|
+
this.iterations = iterations === Infinity ? Infinity : Math.floor(iterations);
|
|
375
|
+
this.direction = validateDirection(direction);
|
|
376
|
+
this.onUpdate = typeof onUpdate === 'function' ? onUpdate : null;
|
|
377
|
+
this.onRepeat = typeof onRepeat === 'function' ? onRepeat : null;
|
|
378
|
+
this.onComplete = typeof onComplete === 'function' ? onComplete : null;
|
|
379
|
+
|
|
380
|
+
this.state = 'idle';
|
|
381
|
+
this.elapsedTime = 0;
|
|
382
|
+
this.currentTime = 0;
|
|
383
|
+
this.progress = 0;
|
|
384
|
+
this.iteration = 0;
|
|
385
|
+
this._registered = false;
|
|
386
|
+
this._deferred = deferred();
|
|
387
|
+
|
|
388
|
+
this._sampleRaw(0, 0);
|
|
389
|
+
if (autoplay) this.play();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
get duration() { return this.timeline.duration; }
|
|
393
|
+
get totalDuration() { return this.iterations === Infinity ? Infinity : this.duration * this.iterations; }
|
|
394
|
+
get finished() { return this._deferred.promise; }
|
|
395
|
+
get running() { return this.state === 'running'; }
|
|
396
|
+
|
|
397
|
+
_resetDeferredIfNeeded() {
|
|
398
|
+
if (this._deferred.settled) this._deferred = deferred();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
_register() {
|
|
402
|
+
if (this._registered) return;
|
|
403
|
+
this._registered = true;
|
|
404
|
+
this.engine.addDriver(this);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_unregister() {
|
|
408
|
+
if (!this._registered) return;
|
|
409
|
+
this._registered = false;
|
|
410
|
+
this.engine.removeDriver(this);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
_mapping(rawTime, traversal = 1) {
|
|
414
|
+
const duration = this.duration;
|
|
415
|
+
if (duration <= EPSILON) return { iteration: 0, local: 0, sign: 1 };
|
|
416
|
+
|
|
417
|
+
let raw = Math.max(0, rawTime);
|
|
418
|
+
const total = this.totalDuration;
|
|
419
|
+
if (Number.isFinite(total)) raw = Math.min(raw, total);
|
|
420
|
+
|
|
421
|
+
let iteration;
|
|
422
|
+
let phase;
|
|
423
|
+
if (Number.isFinite(total) && raw >= total - EPSILON) {
|
|
424
|
+
iteration = Math.max(0, this.iterations - 1);
|
|
425
|
+
phase = duration;
|
|
426
|
+
} else {
|
|
427
|
+
const boundary = Math.round(raw / duration);
|
|
428
|
+
const onBoundary = raw > 0 && Math.abs(raw - boundary * duration) <= EPSILON;
|
|
429
|
+
if (traversal < 0 && onBoundary) {
|
|
430
|
+
iteration = Math.max(0, boundary - 1);
|
|
431
|
+
phase = duration;
|
|
432
|
+
} else {
|
|
433
|
+
iteration = Math.max(0, Math.floor(raw / duration));
|
|
434
|
+
phase = raw - iteration * duration;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const sign = directionSign(this.direction, iteration);
|
|
438
|
+
const local = sign > 0 ? phase : duration - phase;
|
|
439
|
+
return { iteration, local: clamp(local, 0, duration), sign };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
_sampleRaw(rawTime, realTimeScale) {
|
|
443
|
+
const mapping = this._mapping(rawTime, realTimeScale);
|
|
444
|
+
this.elapsedTime = rawTime;
|
|
445
|
+
this.iteration = mapping.iteration;
|
|
446
|
+
this.currentTime = mapping.local;
|
|
447
|
+
this.progress = this.duration <= EPSILON ? 1 : mapping.local / this.duration;
|
|
448
|
+
this.timeline.sample(mapping.local, { velocityScale: mapping.sign * realTimeScale });
|
|
449
|
+
this.onUpdate?.(this);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
_stop(status, { preserveVelocity = false } = {}) {
|
|
453
|
+
this._unregister();
|
|
454
|
+
if (!preserveVelocity) this.timeline.zeroVelocities();
|
|
455
|
+
this.state = status;
|
|
456
|
+
const result = {
|
|
457
|
+
status,
|
|
458
|
+
currentTime: this.currentTime,
|
|
459
|
+
elapsedTime: this.elapsedTime,
|
|
460
|
+
progress: this.progress,
|
|
461
|
+
iteration: this.iteration,
|
|
462
|
+
};
|
|
463
|
+
this._deferred.settle(result);
|
|
464
|
+
if (status === 'finished') this.onComplete?.(this);
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
play() {
|
|
469
|
+
if (this.state === 'running') return this;
|
|
470
|
+
this._resetDeferredIfNeeded();
|
|
471
|
+
this.timeline.stopConflicts(this.engine);
|
|
472
|
+
|
|
473
|
+
const total = this.totalDuration;
|
|
474
|
+
if (this.playbackRate >= 0 && Number.isFinite(total) && this.elapsedTime >= total - EPSILON) this.elapsedTime = 0;
|
|
475
|
+
if (this.playbackRate < 0 && this.elapsedTime <= EPSILON) {
|
|
476
|
+
this.elapsedTime = Number.isFinite(total) ? total : this.duration;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.state = 'running';
|
|
480
|
+
this._sampleRaw(this.elapsedTime, this.playbackRate);
|
|
481
|
+
if (this.duration <= EPSILON || this.playbackRate === 0) {
|
|
482
|
+
if (this.duration <= EPSILON) this._stop('finished');
|
|
483
|
+
else this.pause();
|
|
484
|
+
return this;
|
|
485
|
+
}
|
|
486
|
+
this._register();
|
|
487
|
+
return this;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
pause() {
|
|
491
|
+
if (this.state !== 'running') return this;
|
|
492
|
+
this._unregister();
|
|
493
|
+
this.timeline.zeroVelocities();
|
|
494
|
+
this.state = 'paused';
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
cancel() {
|
|
499
|
+
if (this.state === 'finished' || this.state === 'cancelled' || this.state === 'interrupted') return this;
|
|
500
|
+
this._resetDeferredIfNeeded();
|
|
501
|
+
this._stop('cancelled');
|
|
502
|
+
return this;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
interruptValue(value, status = 'interrupted') {
|
|
506
|
+
if (!this.owns(value) || this.state !== 'running') return false;
|
|
507
|
+
const resolved = status === 'cancelled' ? 'cancelled' : 'interrupted';
|
|
508
|
+
this._stop(resolved, { preserveVelocity: true });
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
owns(value) {
|
|
513
|
+
return this.timeline.hasMotionValue(value);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
finish() {
|
|
517
|
+
if (this.state === 'finished') return this;
|
|
518
|
+
this._resetDeferredIfNeeded();
|
|
519
|
+
const duration = this.duration;
|
|
520
|
+
if (duration <= EPSILON) {
|
|
521
|
+
this._sampleRaw(0, 0);
|
|
522
|
+
this._stop('finished');
|
|
523
|
+
return this;
|
|
524
|
+
}
|
|
525
|
+
if (this.playbackRate < 0) this._sampleRaw(0, 0);
|
|
526
|
+
else if (Number.isFinite(this.totalDuration)) this._sampleRaw(this.totalDuration, 0);
|
|
527
|
+
else {
|
|
528
|
+
const sign = directionSign(this.direction, this.iteration);
|
|
529
|
+
const local = sign > 0 ? duration : 0;
|
|
530
|
+
this.currentTime = local;
|
|
531
|
+
this.progress = local / duration;
|
|
532
|
+
this.elapsedTime = (this.iteration + 1) * duration;
|
|
533
|
+
this.timeline.sample(local, { velocityScale: 0 });
|
|
534
|
+
this.onUpdate?.(this);
|
|
535
|
+
}
|
|
536
|
+
this._stop('finished');
|
|
537
|
+
return this;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
reverse() {
|
|
541
|
+
this.playbackRate = this.playbackRate === 0 ? -1 : -this.playbackRate;
|
|
542
|
+
return this;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
setPlaybackRate(rate) {
|
|
546
|
+
if (!Number.isFinite(rate)) throw new TypeError('playbackRate must be finite.');
|
|
547
|
+
this.playbackRate = rate;
|
|
548
|
+
if (rate === 0 && this.state === 'running') this.pause();
|
|
549
|
+
return this;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
seek(timeSeconds, { iteration = this.iteration } = {}) {
|
|
553
|
+
const duration = this.duration;
|
|
554
|
+
if (duration <= EPSILON) {
|
|
555
|
+
this._sampleRaw(0, 0);
|
|
556
|
+
return this;
|
|
557
|
+
}
|
|
558
|
+
const maxIteration = this.iterations === Infinity ? Math.max(0, Math.floor(iteration)) : this.iterations - 1;
|
|
559
|
+
const resolvedIteration = clamp(Math.floor(iteration), 0, maxIteration);
|
|
560
|
+
const local = clamp(Number(timeSeconds) || 0, 0, duration);
|
|
561
|
+
const sign = directionSign(this.direction, resolvedIteration);
|
|
562
|
+
const rawPhase = sign > 0 ? local : duration - local;
|
|
563
|
+
const raw = resolvedIteration * duration + rawPhase;
|
|
564
|
+
this._sampleRaw(raw, 0);
|
|
565
|
+
return this;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
seekProgress(progress, options) {
|
|
569
|
+
return this.seek(clamp(Number(progress) || 0, 0, 1) * this.duration, options);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
scrub(progress, options) {
|
|
573
|
+
return this.seekProgress(progress, options);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
seekElapsed(elapsedSeconds) {
|
|
577
|
+
let raw = Math.max(0, Number(elapsedSeconds) || 0);
|
|
578
|
+
if (Number.isFinite(this.totalDuration)) raw = Math.min(raw, this.totalDuration);
|
|
579
|
+
this._sampleRaw(raw, 0);
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
step(dtMs) {
|
|
584
|
+
if (this.state !== 'running') return false;
|
|
585
|
+
if (!Number.isFinite(dtMs) || dtMs <= 0) return true;
|
|
586
|
+
const duration = this.duration;
|
|
587
|
+
if (duration <= EPSILON) {
|
|
588
|
+
this.finish();
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const previousIteration = this.iteration;
|
|
593
|
+
const deltaSeconds = dtMs / 1000 * this.playbackRate;
|
|
594
|
+
let next = this.elapsedTime + deltaSeconds;
|
|
595
|
+
const total = this.totalDuration;
|
|
596
|
+
|
|
597
|
+
if (this.playbackRate > 0 && Number.isFinite(total) && next >= total - EPSILON) {
|
|
598
|
+
this._sampleRaw(total, 0);
|
|
599
|
+
this._stop('finished');
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
if (this.playbackRate < 0 && next <= EPSILON) {
|
|
603
|
+
this._sampleRaw(0, 0);
|
|
604
|
+
this._stop('finished');
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (next < 0) next = 0;
|
|
609
|
+
this._sampleRaw(next, this.playbackRate);
|
|
610
|
+
|
|
611
|
+
if (this.onRepeat && this.iteration !== previousIteration) {
|
|
612
|
+
this.onRepeat(this.iteration, this, this.iteration - previousIteration);
|
|
613
|
+
}
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
onEngineDispose() {
|
|
618
|
+
if (this.state === 'running') {
|
|
619
|
+
this._registered = false;
|
|
620
|
+
this.timeline.zeroVelocities();
|
|
621
|
+
this.state = 'cancelled';
|
|
622
|
+
this._deferred.settle({
|
|
623
|
+
status: 'cancelled',
|
|
624
|
+
currentTime: this.currentTime,
|
|
625
|
+
elapsedTime: this.elapsedTime,
|
|
626
|
+
progress: this.progress,
|
|
627
|
+
iteration: this.iteration,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export class PhaseTimeline {
|
|
634
|
+
constructor(targets, phases, {
|
|
635
|
+
defaultDuration = 0.2,
|
|
636
|
+
easing = curves.smooth,
|
|
637
|
+
} = {}) {
|
|
638
|
+
if (!targets || typeof targets !== 'object') throw new TypeError('PhaseTimeline targets must be an object.');
|
|
639
|
+
if (!Array.isArray(phases) || phases.length === 0) throw new TypeError('PhaseTimeline requires at least one phase.');
|
|
640
|
+
if (!Number.isFinite(defaultDuration) || defaultDuration < 0) throw new RangeError('defaultDuration must be non-negative.');
|
|
641
|
+
|
|
642
|
+
this.names = phases.map((phase, index) => String(phase.name ?? index));
|
|
643
|
+
this.arrivals = new Float64Array(phases.length);
|
|
644
|
+
this.timeline = new Timeline({ easing });
|
|
645
|
+
|
|
646
|
+
const entries = Object.entries(targets).map(([key, entry]) => {
|
|
647
|
+
if (entry && typeof entry === 'object' && 'target' in entry) return [key, entry.target, entry];
|
|
648
|
+
return [key, entry, {}];
|
|
649
|
+
});
|
|
650
|
+
const framesByKey = new Map(entries.map(([key]) => [key, []]));
|
|
651
|
+
const current = new Map();
|
|
652
|
+
|
|
653
|
+
for (const [key, target] of entries) {
|
|
654
|
+
const initial = phases[0].values?.[key];
|
|
655
|
+
if (initial === undefined) {
|
|
656
|
+
if (isMotionValue(target)) current.set(key, target.get());
|
|
657
|
+
else throw new TypeError(`Initial phase is missing a value for '${key}'.`);
|
|
658
|
+
} else current.set(key, initial);
|
|
659
|
+
framesByKey.get(key).push({ at: 0, value: current.get(key), easing: phases[1]?.easing ?? easing });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let time = 0;
|
|
663
|
+
const firstHold = Math.max(0, Number(phases[0].hold) || 0);
|
|
664
|
+
if (firstHold > 0) {
|
|
665
|
+
time += firstHold;
|
|
666
|
+
for (const [key] of entries) framesByKey.get(key).push({ at: time, value: current.get(key), easing: phases[1]?.easing ?? easing });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
for (let index = 1; index < phases.length; index += 1) {
|
|
670
|
+
const phase = phases[index];
|
|
671
|
+
const transition = phase.duration == null ? defaultDuration : Math.max(0, Number(phase.duration) || 0);
|
|
672
|
+
const phaseEasing = phase.easing ?? easing;
|
|
673
|
+
for (const [key] of entries) {
|
|
674
|
+
const frames = framesByKey.get(key);
|
|
675
|
+
if (frames.length) frames[frames.length - 1].easing = phaseEasing;
|
|
676
|
+
}
|
|
677
|
+
time += transition;
|
|
678
|
+
this.arrivals[index] = time;
|
|
679
|
+
for (const [key] of entries) {
|
|
680
|
+
if (phase.values?.[key] !== undefined) current.set(key, phase.values[key]);
|
|
681
|
+
framesByKey.get(key).push({ at: time, value: current.get(key), easing: phases[index + 1]?.easing ?? easing });
|
|
682
|
+
}
|
|
683
|
+
const hold = Math.max(0, Number(phase.hold) || 0);
|
|
684
|
+
if (hold > 0) {
|
|
685
|
+
time += hold;
|
|
686
|
+
for (const [key] of entries) framesByKey.get(key).push({ at: time, value: current.get(key), easing: phases[index + 1]?.easing ?? easing });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const [key, target, options] of entries) this.timeline.track(target, framesByKey.get(key), options);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
get duration() { return this.timeline.duration; }
|
|
694
|
+
|
|
695
|
+
phaseAt(timeSeconds) {
|
|
696
|
+
const time = clamp(Number(timeSeconds) || 0, 0, this.duration);
|
|
697
|
+
let index = 0;
|
|
698
|
+
for (let i = 1; i < this.arrivals.length; i += 1) {
|
|
699
|
+
if (this.arrivals[i] <= time + EPSILON) index = i;
|
|
700
|
+
else break;
|
|
701
|
+
}
|
|
702
|
+
return this.names[index];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
player(options = {}) { return this.timeline.player(options); }
|
|
706
|
+
sample(time, options) { return this.timeline.sample(time, options); }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function timeline(options) {
|
|
710
|
+
return new Timeline(options);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function createPhaseTimeline(targets, phases, options) {
|
|
714
|
+
return new PhaseTimeline(targets, phases, options);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function stagger(interval, {
|
|
718
|
+
start = 0,
|
|
719
|
+
from = 'first',
|
|
720
|
+
easing,
|
|
721
|
+
} = {}) {
|
|
722
|
+
const spacing = Number(interval);
|
|
723
|
+
if (!Number.isFinite(spacing) || spacing < 0) throw new RangeError('stagger interval must be a finite non-negative number.');
|
|
724
|
+
const base = Number(start);
|
|
725
|
+
if (!Number.isFinite(base)) throw new TypeError('stagger start must be finite.');
|
|
726
|
+
|
|
727
|
+
return (index, total) => {
|
|
728
|
+
const count = Math.max(1, Math.floor(total));
|
|
729
|
+
const i = clamp(Math.floor(index), 0, count - 1);
|
|
730
|
+
let rank;
|
|
731
|
+
if (typeof from === 'number' && Number.isFinite(from)) rank = Math.abs(i - clamp(Math.floor(from), 0, count - 1));
|
|
732
|
+
else if (from === 'last') rank = count - 1 - i;
|
|
733
|
+
else if (from === 'center') rank = Math.abs(i - (count - 1) / 2);
|
|
734
|
+
else rank = i;
|
|
735
|
+
|
|
736
|
+
if (!easing || count <= 1) return base + rank * spacing;
|
|
737
|
+
const maxRank = from === 'center' ? Math.max(0.5, (count - 1) / 2) : count - 1;
|
|
738
|
+
const normalized = maxRank <= EPSILON ? 0 : rank / maxRank;
|
|
739
|
+
return base + easingValue(easing, normalized) * maxRank * spacing;
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
function nearestSnap(value, points) {
|
|
745
|
+
if (!Array.isArray(points) || points.length === 0) return value;
|
|
746
|
+
let best = Number(points[0]);
|
|
747
|
+
let distance = Math.abs(value - best);
|
|
748
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
749
|
+
const candidate = Number(points[index]);
|
|
750
|
+
if (!Number.isFinite(candidate)) continue;
|
|
751
|
+
const nextDistance = Math.abs(value - candidate);
|
|
752
|
+
if (nextDistance < distance) {
|
|
753
|
+
best = candidate;
|
|
754
|
+
distance = nextDistance;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return Number.isFinite(best) ? best : value;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Maps an ordinary numeric MotionValue onto TimelinePlayer progress. The input
|
|
762
|
+
* domain can be pixels or any other scalar range, which lets DragController
|
|
763
|
+
* drive a timeline without the timeline package depending on DOM/gestures.
|
|
764
|
+
*/
|
|
765
|
+
export class TimelineScrubber {
|
|
766
|
+
constructor(player, {
|
|
767
|
+
progress,
|
|
768
|
+
engine = player?.engine ?? defaultEngine,
|
|
769
|
+
min = 0,
|
|
770
|
+
max = 1,
|
|
771
|
+
snapPoints = [min, max],
|
|
772
|
+
pauseOnBind = true,
|
|
773
|
+
inertiaOptions = {},
|
|
774
|
+
} = {}) {
|
|
775
|
+
if (!(player instanceof TimelinePlayer)) throw new TypeError('TimelineScrubber requires a TimelinePlayer.');
|
|
776
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) throw new RangeError('TimelineScrubber max must be greater than min.');
|
|
777
|
+
this.player = player;
|
|
778
|
+
this.engine = engine;
|
|
779
|
+
this.min = Number(min);
|
|
780
|
+
this.max = Number(max);
|
|
781
|
+
this.snapPoints = Array.from(snapPoints ?? [], Number).filter(Number.isFinite);
|
|
782
|
+
this.inertiaOptions = { ...inertiaOptions };
|
|
783
|
+
this.progress = progress ?? motionValue(this.min + player.progress * (this.max - this.min));
|
|
784
|
+
if (!isMotionValue(this.progress)) throw new TypeError('TimelineScrubber progress must be a MotionValue.');
|
|
785
|
+
if (pauseOnBind) player.pause();
|
|
786
|
+
this.unsubscribe = this.progress.subscribeValue((value) => this.#sample(value));
|
|
787
|
+
this.controls = null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
#sample(value) {
|
|
791
|
+
const normalized = clamp((value - this.min) / (this.max - this.min), 0, 1);
|
|
792
|
+
this.player.seekProgress(normalized);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
set(value, velocity = 0) {
|
|
796
|
+
this.controls?.cancel?.();
|
|
797
|
+
this.controls = null;
|
|
798
|
+
this.progress.set(clamp(Number(value) || 0, this.min, this.max), Number(velocity) || 0);
|
|
799
|
+
return this;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
seekProgress(progress, velocity = 0) {
|
|
803
|
+
const normalized = clamp(Number(progress) || 0, 0, 1);
|
|
804
|
+
return this.set(this.min + normalized * (this.max - this.min), velocity * (this.max - this.min));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
release({
|
|
808
|
+
velocity = this.progress.getVelocity(),
|
|
809
|
+
snapPoints = this.snapPoints,
|
|
810
|
+
...options
|
|
811
|
+
} = {}) {
|
|
812
|
+
this.player.pause();
|
|
813
|
+
const snaps = Array.from(snapPoints ?? [], Number).filter(Number.isFinite);
|
|
814
|
+
const userModify = options.modifyTarget ?? this.inertiaOptions.modifyTarget;
|
|
815
|
+
const spec = inertia({
|
|
816
|
+
...this.inertiaOptions,
|
|
817
|
+
...options,
|
|
818
|
+
velocity,
|
|
819
|
+
min: this.min,
|
|
820
|
+
max: this.max,
|
|
821
|
+
modifyTarget: (target) => {
|
|
822
|
+
const modified = typeof userModify === 'function' ? Number(userModify(target)) : target;
|
|
823
|
+
return nearestSnap(Number.isFinite(modified) ? modified : target, snaps);
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
this.controls = this.engine.animateVelocity(this.progress, spec);
|
|
827
|
+
return this.controls;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
play({ direction } = {}) {
|
|
831
|
+
this.controls?.cancel?.();
|
|
832
|
+
this.controls = null;
|
|
833
|
+
if (direction === 'forward') this.player.setPlaybackRate(Math.abs(this.player.playbackRate || 1));
|
|
834
|
+
else if (direction === 'reverse') this.player.setPlaybackRate(-Math.abs(this.player.playbackRate || 1));
|
|
835
|
+
this.player.play();
|
|
836
|
+
return this.player;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
dispose() {
|
|
840
|
+
this.controls?.cancel?.();
|
|
841
|
+
this.controls = null;
|
|
842
|
+
this.unsubscribe?.();
|
|
843
|
+
this.unsubscribe = null;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function createTimelineScrubber(player, options) {
|
|
848
|
+
return new TimelineScrubber(player, options);
|
|
849
|
+
}
|