@xtia/timeline 1.1.19 → 1.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 +6 -4
- package/internal/emitters.d.ts +1 -1
- package/internal/emitters.js +8 -19
- package/internal/timeline.d.ts +11 -16
- package/internal/timeline.js +77 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ Timeline is a type-safe, seekable, deterministic choreography system that can co
|
|
|
12
12
|
|
|
13
13
|
## Basic Use:
|
|
14
14
|
|
|
15
|
-
`npm i @xtia/timeline`
|
|
15
|
+
Install with `npm i @xtia/timeline`
|
|
16
|
+
|
|
17
|
+
Timeline's primary building block is a *range* that emits a normalised progression (0 -> 1) as time flows through it. It can be expressively chained to ease progression, interpolate values and apply effects.
|
|
16
18
|
|
|
17
19
|
```ts
|
|
18
20
|
import { Timeline } from "@xtia/timeline";
|
|
@@ -57,12 +59,12 @@ timeline.play();
|
|
|
57
59
|
const firstFiveSeconds = timeline.range(0, 5000);
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
The range object is *applyable* and emits a progression value
|
|
62
|
+
The range object is *applyable* and emits a progression value when the Timeline seeks through or over that period.
|
|
61
63
|
|
|
62
64
|
```ts
|
|
63
65
|
firstFiveSeconds
|
|
64
66
|
.apply(
|
|
65
|
-
value => console.log(
|
|
67
|
+
value => console.log(value) // logs 0 -> 1 over 5 seconds
|
|
66
68
|
);
|
|
67
69
|
```
|
|
68
70
|
|
|
@@ -104,7 +106,7 @@ range
|
|
|
104
106
|
// each step in a chain is a 'pure', independent emitter that emits a
|
|
105
107
|
// transformation of its parent's emissions
|
|
106
108
|
const filenameEmitter = range
|
|
107
|
-
.tween(0,
|
|
109
|
+
.tween(0, 30)
|
|
108
110
|
.map(Math.floor)
|
|
109
111
|
.dedupe()
|
|
110
112
|
.map(n => `animation-frame-${n}.png`);
|
package/internal/emitters.d.ts
CHANGED
|
@@ -221,7 +221,7 @@ export declare class RangeProgression extends Emitter<number> {
|
|
|
221
221
|
*/
|
|
222
222
|
tap(cb: Handler<number>): RangeProgression;
|
|
223
223
|
}
|
|
224
|
-
export declare function createListenable<T>(
|
|
224
|
+
export declare function createListenable<T>(sourceListen?: () => UnsubscribeFunc | undefined): {
|
|
225
225
|
listen: (fn: (v: T) => void) => UnsubscribeFunc;
|
|
226
226
|
emit: (value: T) => void;
|
|
227
227
|
};
|
package/internal/emitters.js
CHANGED
|
@@ -7,14 +7,9 @@ export class Emitter {
|
|
|
7
7
|
this.onListen = onListen;
|
|
8
8
|
}
|
|
9
9
|
transform(handler) {
|
|
10
|
-
|
|
11
|
-
const parentListen = this.onListen;
|
|
12
|
-
const { emit, listen } = createListenable(() => parentUnsubscribe = parentListen(value => {
|
|
10
|
+
const { emit, listen } = createListenable(() => this.onListen(value => {
|
|
13
11
|
handler(value, emit);
|
|
14
|
-
})
|
|
15
|
-
parentUnsubscribe();
|
|
16
|
-
parentUnsubscribe = null;
|
|
17
|
-
});
|
|
12
|
+
}));
|
|
18
13
|
return listen;
|
|
19
14
|
}
|
|
20
15
|
/**
|
|
@@ -246,21 +241,14 @@ export class RangeProgression extends Emitter {
|
|
|
246
241
|
let parentUnsubscribe = null;
|
|
247
242
|
let pathUnsubscribe = null;
|
|
248
243
|
const { listen, emit } = createListenable(() => {
|
|
249
|
-
// onAddFirst - when first listener subscribes
|
|
250
244
|
pathUnsubscribe = pathEvaluator.listen(emit);
|
|
251
245
|
parentUnsubscribe = this.listen((timeValue) => {
|
|
252
246
|
pathEvaluator.seek(timeValue);
|
|
253
247
|
});
|
|
254
|
-
|
|
255
|
-
// onRemoveLast - when last listener unsubscribes
|
|
256
|
-
if (pathUnsubscribe) {
|
|
248
|
+
return () => {
|
|
257
249
|
pathUnsubscribe();
|
|
258
|
-
pathUnsubscribe = null;
|
|
259
|
-
}
|
|
260
|
-
if (parentUnsubscribe) {
|
|
261
250
|
parentUnsubscribe();
|
|
262
|
-
|
|
263
|
-
}
|
|
251
|
+
};
|
|
264
252
|
});
|
|
265
253
|
return new Emitter(listen);
|
|
266
254
|
}
|
|
@@ -303,13 +291,14 @@ export class RangeProgression extends Emitter {
|
|
|
303
291
|
return new RangeProgression(listen);
|
|
304
292
|
}
|
|
305
293
|
}
|
|
306
|
-
export function createListenable(
|
|
294
|
+
export function createListenable(sourceListen) {
|
|
307
295
|
const handlers = [];
|
|
296
|
+
let onRemoveLast;
|
|
308
297
|
const addListener = (fn) => {
|
|
309
298
|
const unique = { fn };
|
|
310
299
|
handlers.push(unique);
|
|
311
|
-
if (
|
|
312
|
-
|
|
300
|
+
if (sourceListen && handlers.length == 1)
|
|
301
|
+
onRemoveLast = sourceListen();
|
|
313
302
|
return () => {
|
|
314
303
|
const idx = handlers.indexOf(unique);
|
|
315
304
|
if (idx === -1)
|
package/internal/timeline.d.ts
CHANGED
|
@@ -20,6 +20,14 @@ type Period = {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function animate(durationMs: number): TimelineRange;
|
|
22
22
|
export declare function animate(period: Period): TimelineRange;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a looping Timeline and returns a range from it
|
|
25
|
+
*
|
|
26
|
+
* This timeline will play while it has active listeners
|
|
27
|
+
* @param duration Animation duration, in milliseconds, or a Period
|
|
28
|
+
* @returns Object representing a range on a single-use, autoplaying Timeline
|
|
29
|
+
*/
|
|
30
|
+
export declare function animate(duration: number | Period, looping: true): TimelineRange;
|
|
23
31
|
type TimelineOptions = {
|
|
24
32
|
atEnd?: {
|
|
25
33
|
wrapAt: number;
|
|
@@ -27,13 +35,8 @@ type TimelineOptions = {
|
|
|
27
35
|
restartAt: number;
|
|
28
36
|
} | keyof typeof EndAction;
|
|
29
37
|
timeScale?: number;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
fps?: number;
|
|
33
|
-
} | ({
|
|
34
|
-
autoplay?: false;
|
|
35
|
-
fps?: never;
|
|
36
|
-
}));
|
|
38
|
+
autoplay?: boolean;
|
|
39
|
+
};
|
|
37
40
|
export declare class Timeline {
|
|
38
41
|
/**
|
|
39
42
|
* Multiplies the speed at which `play()` progresses through the Timeline
|
|
@@ -83,11 +86,6 @@ export declare class Timeline {
|
|
|
83
86
|
* @param autoplay Pass `true` to begin playing at (1000 × this.timeScale) units per second immediately on creation
|
|
84
87
|
*/
|
|
85
88
|
constructor(autoplay: boolean);
|
|
86
|
-
/**
|
|
87
|
-
* Creates a Timeline that begins playing immediately at (1000 × this.timeScale) units per second
|
|
88
|
-
* @param autoplayFps Specifies frames per second
|
|
89
|
-
*/
|
|
90
|
-
constructor(autoplayFps: number);
|
|
91
89
|
/**
|
|
92
90
|
* @param autoplay If this argument is `true`, the Timeline will begin playing immediately on creation. If the argument is a number, the Timeline will begin playing at the specified frames per second
|
|
93
91
|
* @param endAction Specifies what should happen when the final position is passed by `play()`/`autoplay`
|
|
@@ -99,7 +97,7 @@ export declare class Timeline {
|
|
|
99
97
|
* * `{restartAt: number}`: Like `"restart"` but seeking back to `restartAt` instead of 0
|
|
100
98
|
* * `{wrapAt: number}`: Like `"wrap"` but as if restarting at `wrapAt` instead of 0
|
|
101
99
|
*/
|
|
102
|
-
constructor(autoplay: boolean
|
|
100
|
+
constructor(autoplay: boolean, endAction: {
|
|
103
101
|
wrapAt: number;
|
|
104
102
|
} | {
|
|
105
103
|
restartAt: number;
|
|
@@ -166,13 +164,10 @@ export declare class Timeline {
|
|
|
166
164
|
* Starts progression of the Timeline from its current position at (1000 × this.timeScale) units per second
|
|
167
165
|
*/
|
|
168
166
|
play(): void;
|
|
169
|
-
play(fps: number): void;
|
|
170
167
|
/**
|
|
171
168
|
* Performs a smooth-seek through a range at (1000 × this.timeScale) units per second
|
|
172
169
|
*/
|
|
173
170
|
play(range: TimelineRange, easer?: Easer): Promise<void>;
|
|
174
|
-
private playWithInterval;
|
|
175
|
-
private playWithRAF;
|
|
176
171
|
private next;
|
|
177
172
|
/**
|
|
178
173
|
* Stops normal progression instigated by play()
|
package/internal/timeline.js
CHANGED
|
@@ -3,38 +3,52 @@ import { TimelinePoint } from "./point.js";
|
|
|
3
3
|
import { TimelineRange } from "./range.js";
|
|
4
4
|
import { clamp } from "./utils.js";
|
|
5
5
|
const default_interval_fps = 60;
|
|
6
|
-
const
|
|
7
|
-
const cancelAnimFrame = globalThis?.cancelAnimationFrame;
|
|
8
|
-
const rafController = (() => {
|
|
9
|
-
const timelines = new Map();
|
|
6
|
+
const createRafDriver = (tick) => {
|
|
10
7
|
let rafId = null;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
previousTime = currentTime;
|
|
16
|
-
}
|
|
17
|
-
const elapsed = currentTime - previousTime;
|
|
18
|
-
previousTime = currentTime;
|
|
19
|
-
timelines.forEach((step, tl) => {
|
|
20
|
-
const delta = elapsed * tl.timeScale;
|
|
21
|
-
step(delta);
|
|
22
|
-
});
|
|
23
|
-
rafId = requestAnimFrame(frame);
|
|
8
|
+
return () => {
|
|
9
|
+
const frame = (ts) => {
|
|
10
|
+
tick(ts);
|
|
11
|
+
rafId = requestAnimationFrame(frame);
|
|
24
12
|
};
|
|
25
|
-
rafId =
|
|
13
|
+
rafId = requestAnimationFrame(frame);
|
|
14
|
+
return () => cancelAnimationFrame(rafId);
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
const createIntervalDriver = (tick) => {
|
|
18
|
+
return () => {
|
|
19
|
+
const intervalId = setInterval(() => tick(performance.now()), 1000 / default_interval_fps);
|
|
20
|
+
return () => clearInterval(intervalId);
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const masterDriver = (() => {
|
|
24
|
+
const timelines = new Map();
|
|
25
|
+
let previousTime = null;
|
|
26
|
+
let pause = null;
|
|
27
|
+
const step = (currentTime) => {
|
|
28
|
+
if (previousTime === null) {
|
|
29
|
+
previousTime = currentTime;
|
|
30
|
+
}
|
|
31
|
+
const delta = currentTime - previousTime;
|
|
32
|
+
previousTime = currentTime;
|
|
33
|
+
timelines.forEach((step, tl) => {
|
|
34
|
+
step(delta * tl.timeScale);
|
|
35
|
+
});
|
|
26
36
|
};
|
|
37
|
+
const start = "requestAnimationFrame" in globalThis
|
|
38
|
+
? createRafDriver(step)
|
|
39
|
+
: createIntervalDriver(step);
|
|
27
40
|
return {
|
|
28
41
|
add: (timeline, stepFn) => {
|
|
29
42
|
timelines.set(timeline, stepFn);
|
|
30
|
-
if (
|
|
31
|
-
|
|
43
|
+
if (timelines.size === 1) {
|
|
44
|
+
previousTime = null;
|
|
45
|
+
pause = start();
|
|
46
|
+
}
|
|
32
47
|
},
|
|
33
48
|
remove: (timeline) => {
|
|
34
49
|
timelines.delete(timeline);
|
|
35
50
|
if (timelines.size === 0) {
|
|
36
|
-
|
|
37
|
-
rafId = null;
|
|
51
|
+
pause();
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
};
|
|
@@ -45,11 +59,31 @@ const EndAction = {
|
|
|
45
59
|
wrap: 2,
|
|
46
60
|
restart: 3,
|
|
47
61
|
};
|
|
48
|
-
export function animate(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
?
|
|
52
|
-
:
|
|
62
|
+
export function animate(duration, looping = false) {
|
|
63
|
+
const tl = new Timeline(false, "wrap");
|
|
64
|
+
const durationMs = typeof duration == "number"
|
|
65
|
+
? duration
|
|
66
|
+
: duration.asMilliseconds;
|
|
67
|
+
const parentRange = tl.range(0, durationMs).ease();
|
|
68
|
+
if (looping) {
|
|
69
|
+
let listeners = 0;
|
|
70
|
+
const range = new TimelineRange(h => {
|
|
71
|
+
if (++listeners == 1) {
|
|
72
|
+
tl.play();
|
|
73
|
+
}
|
|
74
|
+
let unsub = parentRange.apply(h);
|
|
75
|
+
return () => {
|
|
76
|
+
if (--listeners == 0)
|
|
77
|
+
tl.pause();
|
|
78
|
+
unsub();
|
|
79
|
+
};
|
|
80
|
+
}, tl, tl.start, tl.point(durationMs));
|
|
81
|
+
return range;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
tl.play();
|
|
85
|
+
return parentRange;
|
|
86
|
+
}
|
|
53
87
|
}
|
|
54
88
|
export class Timeline {
|
|
55
89
|
/**
|
|
@@ -125,21 +159,12 @@ export class Timeline {
|
|
|
125
159
|
if (typeof optionsOrAutoplay == "object") {
|
|
126
160
|
endAction = optionsOrAutoplay.atEnd ?? "pause";
|
|
127
161
|
this.timeScale = optionsOrAutoplay.timeScale ?? 1;
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
this.play(optionsOrAutoplay.fps);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
this.play();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
162
|
+
if (optionsOrAutoplay.autoplay)
|
|
163
|
+
this.play();
|
|
136
164
|
}
|
|
137
165
|
else if (optionsOrAutoplay === true) {
|
|
138
166
|
this.play();
|
|
139
167
|
}
|
|
140
|
-
else if (typeof optionsOrAutoplay == "number") {
|
|
141
|
-
this.play(optionsOrAutoplay);
|
|
142
|
-
}
|
|
143
168
|
if (typeof endAction == "object"
|
|
144
169
|
&& "restartAt" in endAction) {
|
|
145
170
|
this.endAction = {
|
|
@@ -173,9 +198,12 @@ export class Timeline {
|
|
|
173
198
|
this._endPosition = position;
|
|
174
199
|
this._progression?.emit(this._currentTime / position);
|
|
175
200
|
}
|
|
176
|
-
const { emit, listen } = createListenable(() =>
|
|
177
|
-
|
|
178
|
-
|
|
201
|
+
const { emit, listen } = createListenable(() => {
|
|
202
|
+
this.points.push(data);
|
|
203
|
+
return () => {
|
|
204
|
+
const idx = this.points.indexOf(data);
|
|
205
|
+
this.points.splice(idx, 1);
|
|
206
|
+
};
|
|
179
207
|
});
|
|
180
208
|
const addHandler = (handler) => {
|
|
181
209
|
if (this.seeking)
|
|
@@ -200,9 +228,12 @@ export class Timeline {
|
|
|
200
228
|
const startPosition = startPoint.position;
|
|
201
229
|
const duration = optionalDuration ?? this._endPosition - startPosition;
|
|
202
230
|
const endPoint = this.point(startPosition + duration);
|
|
203
|
-
const { emit, listen } = createListenable(() =>
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
const { emit, listen } = createListenable(() => {
|
|
232
|
+
this.ranges.push(rangeData);
|
|
233
|
+
return () => {
|
|
234
|
+
const idx = this.ranges.indexOf(rangeData);
|
|
235
|
+
this.ranges.splice(idx, 1);
|
|
236
|
+
};
|
|
206
237
|
});
|
|
207
238
|
const rangeData = {
|
|
208
239
|
position: startPosition,
|
|
@@ -380,26 +411,8 @@ export class Timeline {
|
|
|
380
411
|
this.seek(arg.start);
|
|
381
412
|
return this.seek(arg.end, arg.duration / this.timeScale, easer);
|
|
382
413
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
this.playWithInterval(arg ?? default_interval_fps);
|
|
388
|
-
}
|
|
389
|
-
playWithInterval(fps) {
|
|
390
|
-
let previousTime = performance.now();
|
|
391
|
-
const interval = setInterval(() => {
|
|
392
|
-
const newTime = performance.now();
|
|
393
|
-
const elapsed = newTime - previousTime;
|
|
394
|
-
previousTime = newTime;
|
|
395
|
-
let delta = elapsed * this.timeScale;
|
|
396
|
-
this.next(delta);
|
|
397
|
-
}, 1000 / fps);
|
|
398
|
-
this._pause = () => clearInterval(interval);
|
|
399
|
-
}
|
|
400
|
-
playWithRAF() {
|
|
401
|
-
rafController.add(this, n => this.next(n));
|
|
402
|
-
this._pause = () => rafController.remove(this);
|
|
414
|
+
masterDriver.add(this, n => this.next(n));
|
|
415
|
+
this._pause = () => masterDriver.remove(this);
|
|
403
416
|
}
|
|
404
417
|
next(delta) {
|
|
405
418
|
if (this._currentTime + delta <= this._endPosition) {
|