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,119 +1,144 @@
|
|
|
1
|
+
import { easingFunctions } from "../easing/easing.js";
|
|
1
2
|
import { getTicker } from "../ticker/get-ticker.js";
|
|
3
|
+
import { createKeyframeRunner } from "../animation/runner.js";
|
|
2
4
|
//#region src/timeline/create-timeline.ts
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
var noop = () => {};
|
|
6
|
+
var resolveValue = (v) => typeof v === "function" ? v() : v;
|
|
7
|
+
var resolveEasing = (ease) => typeof ease === "function" ? ease : easingFunctions[ease];
|
|
8
|
+
var buildFromConfigs = (rawLayers) => {
|
|
9
|
+
const activeLayers = [];
|
|
10
|
+
let previousEndAt = 0;
|
|
11
|
+
for (const layer of rawLayers) {
|
|
12
|
+
const startAt = "at" in layer ? resolveValue(layer.at) : previousEndAt + layer.gap;
|
|
13
|
+
const resolvedKeyframes = layer.keyframe.keyframes.map((kf, j) => ({
|
|
14
|
+
value: resolveValue(kf.value),
|
|
15
|
+
gap: j === 0 ? 0 : resolveValue(kf.gap ?? 0),
|
|
16
|
+
easeFn: resolveEasing(kf.ease ?? "inOutSine")
|
|
17
|
+
}));
|
|
18
|
+
let layerDuration = 0;
|
|
19
|
+
for (let i = 1; i < resolvedKeyframes.length; i++) layerDuration += resolvedKeyframes[i].gap;
|
|
20
|
+
const runner = createKeyframeRunner({
|
|
21
|
+
keyframes: resolvedKeyframes,
|
|
22
|
+
onStarted: layer.keyframe.onStarted,
|
|
23
|
+
onUpdate: layer.keyframe.onUpdate,
|
|
24
|
+
onProgress: layer.keyframe.onProgress,
|
|
25
|
+
onEnded: layer.keyframe.onEnded
|
|
26
|
+
});
|
|
27
|
+
activeLayers.push({
|
|
13
28
|
startAt,
|
|
14
|
-
endAt,
|
|
15
|
-
|
|
29
|
+
endAt: startAt + layerDuration,
|
|
30
|
+
runner,
|
|
31
|
+
started: false,
|
|
32
|
+
ended: false
|
|
16
33
|
});
|
|
17
|
-
|
|
34
|
+
previousEndAt = startAt + layerDuration;
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
activeLayers.sort((a, b) => a.startAt - b.startAt);
|
|
37
|
+
return {
|
|
38
|
+
activeLayers,
|
|
39
|
+
totalDurationMs: Math.max(0, ...activeLayers.map((l) => l.endAt))
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
var createTimeline = (layers, options) => {
|
|
43
|
+
const { onStarted, onEnded } = options ?? {};
|
|
44
|
+
const onProgress = options?.onProgress ?? noop;
|
|
45
|
+
const rawLayers = layers;
|
|
46
|
+
let state = buildFromConfigs(rawLayers);
|
|
47
|
+
let activeLayers = state.activeLayers;
|
|
48
|
+
let totalDurationMs = state.totalDurationMs;
|
|
49
|
+
let timelineStatus = "stopped";
|
|
22
50
|
let elapsedMs = 0;
|
|
23
51
|
let resolvePromise;
|
|
24
|
-
let
|
|
52
|
+
let remainingLayers = activeLayers.length;
|
|
25
53
|
const ticker = getTicker();
|
|
54
|
+
const finish = () => {
|
|
55
|
+
timelineStatus = "stopped";
|
|
56
|
+
ticker.remove(update);
|
|
57
|
+
onEnded?.();
|
|
58
|
+
resolvePromise?.(timeline);
|
|
59
|
+
resolvePromise = void 0;
|
|
60
|
+
};
|
|
61
|
+
const update = (deltaMs) => {
|
|
62
|
+
elapsedMs += deltaMs;
|
|
63
|
+
for (const layer of activeLayers) {
|
|
64
|
+
if (!layer.started && elapsedMs >= layer.startAt) {
|
|
65
|
+
layer.runner.reset();
|
|
66
|
+
layer.runner.onStarted?.();
|
|
67
|
+
layer.started = true;
|
|
68
|
+
}
|
|
69
|
+
if (layer.started && !layer.ended) {
|
|
70
|
+
if (layer.runner(deltaMs)) {
|
|
71
|
+
layer.ended = true;
|
|
72
|
+
remainingLayers--;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
onProgress(totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 1);
|
|
77
|
+
if (remainingLayers <= 0) finish();
|
|
78
|
+
};
|
|
26
79
|
const play = () => {
|
|
27
|
-
if (
|
|
80
|
+
if (timelineStatus === "dead") throw new Error("Cannot play a dead timeline");
|
|
81
|
+
state = buildFromConfigs(rawLayers);
|
|
82
|
+
activeLayers = state.activeLayers;
|
|
83
|
+
totalDurationMs = state.totalDurationMs;
|
|
84
|
+
remainingLayers = activeLayers.length;
|
|
28
85
|
elapsedMs = 0;
|
|
29
|
-
pendingAnimations = 0;
|
|
30
|
-
batches.forEach((b) => {
|
|
31
|
-
b.started = false;
|
|
32
|
-
});
|
|
33
86
|
const promise = new Promise((resolve) => {
|
|
34
87
|
resolvePromise = resolve;
|
|
35
88
|
});
|
|
36
|
-
|
|
89
|
+
timelineStatus = "playing";
|
|
37
90
|
onStarted?.();
|
|
38
91
|
ticker.add(update);
|
|
39
92
|
return promise;
|
|
40
93
|
};
|
|
41
94
|
const pause = () => {
|
|
42
|
-
if (
|
|
43
|
-
|
|
95
|
+
if (timelineStatus !== "playing") return;
|
|
96
|
+
timelineStatus = "paused";
|
|
44
97
|
ticker.remove(update);
|
|
45
|
-
for (const batch of batches) if (batch.started) {
|
|
46
|
-
for (const anim of batch.animations) if (anim.status === "playing") anim.pause();
|
|
47
|
-
}
|
|
48
98
|
};
|
|
49
99
|
const resume = () => {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
for (const batch of batches) if (batch.started) {
|
|
53
|
-
for (const anim of batch.animations) if (anim.status === "paused") anim.resume();
|
|
54
|
-
}
|
|
100
|
+
if (timelineStatus !== "paused") return;
|
|
101
|
+
timelineStatus = "playing";
|
|
55
102
|
ticker.add(update);
|
|
56
103
|
};
|
|
57
104
|
const stop = () => {
|
|
58
|
-
if (
|
|
59
|
-
|
|
105
|
+
if (timelineStatus !== "playing" && timelineStatus !== "paused") return;
|
|
106
|
+
timelineStatus = "stopped";
|
|
60
107
|
ticker.remove(update);
|
|
61
|
-
for (const batch of batches) if (batch.started) for (const anim of batch.animations) anim.stop();
|
|
62
108
|
resolvePromise?.(timeline);
|
|
63
109
|
resolvePromise = void 0;
|
|
64
110
|
};
|
|
65
111
|
const skipToEnd = () => {
|
|
66
|
-
|
|
112
|
+
for (const layer of activeLayers) {
|
|
113
|
+
layer.runner.evaluate(1);
|
|
114
|
+
layer.runner.onEnded?.();
|
|
115
|
+
}
|
|
116
|
+
timelineStatus = "stopped";
|
|
67
117
|
ticker.remove(update);
|
|
68
|
-
for (const batch of batches) for (const anim of batch.animations) anim.skipToEnd();
|
|
69
118
|
onEnded?.();
|
|
70
119
|
resolvePromise?.(timeline);
|
|
71
120
|
resolvePromise = void 0;
|
|
72
121
|
};
|
|
73
122
|
const kill = () => {
|
|
74
|
-
|
|
75
|
-
ticker.remove(update);
|
|
76
|
-
for (const batch of batches) for (const anim of batch.animations) anim.kill();
|
|
77
|
-
resolvePromise = void 0;
|
|
78
|
-
};
|
|
79
|
-
const update = (deltaMs) => {
|
|
80
|
-
if (status !== "playing") return;
|
|
81
|
-
elapsedMs += deltaMs;
|
|
82
|
-
for (const batch of batches) if (!batch.started && elapsedMs >= batch.startAt) {
|
|
83
|
-
batch.started = true;
|
|
84
|
-
pendingAnimations += batch.animations.length;
|
|
85
|
-
for (const anim of batch.animations) anim.play().then(() => {
|
|
86
|
-
pendingAnimations--;
|
|
87
|
-
checkComplete();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
onProgress?.(totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 0);
|
|
91
|
-
if (batches.every((b) => b.started) && pendingAnimations <= 0) finish();
|
|
92
|
-
};
|
|
93
|
-
const checkComplete = () => {
|
|
94
|
-
if (pendingAnimations <= 0 && status === "playing" && batches.every((b) => b.started)) finish();
|
|
95
|
-
};
|
|
96
|
-
const finish = () => {
|
|
97
|
-
status = "stopped";
|
|
123
|
+
timelineStatus = "dead";
|
|
98
124
|
ticker.remove(update);
|
|
99
|
-
onEnded?.();
|
|
100
|
-
resolvePromise?.(timeline);
|
|
101
125
|
resolvePromise = void 0;
|
|
102
126
|
};
|
|
103
127
|
const setProgress = (value) => {
|
|
104
|
-
if (
|
|
128
|
+
if (timelineStatus === "playing") pause();
|
|
105
129
|
elapsedMs = Math.max(0, Math.min(1, value)) * totalDurationMs;
|
|
106
|
-
for (const
|
|
107
|
-
|
|
108
|
-
|
|
130
|
+
for (const layer of activeLayers) if (elapsedMs < layer.startAt) {
|
|
131
|
+
layer.runner.evaluate(0);
|
|
132
|
+
layer.started = false;
|
|
133
|
+
layer.ended = false;
|
|
109
134
|
} else {
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
135
|
+
layer.started = true;
|
|
136
|
+
const layerDuration = layer.endAt - layer.startAt;
|
|
137
|
+
const localProgress = layerDuration > 0 ? Math.min((elapsedMs - layer.startAt) / layerDuration, 1) : 1;
|
|
138
|
+
layer.runner.evaluate(localProgress);
|
|
139
|
+
layer.ended = localProgress >= 1;
|
|
116
140
|
}
|
|
141
|
+
remainingLayers = activeLayers.filter((l) => !l.ended).length;
|
|
117
142
|
};
|
|
118
143
|
const timeline = {
|
|
119
144
|
play,
|
|
@@ -124,10 +149,10 @@ var createTimeline = (layers, options) => {
|
|
|
124
149
|
kill,
|
|
125
150
|
setProgress,
|
|
126
151
|
get progress() {
|
|
127
|
-
return totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) :
|
|
152
|
+
return totalDurationMs > 0 ? Math.min(elapsedMs / totalDurationMs, 1) : 1;
|
|
128
153
|
},
|
|
129
154
|
get status() {
|
|
130
|
-
return
|
|
155
|
+
return timelineStatus;
|
|
131
156
|
},
|
|
132
157
|
get durationMs() {
|
|
133
158
|
return totalDurationMs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anim-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "JavaScript library for animating numbers",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"prepublishOnly": "npm run build",
|
|
31
31
|
"bench": "vitest bench src/benchmarks/ --run",
|
|
32
32
|
"bench:baseline": "vitest bench src/benchmarks/ --run --outputJson baselines/benchmark.json",
|
|
33
|
+
"bench:compare": "vitest bench src/benchmarks/ --run --compare baselines/benchmark.json",
|
|
33
34
|
"format": "oxfmt src/",
|
|
34
35
|
"format:check": "oxfmt --check src/",
|
|
35
36
|
"fmt": "oxfmt src/"
|