anim-engine 0.1.2 → 0.2.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/README.md +152 -103
- package/dist/animation/create-animation.d.ts +3 -3
- package/dist/animation/create-animation.js +90 -201
- package/dist/animation/runner.d.ts +37 -0
- package/dist/animation/runner.js +220 -0
- package/dist/shared/types.d.ts +0 -1
- package/dist/timeline/create-timeline.d.ts +5 -4
- package/dist/timeline/create-timeline.js +101 -76
- package/package.json +2 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { easingFunctions } from "../easing/easing.js";
|
|
2
2
|
import { getTicker } from "../ticker/get-ticker.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createKeyframeRunner, createTweenRunner } from "./runner.js";
|
|
4
4
|
//#region src/animation/create-animation.ts
|
|
5
5
|
var resolveEasing = (ease) => typeof ease === "function" ? ease : easingFunctions[ease];
|
|
6
|
+
var resolveValue = (v) => typeof v === "function" ? v() : v;
|
|
6
7
|
var isKeyframeMode = (options) => {
|
|
7
8
|
return "keyframes" in options && Array.isArray(options.keyframes);
|
|
8
9
|
};
|
|
@@ -12,101 +13,79 @@ var createAnimation = (options) => {
|
|
|
12
13
|
};
|
|
13
14
|
var createSingleTween = (options) => {
|
|
14
15
|
const easeName = options.ease ?? "inOutSine";
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
progress: 0,
|
|
21
|
-
currentValue: 0,
|
|
22
|
-
velocity: 0
|
|
23
|
-
};
|
|
16
|
+
const { onStarted, onUpdate, onEnded } = options;
|
|
17
|
+
const rawFrom = options.from;
|
|
18
|
+
const rawTo = options.to;
|
|
19
|
+
const rawDurationMs = options.durationMs;
|
|
20
|
+
let cachedDurationMs = resolveValue(rawDurationMs);
|
|
24
21
|
let status = "stopped";
|
|
25
|
-
let stopped = false;
|
|
26
22
|
let resolvePromise;
|
|
27
|
-
let delayRemainingMs = 0;
|
|
28
|
-
let pendingStart = false;
|
|
29
23
|
const ticker = getTicker();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
onStarted?.();
|
|
37
|
-
deltaMs = -delayRemainingMs;
|
|
38
|
-
}
|
|
39
|
-
const from = typeof rawFrom === "function" ? rawFrom() : rawFrom;
|
|
40
|
-
const to = typeof rawTo === "function" ? rawTo() : rawTo;
|
|
41
|
-
const completed = updateTween(state, deltaMs, durationMs, currentEase, from, to);
|
|
42
|
-
onUpdate?.(state.currentValue, state.velocity);
|
|
43
|
-
if (completed) handleCompletion();
|
|
24
|
+
let runner;
|
|
25
|
+
const finish = () => {
|
|
26
|
+
status = "stopped";
|
|
27
|
+
ticker.remove(runner);
|
|
28
|
+
resolvePromise?.(controls);
|
|
29
|
+
resolvePromise = void 0;
|
|
44
30
|
};
|
|
45
|
-
|
|
31
|
+
const buildRunner = () => {
|
|
32
|
+
const from = resolveValue(rawFrom);
|
|
33
|
+
const to = resolveValue(rawTo);
|
|
34
|
+
cachedDurationMs = resolveValue(rawDurationMs);
|
|
35
|
+
return createTweenRunner({
|
|
36
|
+
from,
|
|
37
|
+
to,
|
|
38
|
+
durationMs: cachedDurationMs,
|
|
39
|
+
easeFn: resolveEasing(easeName),
|
|
40
|
+
onStarted,
|
|
41
|
+
onUpdate,
|
|
42
|
+
onEnded,
|
|
43
|
+
onComplete: finish
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const hasDynamic = typeof rawFrom === "function" || typeof rawTo === "function" || typeof rawDurationMs === "function";
|
|
47
|
+
runner = buildRunner();
|
|
46
48
|
const play = () => {
|
|
47
49
|
if (status === "dead") throw new Error("Cannot play a dead animation");
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const initFrom = typeof rawFrom === "function" ? rawFrom() : rawFrom;
|
|
51
|
-
state.currentValue = initFrom;
|
|
52
|
-
state.velocity = 0;
|
|
53
|
-
if (delayMs > 0) {
|
|
54
|
-
delayRemainingMs = delayMs;
|
|
55
|
-
pendingStart = true;
|
|
56
|
-
} else {
|
|
57
|
-
pendingStart = false;
|
|
58
|
-
delayRemainingMs = 0;
|
|
59
|
-
}
|
|
50
|
+
if (hasDynamic) runner = buildRunner();
|
|
51
|
+
else runner.reset();
|
|
60
52
|
const promise = new Promise((resolve) => {
|
|
61
53
|
resolvePromise = resolve;
|
|
62
54
|
});
|
|
63
55
|
status = "playing";
|
|
64
|
-
ticker.add(
|
|
65
|
-
|
|
56
|
+
ticker.add(runner);
|
|
57
|
+
onStarted?.();
|
|
66
58
|
return promise;
|
|
67
59
|
};
|
|
68
60
|
const pause = () => {
|
|
69
61
|
if (status !== "playing") return;
|
|
70
62
|
status = "paused";
|
|
71
|
-
ticker.remove(
|
|
63
|
+
ticker.remove(runner);
|
|
72
64
|
};
|
|
73
65
|
const resume = () => {
|
|
74
66
|
if (status !== "paused") return;
|
|
75
67
|
status = "playing";
|
|
76
|
-
ticker.add(
|
|
68
|
+
ticker.add(runner);
|
|
77
69
|
};
|
|
78
70
|
const stop = () => {
|
|
79
|
-
stopped = true;
|
|
80
71
|
status = "stopped";
|
|
81
|
-
ticker.remove(
|
|
72
|
+
ticker.remove(runner);
|
|
82
73
|
resolvePromise?.(controls);
|
|
83
74
|
resolvePromise = void 0;
|
|
84
75
|
};
|
|
85
76
|
const skipToEnd = () => {
|
|
86
|
-
|
|
87
|
-
state.currentValue = skipTo;
|
|
88
|
-
state.velocity = 0;
|
|
89
|
-
state.progress = 1;
|
|
90
|
-
onUpdate?.(state.currentValue, state.velocity);
|
|
77
|
+
runner.evaluate(1);
|
|
91
78
|
if (status === "playing" || status === "paused") onEnded?.();
|
|
92
79
|
status = "stopped";
|
|
93
|
-
ticker.remove(
|
|
80
|
+
ticker.remove(runner);
|
|
94
81
|
resolvePromise?.(controls);
|
|
95
82
|
resolvePromise = void 0;
|
|
96
83
|
};
|
|
97
84
|
const kill = () => {
|
|
98
85
|
status = "dead";
|
|
99
|
-
ticker.remove(
|
|
100
|
-
stopped = true;
|
|
86
|
+
ticker.remove(runner);
|
|
101
87
|
resolvePromise = void 0;
|
|
102
88
|
};
|
|
103
|
-
function handleCompletion() {
|
|
104
|
-
onEnded?.();
|
|
105
|
-
status = "stopped";
|
|
106
|
-
ticker.remove(update);
|
|
107
|
-
resolvePromise?.(controls);
|
|
108
|
-
resolvePromise = void 0;
|
|
109
|
-
}
|
|
110
89
|
const controls = {
|
|
111
90
|
play,
|
|
112
91
|
pause,
|
|
@@ -114,170 +93,103 @@ var createSingleTween = (options) => {
|
|
|
114
93
|
stop,
|
|
115
94
|
skipToEnd,
|
|
116
95
|
kill,
|
|
117
|
-
setCurrentValue: (value) => {
|
|
118
|
-
state.currentValue = value;
|
|
119
|
-
state.velocity = 0;
|
|
120
|
-
},
|
|
121
96
|
get currentValue() {
|
|
122
|
-
return
|
|
97
|
+
return runner.currentValue;
|
|
123
98
|
},
|
|
124
99
|
get velocity() {
|
|
125
|
-
return
|
|
100
|
+
return runner.velocity;
|
|
126
101
|
},
|
|
127
102
|
get progress() {
|
|
128
|
-
return
|
|
103
|
+
return runner.progress;
|
|
129
104
|
},
|
|
130
105
|
setProgress(value) {
|
|
131
106
|
if (status === "playing") pause();
|
|
132
|
-
|
|
133
|
-
state.progress = clamped;
|
|
134
|
-
const from = typeof rawFrom === "function" ? rawFrom() : rawFrom;
|
|
135
|
-
const to = typeof rawTo === "function" ? rawTo() : rawTo;
|
|
136
|
-
state.currentValue = from + (to - from) * currentEase(clamped);
|
|
137
|
-
state.velocity = 0;
|
|
138
|
-
onUpdate?.(state.currentValue, state.velocity);
|
|
107
|
+
runner.evaluate(Math.max(0, Math.min(1, value)));
|
|
139
108
|
},
|
|
140
109
|
get status() {
|
|
141
110
|
return status;
|
|
142
111
|
},
|
|
143
112
|
get durationMs() {
|
|
144
|
-
return
|
|
113
|
+
return cachedDurationMs;
|
|
145
114
|
}
|
|
146
115
|
};
|
|
147
116
|
return controls;
|
|
148
117
|
};
|
|
149
118
|
var createKeyframeAnimation = (options) => {
|
|
150
|
-
const keyframes = options
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const totalDurationMs = sorted[sorted.length - 1].at;
|
|
156
|
-
const invTotalDuration = 1 / totalDurationMs;
|
|
157
|
-
const resolveKeyframeValue = (kf) => {
|
|
158
|
-
return typeof kf.value === "function" ? kf.value() : kf.value;
|
|
159
|
-
};
|
|
160
|
-
const segments = [];
|
|
161
|
-
for (let i = 0; i < sorted.length - 1; i++) {
|
|
162
|
-
const current = sorted[i];
|
|
163
|
-
const next = sorted[i + 1];
|
|
164
|
-
const from = resolveKeyframeValue(current);
|
|
165
|
-
const to = resolveKeyframeValue(next);
|
|
166
|
-
segments.push({
|
|
167
|
-
from,
|
|
168
|
-
to,
|
|
169
|
-
range: to - from,
|
|
170
|
-
durationMs: next.at - current.at,
|
|
171
|
-
easeFn: resolveEasing(next.ease ?? "inOutSine")
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
const prefixSum = [0];
|
|
175
|
-
for (let i = 0; i < segments.length; i++) prefixSum.push(prefixSum[i] + segments[i].durationMs);
|
|
176
|
-
if (segments.length === 0) return createSingleTween({
|
|
177
|
-
from: resolveKeyframeValue(sorted[0]),
|
|
178
|
-
to: resolveKeyframeValue(sorted[0]),
|
|
179
|
-
durationMs: totalDurationMs,
|
|
180
|
-
ease: "linear",
|
|
181
|
-
onUpdate,
|
|
182
|
-
onEnded
|
|
183
|
-
});
|
|
184
|
-
const state = {
|
|
185
|
-
progress: 0,
|
|
186
|
-
currentValue: 0,
|
|
187
|
-
velocity: 0
|
|
119
|
+
const { keyframes: rawKeyframes, onStarted, onUpdate, onProgress, onEnded } = options;
|
|
120
|
+
const resolveKeyframeGaps = () => {
|
|
121
|
+
let total = 0;
|
|
122
|
+
for (let i = 1; i < rawKeyframes.length; i++) total += resolveValue(rawKeyframes[i].gap ?? 0);
|
|
123
|
+
return total;
|
|
188
124
|
};
|
|
125
|
+
let cachedDurationMs = resolveKeyframeGaps();
|
|
189
126
|
let status = "stopped";
|
|
190
|
-
let stopped = false;
|
|
191
127
|
let resolvePromise;
|
|
192
|
-
let currentSegmentIndex = 0;
|
|
193
|
-
let previousValue = resolveKeyframeValue(sorted[0]);
|
|
194
128
|
const ticker = getTicker();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const eased = segment.easeFn(segmentProgress);
|
|
202
|
-
previousValue = state.currentValue;
|
|
203
|
-
state.currentValue = segment.from + segment.range * eased;
|
|
204
|
-
if (segmentProgress >= 1) {
|
|
205
|
-
state.currentValue = segment.to;
|
|
206
|
-
state.velocity = 0;
|
|
207
|
-
} else state.velocity = (state.currentValue - previousValue) / (deltaMs / 1e3);
|
|
208
|
-
const elapsedTotal = prefixSum[currentSegmentIndex] + segmentElapsed;
|
|
209
|
-
state.progress = Math.min(elapsedTotal * invTotalDuration, 1);
|
|
210
|
-
onProgress?.(state.progress);
|
|
211
|
-
onUpdate?.(state.currentValue, state.velocity);
|
|
212
|
-
if (segmentProgress >= 1) if (currentSegmentIndex < segments.length - 1) {
|
|
213
|
-
currentSegmentIndex++;
|
|
214
|
-
segmentElapsed = 0;
|
|
215
|
-
segmentProgress = 0;
|
|
216
|
-
state.currentValue = segments[currentSegmentIndex].from;
|
|
217
|
-
previousValue = state.currentValue;
|
|
218
|
-
state.velocity = 0;
|
|
219
|
-
state.progress = Math.min(prefixSum[currentSegmentIndex] * invTotalDuration, 1);
|
|
220
|
-
onProgress?.(state.progress);
|
|
221
|
-
} else {
|
|
222
|
-
status = "stopped";
|
|
223
|
-
ticker.remove(update);
|
|
224
|
-
onEnded?.();
|
|
225
|
-
resolvePromise?.(controls);
|
|
226
|
-
resolvePromise = void 0;
|
|
227
|
-
}
|
|
129
|
+
let runner;
|
|
130
|
+
const finish = () => {
|
|
131
|
+
status = "stopped";
|
|
132
|
+
ticker.remove(runner);
|
|
133
|
+
resolvePromise?.(controls);
|
|
134
|
+
resolvePromise = void 0;
|
|
228
135
|
};
|
|
229
|
-
|
|
230
|
-
|
|
136
|
+
const buildRunner = () => {
|
|
137
|
+
return createKeyframeRunner({
|
|
138
|
+
keyframes: rawKeyframes.map((kf, i) => ({
|
|
139
|
+
value: resolveValue(kf.value),
|
|
140
|
+
gap: i === 0 ? 0 : resolveValue(kf.gap ?? 0),
|
|
141
|
+
easeFn: resolveEasing(kf.ease ?? "inOutSine")
|
|
142
|
+
})),
|
|
143
|
+
onStarted,
|
|
144
|
+
onUpdate,
|
|
145
|
+
onProgress,
|
|
146
|
+
onEnded,
|
|
147
|
+
onComplete: finish
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const hasDynamic = rawKeyframes.some((kf) => typeof kf.value === "function" || typeof kf.gap === "function");
|
|
151
|
+
runner = buildRunner();
|
|
231
152
|
const play = () => {
|
|
232
153
|
if (status === "dead") throw new Error("Cannot play a dead animation");
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
state.progress = 0;
|
|
238
|
-
state.currentValue = segments[0].from;
|
|
239
|
-
previousValue = segments[0].from;
|
|
240
|
-
state.velocity = 0;
|
|
154
|
+
if (hasDynamic) {
|
|
155
|
+
runner = buildRunner();
|
|
156
|
+
cachedDurationMs = resolveKeyframeGaps();
|
|
157
|
+
} else runner.reset();
|
|
241
158
|
const promise = new Promise((resolve) => {
|
|
242
159
|
resolvePromise = resolve;
|
|
243
160
|
});
|
|
244
161
|
status = "playing";
|
|
245
|
-
ticker.add(
|
|
162
|
+
ticker.add(runner);
|
|
163
|
+
onStarted?.();
|
|
246
164
|
return promise;
|
|
247
165
|
};
|
|
248
166
|
const pause = () => {
|
|
249
167
|
if (status !== "playing") return;
|
|
250
168
|
status = "paused";
|
|
251
|
-
ticker.remove(
|
|
169
|
+
ticker.remove(runner);
|
|
252
170
|
};
|
|
253
171
|
const resume = () => {
|
|
254
172
|
if (status !== "paused") return;
|
|
255
173
|
status = "playing";
|
|
256
|
-
ticker.add(
|
|
174
|
+
ticker.add(runner);
|
|
257
175
|
};
|
|
258
176
|
const stop = () => {
|
|
259
|
-
stopped = true;
|
|
260
177
|
status = "stopped";
|
|
261
|
-
ticker.remove(
|
|
178
|
+
ticker.remove(runner);
|
|
262
179
|
resolvePromise?.(controls);
|
|
263
180
|
resolvePromise = void 0;
|
|
264
181
|
};
|
|
265
182
|
const skipToEnd = () => {
|
|
266
|
-
|
|
267
|
-
state.currentValue = last.to;
|
|
268
|
-
state.velocity = 0;
|
|
269
|
-
state.progress = 1;
|
|
270
|
-
onUpdate?.(state.currentValue, state.velocity);
|
|
183
|
+
runner.evaluate(1);
|
|
271
184
|
if (status === "playing" || status === "paused") onEnded?.();
|
|
272
185
|
status = "stopped";
|
|
273
|
-
ticker.remove(
|
|
186
|
+
ticker.remove(runner);
|
|
274
187
|
resolvePromise?.(controls);
|
|
275
188
|
resolvePromise = void 0;
|
|
276
189
|
};
|
|
277
190
|
const kill = () => {
|
|
278
191
|
status = "dead";
|
|
279
|
-
ticker.remove(
|
|
280
|
-
stopped = true;
|
|
192
|
+
ticker.remove(runner);
|
|
281
193
|
resolvePromise = void 0;
|
|
282
194
|
};
|
|
283
195
|
const controls = {
|
|
@@ -287,47 +199,24 @@ var createKeyframeAnimation = (options) => {
|
|
|
287
199
|
stop,
|
|
288
200
|
skipToEnd,
|
|
289
201
|
kill,
|
|
290
|
-
setCurrentValue: (value) => {
|
|
291
|
-
state.currentValue = value;
|
|
292
|
-
state.velocity = 0;
|
|
293
|
-
},
|
|
294
202
|
get currentValue() {
|
|
295
|
-
return
|
|
203
|
+
return runner.currentValue;
|
|
296
204
|
},
|
|
297
205
|
get velocity() {
|
|
298
|
-
return
|
|
206
|
+
return runner.velocity;
|
|
299
207
|
},
|
|
300
208
|
get progress() {
|
|
301
|
-
return
|
|
209
|
+
return runner.progress;
|
|
302
210
|
},
|
|
303
211
|
setProgress(value) {
|
|
304
212
|
if (status === "playing") pause();
|
|
305
|
-
|
|
306
|
-
state.progress = clamped;
|
|
307
|
-
const elapsed = clamped * totalDurationMs;
|
|
308
|
-
let segIdx = 0;
|
|
309
|
-
for (let i = 0; i < segments.length; i++) {
|
|
310
|
-
if (elapsed <= prefixSum[i + 1]) {
|
|
311
|
-
segIdx = i;
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
|
-
segIdx = i;
|
|
315
|
-
}
|
|
316
|
-
const segment = segments[segIdx];
|
|
317
|
-
const segStart = prefixSum[segIdx];
|
|
318
|
-
const segDuration = segment.durationMs;
|
|
319
|
-
const segProgress = segDuration > 0 ? (elapsed - segStart) / segDuration : 1;
|
|
320
|
-
const eased = segment.easeFn(Math.max(0, Math.min(1, segProgress)));
|
|
321
|
-
state.currentValue = segment.from + segment.range * eased;
|
|
322
|
-
state.velocity = 0;
|
|
323
|
-
onUpdate?.(state.currentValue, state.velocity);
|
|
324
|
-
onProgress?.(clamped);
|
|
213
|
+
runner.evaluate(Math.max(0, Math.min(1, value)));
|
|
325
214
|
},
|
|
326
215
|
get status() {
|
|
327
216
|
return status;
|
|
328
217
|
},
|
|
329
218
|
get durationMs() {
|
|
330
|
-
return
|
|
219
|
+
return cachedDurationMs;
|
|
331
220
|
}
|
|
332
221
|
};
|
|
333
222
|
return controls;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EaseFunction } from '../shared/types';
|
|
2
|
+
export type Runner = {
|
|
3
|
+
(deltaMs: number): boolean;
|
|
4
|
+
evaluate: (progress: number) => number;
|
|
5
|
+
reset: () => void;
|
|
6
|
+
currentValue: number;
|
|
7
|
+
velocity: number;
|
|
8
|
+
progress: number;
|
|
9
|
+
onStarted: (() => void) | undefined;
|
|
10
|
+
onUpdate: ((value: number, velocity: number) => void) | undefined;
|
|
11
|
+
onProgress: ((progress: number) => void) | undefined;
|
|
12
|
+
onEnded: (() => void) | undefined;
|
|
13
|
+
};
|
|
14
|
+
export type TweenRunnerConfig = {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
easeFn: EaseFunction;
|
|
19
|
+
onStarted?: () => void;
|
|
20
|
+
onUpdate?: (value: number, velocity: number) => void;
|
|
21
|
+
onEnded?: () => void;
|
|
22
|
+
onComplete?: () => void;
|
|
23
|
+
};
|
|
24
|
+
export declare const createTweenRunner: (config: TweenRunnerConfig) => Runner;
|
|
25
|
+
export type KeyframeRunnerConfig = {
|
|
26
|
+
keyframes: {
|
|
27
|
+
value: number;
|
|
28
|
+
gap: number;
|
|
29
|
+
easeFn: EaseFunction;
|
|
30
|
+
}[];
|
|
31
|
+
onStarted?: () => void;
|
|
32
|
+
onUpdate?: (value: number, velocity: number) => void;
|
|
33
|
+
onProgress?: (progress: number) => void;
|
|
34
|
+
onEnded?: () => void;
|
|
35
|
+
onComplete?: () => void;
|
|
36
|
+
};
|
|
37
|
+
export declare const createKeyframeRunner: (config: KeyframeRunnerConfig) => Runner;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { updateTween } from "./update.js";
|
|
2
|
+
//#region src/animation/runner.ts
|
|
3
|
+
var noop = () => {};
|
|
4
|
+
var createTweenRunner = (config) => {
|
|
5
|
+
const { from, to, durationMs, easeFn } = config;
|
|
6
|
+
const onStarted = config.onStarted;
|
|
7
|
+
const onUpdate = config.onUpdate ?? noop;
|
|
8
|
+
const onEnded = config.onEnded;
|
|
9
|
+
const onComplete = config.onComplete;
|
|
10
|
+
const state = {
|
|
11
|
+
progress: 0,
|
|
12
|
+
currentValue: from,
|
|
13
|
+
velocity: 0
|
|
14
|
+
};
|
|
15
|
+
let runner;
|
|
16
|
+
const step = (deltaMs) => {
|
|
17
|
+
const completed = updateTween(state, deltaMs, durationMs, easeFn, from, to);
|
|
18
|
+
onUpdate(state.currentValue, state.velocity);
|
|
19
|
+
if (completed) {
|
|
20
|
+
onEnded?.();
|
|
21
|
+
onComplete?.();
|
|
22
|
+
}
|
|
23
|
+
return completed;
|
|
24
|
+
};
|
|
25
|
+
const evaluate = (progress) => {
|
|
26
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
27
|
+
state.progress = clamped;
|
|
28
|
+
if (clamped >= 1) {
|
|
29
|
+
state.currentValue = to;
|
|
30
|
+
state.velocity = 0;
|
|
31
|
+
} else {
|
|
32
|
+
const range = to - from;
|
|
33
|
+
state.currentValue = from + range * easeFn(clamped);
|
|
34
|
+
state.velocity = 0;
|
|
35
|
+
}
|
|
36
|
+
onUpdate(state.currentValue, state.velocity);
|
|
37
|
+
return state.currentValue;
|
|
38
|
+
};
|
|
39
|
+
const reset = () => {
|
|
40
|
+
state.progress = 0;
|
|
41
|
+
state.currentValue = from;
|
|
42
|
+
state.velocity = 0;
|
|
43
|
+
};
|
|
44
|
+
runner = step;
|
|
45
|
+
runner.evaluate = evaluate;
|
|
46
|
+
runner.reset = reset;
|
|
47
|
+
Object.defineProperty(runner, "currentValue", {
|
|
48
|
+
get: () => state.currentValue,
|
|
49
|
+
configurable: true
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(runner, "velocity", {
|
|
52
|
+
get: () => state.velocity,
|
|
53
|
+
configurable: true
|
|
54
|
+
});
|
|
55
|
+
Object.defineProperty(runner, "progress", {
|
|
56
|
+
get: () => state.progress,
|
|
57
|
+
configurable: true
|
|
58
|
+
});
|
|
59
|
+
runner.onStarted = onStarted;
|
|
60
|
+
runner.onUpdate = config.onUpdate;
|
|
61
|
+
runner.onProgress = void 0;
|
|
62
|
+
runner.onEnded = onEnded;
|
|
63
|
+
return runner;
|
|
64
|
+
};
|
|
65
|
+
var createKeyframeRunner = (config) => {
|
|
66
|
+
const { keyframes } = config;
|
|
67
|
+
const onStarted = config.onStarted;
|
|
68
|
+
const onUpdate = config.onUpdate ?? noop;
|
|
69
|
+
const onProgress = config.onProgress ?? noop;
|
|
70
|
+
const onEnded = config.onEnded;
|
|
71
|
+
const onComplete = config.onComplete;
|
|
72
|
+
const segments = [];
|
|
73
|
+
for (let i = 0; i < keyframes.length - 1; i++) {
|
|
74
|
+
const cur = keyframes[i];
|
|
75
|
+
const nxt = keyframes[i + 1];
|
|
76
|
+
segments.push({
|
|
77
|
+
from: cur.value,
|
|
78
|
+
to: nxt.value,
|
|
79
|
+
range: nxt.value - cur.value,
|
|
80
|
+
durationMs: nxt.gap,
|
|
81
|
+
easeFn: nxt.easeFn
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const prefixSum = [0];
|
|
85
|
+
for (let i = 0; i < segments.length; i++) prefixSum.push(prefixSum[i] + segments[i].durationMs);
|
|
86
|
+
const totalDurationMs = prefixSum[prefixSum.length - 1];
|
|
87
|
+
const invTotalDuration = totalDurationMs > 0 ? 1 / totalDurationMs : 1;
|
|
88
|
+
let currentValue = keyframes[0].value;
|
|
89
|
+
let velocity = 0;
|
|
90
|
+
let globalProgress = 0;
|
|
91
|
+
let currentSegmentIndex = 0;
|
|
92
|
+
let segmentElapsed = 0;
|
|
93
|
+
let segmentProgress = 0;
|
|
94
|
+
let runner;
|
|
95
|
+
if (segments.length === 0) {
|
|
96
|
+
const complete = () => {
|
|
97
|
+
onEnded?.();
|
|
98
|
+
onComplete?.();
|
|
99
|
+
};
|
|
100
|
+
const step = (_deltaMs) => {
|
|
101
|
+
onUpdate(currentValue, 0);
|
|
102
|
+
onProgress(1);
|
|
103
|
+
complete();
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
const evaluate = (prog) => {
|
|
107
|
+
onUpdate(currentValue, 0);
|
|
108
|
+
onProgress(Math.max(0, Math.min(1, prog)));
|
|
109
|
+
return currentValue;
|
|
110
|
+
};
|
|
111
|
+
runner = step;
|
|
112
|
+
runner.evaluate = evaluate;
|
|
113
|
+
runner.reset = () => {};
|
|
114
|
+
Object.defineProperty(runner, "currentValue", {
|
|
115
|
+
get: () => currentValue,
|
|
116
|
+
configurable: true
|
|
117
|
+
});
|
|
118
|
+
Object.defineProperty(runner, "velocity", {
|
|
119
|
+
get: () => 0,
|
|
120
|
+
configurable: true
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(runner, "progress", {
|
|
123
|
+
get: () => 1,
|
|
124
|
+
configurable: true
|
|
125
|
+
});
|
|
126
|
+
runner.onStarted = onStarted;
|
|
127
|
+
runner.onUpdate = config.onUpdate;
|
|
128
|
+
runner.onProgress = config.onProgress;
|
|
129
|
+
runner.onEnded = onEnded;
|
|
130
|
+
return runner;
|
|
131
|
+
}
|
|
132
|
+
const step = (deltaMs) => {
|
|
133
|
+
const segment = segments[currentSegmentIndex];
|
|
134
|
+
segmentElapsed += deltaMs;
|
|
135
|
+
segmentProgress += deltaMs / segment.durationMs;
|
|
136
|
+
if (segmentProgress >= 1) segmentProgress = 1;
|
|
137
|
+
const previousValue = currentValue;
|
|
138
|
+
const eased = segment.easeFn(segmentProgress);
|
|
139
|
+
currentValue = segment.from + segment.range * eased;
|
|
140
|
+
if (segmentProgress >= 1) {
|
|
141
|
+
currentValue = segment.to;
|
|
142
|
+
velocity = 0;
|
|
143
|
+
} else velocity = (currentValue - previousValue) / (deltaMs / 1e3);
|
|
144
|
+
const elapsedTotal = prefixSum[currentSegmentIndex] + segmentElapsed;
|
|
145
|
+
globalProgress = Math.min(elapsedTotal * invTotalDuration, 1);
|
|
146
|
+
onProgress(globalProgress);
|
|
147
|
+
onUpdate(currentValue, velocity);
|
|
148
|
+
if (segmentProgress >= 1) if (currentSegmentIndex < segments.length - 1) {
|
|
149
|
+
currentSegmentIndex++;
|
|
150
|
+
segmentElapsed = 0;
|
|
151
|
+
segmentProgress = 0;
|
|
152
|
+
currentValue = segments[currentSegmentIndex].from;
|
|
153
|
+
velocity = 0;
|
|
154
|
+
globalProgress = Math.min(prefixSum[currentSegmentIndex] * invTotalDuration, 1);
|
|
155
|
+
onProgress(globalProgress);
|
|
156
|
+
} else {
|
|
157
|
+
onEnded?.();
|
|
158
|
+
onComplete?.();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
const evaluate = (progress) => {
|
|
164
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
165
|
+
const elapsed = clamped * totalDurationMs;
|
|
166
|
+
let segIdx = 0;
|
|
167
|
+
for (let i = 0; i < segments.length; i++) {
|
|
168
|
+
if (elapsed <= prefixSum[i + 1]) {
|
|
169
|
+
segIdx = i;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
segIdx = i;
|
|
173
|
+
}
|
|
174
|
+
const segment = segments[segIdx];
|
|
175
|
+
const segStart = prefixSum[segIdx];
|
|
176
|
+
const segDuration = segment.durationMs;
|
|
177
|
+
const segProgress = segDuration > 0 ? (elapsed - segStart) / segDuration : 1;
|
|
178
|
+
const clampedSegProgress = Math.max(0, Math.min(1, segProgress));
|
|
179
|
+
const eased = segment.easeFn(clampedSegProgress);
|
|
180
|
+
currentValue = segment.from + segment.range * eased;
|
|
181
|
+
velocity = 0;
|
|
182
|
+
globalProgress = clamped;
|
|
183
|
+
currentSegmentIndex = segIdx;
|
|
184
|
+
segmentElapsed = elapsed - segStart;
|
|
185
|
+
segmentProgress = clampedSegProgress;
|
|
186
|
+
onUpdate(currentValue, velocity);
|
|
187
|
+
onProgress(clamped);
|
|
188
|
+
return currentValue;
|
|
189
|
+
};
|
|
190
|
+
const reset = () => {
|
|
191
|
+
currentValue = keyframes[0].value;
|
|
192
|
+
velocity = 0;
|
|
193
|
+
globalProgress = 0;
|
|
194
|
+
currentSegmentIndex = 0;
|
|
195
|
+
segmentElapsed = 0;
|
|
196
|
+
segmentProgress = 0;
|
|
197
|
+
};
|
|
198
|
+
runner = step;
|
|
199
|
+
runner.evaluate = evaluate;
|
|
200
|
+
runner.reset = reset;
|
|
201
|
+
Object.defineProperty(runner, "currentValue", {
|
|
202
|
+
get: () => currentValue,
|
|
203
|
+
configurable: true
|
|
204
|
+
});
|
|
205
|
+
Object.defineProperty(runner, "velocity", {
|
|
206
|
+
get: () => velocity,
|
|
207
|
+
configurable: true
|
|
208
|
+
});
|
|
209
|
+
Object.defineProperty(runner, "progress", {
|
|
210
|
+
get: () => globalProgress,
|
|
211
|
+
configurable: true
|
|
212
|
+
});
|
|
213
|
+
runner.onStarted = onStarted;
|
|
214
|
+
runner.onUpdate = config.onUpdate;
|
|
215
|
+
runner.onProgress = config.onProgress;
|
|
216
|
+
runner.onEnded = onEnded;
|
|
217
|
+
return runner;
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
export { createKeyframeRunner, createTweenRunner };
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { KeyframedAnimationOptions } from '../animation/create-animation';
|
|
2
|
+
import { AnimationStatus, DynamicValue } from '../shared/types';
|
|
2
3
|
export type TimelineLayer = {
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
keyframe: KeyframedAnimationOptions;
|
|
5
|
+
at: DynamicValue;
|
|
5
6
|
} | {
|
|
7
|
+
keyframe: KeyframedAnimationOptions;
|
|
6
8
|
gap: number;
|
|
7
|
-
animation: Animation | Animation[];
|
|
8
9
|
};
|
|
9
10
|
export type Timeline = {
|
|
10
11
|
play: () => Promise<Timeline>;
|