@xtia/timeline 1.0.8 → 1.0.10
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 +11 -11
- package/internal/emitters.d.ts +1 -0
- package/internal/emitters.js +1 -0
- package/internal/point.d.ts +1 -1
- package/internal/range.d.ts +14 -4
- package/internal/range.js +7 -1
- package/internal/timeline.d.ts +21 -12
- package/internal/timeline.js +43 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
**Timeline** is a general‑purpose, environment-agnostic choreography engine that lets you orchestrate any sequence of value changes; numbers, vectors, colour tokens, custom blendable objects, or arbitrary data structures.
|
|
6
6
|
|
|
7
7
|
* [API Reference](#reference)
|
|
8
|
+
* [Playground](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
|
|
8
9
|
|
|
9
10
|
## Basic Use:
|
|
10
11
|
|
|
@@ -21,7 +22,7 @@ timeline
|
|
|
21
22
|
.range(0, 1000)
|
|
22
23
|
.tween("#646", "#000")
|
|
23
24
|
.listen(
|
|
24
|
-
value =>
|
|
25
|
+
value => element.style.background = value
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
// add another tween to make a slow typing effect
|
|
@@ -29,15 +30,14 @@ const message = "Hi, planet!";
|
|
|
29
30
|
timeline
|
|
30
31
|
.range(500, 2000)
|
|
31
32
|
.tween(0, message.length)
|
|
33
|
+
.map(n => message.substring(0, n))
|
|
32
34
|
.listen(
|
|
33
|
-
|
|
35
|
+
s => element.textContent = s
|
|
34
36
|
);
|
|
35
37
|
|
|
36
38
|
// use an easing function
|
|
37
39
|
timeline
|
|
38
|
-
.
|
|
39
|
-
.delta(500)
|
|
40
|
-
.range(3000)
|
|
40
|
+
.range(0, 3000)
|
|
41
41
|
.ease("bounce")
|
|
42
42
|
.tween("50%", "0%")
|
|
43
43
|
.listen(
|
|
@@ -100,8 +100,8 @@ range
|
|
|
100
100
|
.map(n => `animation-frame-${n}.png`)
|
|
101
101
|
.listen(filename => img.src = filename);
|
|
102
102
|
|
|
103
|
-
// each step in
|
|
104
|
-
//
|
|
103
|
+
// each step in a chain is a 'pure', independent emitter that emits a
|
|
104
|
+
// transformation of its parent's emissions
|
|
105
105
|
const filenameEmitter = range
|
|
106
106
|
.tween(0, 3)
|
|
107
107
|
.map(Math.floor)
|
|
@@ -198,7 +198,7 @@ range
|
|
|
198
198
|
import { RGBA } from "@xtia/rgba";
|
|
199
199
|
range
|
|
200
200
|
.tween(RGBA.parse("#c971a7"), RGBA.parse("#fff"))
|
|
201
|
-
.listen(v => element.style.background = v
|
|
201
|
+
.listen(v => element.style.background = v);
|
|
202
202
|
|
|
203
203
|
import { Angle } from "@xtia/mezr";
|
|
204
204
|
range
|
|
@@ -414,15 +414,15 @@ Performs an interruptable 'smooth seek' to a specified position, lasting `durati
|
|
|
414
414
|
|
|
415
415
|
Returns a Promise that will be resolved when the smooth seek is completed (or is interrupted by another seek\*).
|
|
416
416
|
|
|
417
|
-
\*
|
|
417
|
+
\* If a smooth seek is interrupted by another seek, the interrupted seek will immediately complete before the new seek is applied, to ensure any resulting state reflects expectations set by the first seek.
|
|
418
418
|
|
|
419
419
|
##### `play(): void`
|
|
420
420
|
|
|
421
|
-
Begins playing through the Timeline, from its current position, at (1000
|
|
421
|
+
Begins playing through the Timeline, from its current position, at (1000 × `timeScale`) units per second, updating 60 times per second.
|
|
422
422
|
|
|
423
423
|
##### `play(fps): void`
|
|
424
424
|
|
|
425
|
-
Begins playing through the Timeline, from its current position, at (1000
|
|
425
|
+
Begins playing through the Timeline, from its current position, at (1000 × `timeScale`) units per second, updating `fps` times per second.
|
|
426
426
|
|
|
427
427
|
##### `tween<T>(start, duration, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
|
|
428
428
|
|
package/internal/emitters.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class Emitter<T> {
|
|
|
8
8
|
protected constructor(onListen: ListenFunc<T>);
|
|
9
9
|
/**
|
|
10
10
|
* Used by tap() to create a clone of an Emitter with a redirected onListen
|
|
11
|
+
*
|
|
11
12
|
* Should be overridden in all Emitter subclasses
|
|
12
13
|
* @see {@link TimelineRange.redirect}
|
|
13
14
|
* @param listen
|
package/internal/emitters.js
CHANGED
package/internal/point.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Emitter, ListenFunc } from "./emitters";
|
|
|
2
2
|
import { TimelineRange } from "./range";
|
|
3
3
|
import { Timeline } from "./timeline";
|
|
4
4
|
export type PointEvent = {
|
|
5
|
-
direction: -1 | 1;
|
|
5
|
+
readonly direction: -1 | 1;
|
|
6
6
|
};
|
|
7
7
|
export declare class TimelinePoint extends Emitter<PointEvent> {
|
|
8
8
|
private timeline;
|
package/internal/range.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export declare class TimelineRange extends RangeProgression {
|
|
|
8
8
|
/** The duration of this range */
|
|
9
9
|
readonly duration: number;
|
|
10
10
|
private endPosition;
|
|
11
|
+
/** The point on the Timeline at which this range begins */
|
|
12
|
+
readonly start: TimelinePoint;
|
|
13
|
+
/** The point on the Timeline at which this range ends */
|
|
14
|
+
readonly end: TimelinePoint;
|
|
11
15
|
/** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
|
|
12
16
|
constructor(onListen: ListenFunc<number>, timeline: Timeline, startPosition: number,
|
|
13
17
|
/** The duration of this range */
|
|
@@ -52,9 +56,15 @@ export declare class TimelineRange extends RangeProgression {
|
|
|
52
56
|
* @returns true if the provided point is within the range
|
|
53
57
|
*/
|
|
54
58
|
contains(point: TimelinePoint): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a range is fully within this range
|
|
61
|
+
* @param range The range to check
|
|
62
|
+
* @returns true if the provided range is within the parent
|
|
63
|
+
*/
|
|
55
64
|
contains(range: TimelineRange): boolean;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
overlaps(range: TimelineRange): boolean;
|
|
66
|
+
overlaps(range: {
|
|
67
|
+
position: number;
|
|
68
|
+
duration: number;
|
|
69
|
+
}): boolean;
|
|
60
70
|
}
|
package/internal/range.js
CHANGED
|
@@ -11,9 +11,9 @@ export class TimelineRange extends RangeProgression {
|
|
|
11
11
|
this.startPosition = startPosition;
|
|
12
12
|
this.duration = duration;
|
|
13
13
|
this.redirect = (listen) => new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
|
|
14
|
-
this.start = timeline.point(startPosition);
|
|
15
14
|
this.endPosition = startPosition + duration;
|
|
16
15
|
this.end = timeline.point(this.endPosition);
|
|
16
|
+
this.start = timeline.point(startPosition);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Creates two ranges by seperating one at a given point
|
|
@@ -93,4 +93,10 @@ export class TimelineRange extends RangeProgression {
|
|
|
93
93
|
: [target.startPosition, target.startPosition + target.duration];
|
|
94
94
|
return targetStart >= this.startPosition && targetEnd < this.endPosition;
|
|
95
95
|
}
|
|
96
|
+
overlaps(range) {
|
|
97
|
+
const [start, end] = range instanceof TimelineRange
|
|
98
|
+
? [range.startPosition, range.endPosition]
|
|
99
|
+
: [range.position, range.position + range.duration];
|
|
100
|
+
return this.startPosition <= end && this.endPosition >= start;
|
|
101
|
+
}
|
|
96
102
|
}
|
package/internal/timeline.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Easer, easers } from "./easing";
|
|
2
|
+
import { RangeProgression } from "./emitters";
|
|
2
3
|
import { TimelinePoint } from "./point";
|
|
3
4
|
import { TimelineRange } from "./range";
|
|
4
5
|
import { Tweenable } from "./tween";
|
|
@@ -11,7 +12,7 @@ declare const EndAction: {
|
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
14
|
* Creates an autoplaying Timeline and returns a range from it
|
|
14
|
-
* @param duration
|
|
15
|
+
* @param duration Animation duration, in milliseconds
|
|
15
16
|
* @returns Object representing a range on a single-use, autoplaying Timeline
|
|
16
17
|
*/
|
|
17
18
|
export declare function animate(duration: number): TimelineRange;
|
|
@@ -36,14 +37,22 @@ export declare class Timeline {
|
|
|
36
37
|
private smoothSeeker;
|
|
37
38
|
private seeking;
|
|
38
39
|
readonly start: TimelinePoint;
|
|
39
|
-
private
|
|
40
|
+
private progressionHandlers;
|
|
41
|
+
private _progression;
|
|
42
|
+
/**
|
|
43
|
+
* Listenable: emits a progression value (0..1) when the Timeline's internal
|
|
44
|
+
* position changes, and when the Timeline's total duration is extended
|
|
45
|
+
*
|
|
46
|
+
* **Experimental**
|
|
47
|
+
*/
|
|
48
|
+
get progression(): RangeProgression;
|
|
40
49
|
constructor();
|
|
41
50
|
/**
|
|
42
|
-
* @param autoplay Pass `true` to begin playing at (1000
|
|
51
|
+
* @param autoplay Pass `true` to begin playing at (1000 × this.timeScale) units per second immediately on creation
|
|
43
52
|
*/
|
|
44
53
|
constructor(autoplay: boolean);
|
|
45
54
|
/**
|
|
46
|
-
* Creates a Timeline that begins playing immediately at (1000
|
|
55
|
+
* Creates a Timeline that begins playing immediately at (1000 × this.timeScale) units per second
|
|
47
56
|
* @param autoplayFps Specifies frames per second
|
|
48
57
|
*/
|
|
49
58
|
constructor(autoplayFps: number);
|
|
@@ -51,12 +60,12 @@ export declare class Timeline {
|
|
|
51
60
|
* @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
|
|
52
61
|
* @param endAction Specifies what should happen when the final position is passed by `play()`/`autoplay`
|
|
53
62
|
*
|
|
54
|
-
* `"pause"`: **(default)** the Timeline will pause at its final position
|
|
55
|
-
* `"continue"`: The Timeline will continue progressing beyond its final position
|
|
56
|
-
* `"restart"`: The Timeline will seek back to 0 then forward to account for any overshoot and continue progressing
|
|
57
|
-
* `"wrap"`: The Timeline's position will continue to increase beyond the final position, but Points and Ranges will be activated as if looping
|
|
58
|
-
* `{restartAt: number}`: Like `"restart"` but seeking back to `restartAt` instead of 0
|
|
59
|
-
* `{wrapAt: number}`: Like `"wrap"` but as if restarting at `wrapAt` instead of 0
|
|
63
|
+
* * `"pause"`: **(default)** the Timeline will pause at its final position
|
|
64
|
+
* * `"continue"`: The Timeline will continue progressing beyond its final position
|
|
65
|
+
* * `"restart"`: The Timeline will seek back to 0 then forward to account for any overshoot and continue progressing
|
|
66
|
+
* * `"wrap"`: The Timeline's position will continue to increase beyond the final position, but Points and Ranges will be activated as if looping
|
|
67
|
+
* * `{restartAt: number}`: Like `"restart"` but seeking back to `restartAt` instead of 0
|
|
68
|
+
* * `{wrapAt: number}`: Like `"wrap"` but as if restarting at `wrapAt` instead of 0
|
|
60
69
|
*/
|
|
61
70
|
constructor(autoplay: boolean | number, endAction: {
|
|
62
71
|
wrapAt: number;
|
|
@@ -99,7 +108,7 @@ export declare class Timeline {
|
|
|
99
108
|
/**
|
|
100
109
|
* Smooth-seeks to a specified position
|
|
101
110
|
*
|
|
102
|
-
*
|
|
111
|
+
* Immediately completes and replaces any ongoing smooth-seek process on this Timeline
|
|
103
112
|
* @param toPosition
|
|
104
113
|
* @param duration Duration of the smooth-seek process in milliseconds
|
|
105
114
|
* @param easer Optional easing function for the smooth-seek process
|
|
@@ -111,7 +120,7 @@ export declare class Timeline {
|
|
|
111
120
|
private seekRanges;
|
|
112
121
|
private sortEntries;
|
|
113
122
|
/**
|
|
114
|
-
* Starts progression of the Timeline from its current position at (1000
|
|
123
|
+
* Starts progression of the Timeline from its current position at (1000 × this.timeScale) units per second
|
|
115
124
|
*/
|
|
116
125
|
play(): void;
|
|
117
126
|
play(fps: number): void;
|
package/internal/timeline.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RangeProgression } from "./emitters";
|
|
1
2
|
import { TimelinePoint } from "./point";
|
|
2
3
|
import { TimelineRange } from "./range";
|
|
3
4
|
import { clamp } from "./utils";
|
|
@@ -10,7 +11,7 @@ const EndAction = {
|
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
13
|
* Creates an autoplaying Timeline and returns a range from it
|
|
13
|
-
* @param duration
|
|
14
|
+
* @param duration Animation duration, in milliseconds
|
|
14
15
|
* @returns Object representing a range on a single-use, autoplaying Timeline
|
|
15
16
|
*/
|
|
16
17
|
export function animate(duration) {
|
|
@@ -27,6 +28,17 @@ export class Timeline {
|
|
|
27
28
|
get end() {
|
|
28
29
|
return this.point(this._endPosition);
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Listenable: emits a progression value (0..1) when the Timeline's internal
|
|
33
|
+
* position changes, and when the Timeline's total duration is extended
|
|
34
|
+
*
|
|
35
|
+
* **Experimental**
|
|
36
|
+
*/
|
|
37
|
+
get progression() {
|
|
38
|
+
if (this._progression === null)
|
|
39
|
+
this._progression = new TimelineProgressionEmitter(this.progressionHandlers);
|
|
40
|
+
return this._progression;
|
|
41
|
+
}
|
|
30
42
|
constructor(autoplay = false, endAction = "pause") {
|
|
31
43
|
/**
|
|
32
44
|
* Multiplies the speed at which `play()` progresses through the Timeline
|
|
@@ -43,7 +55,8 @@ export class Timeline {
|
|
|
43
55
|
this.smoothSeeker = null;
|
|
44
56
|
this.seeking = false;
|
|
45
57
|
this.start = this.point(0);
|
|
46
|
-
this.
|
|
58
|
+
this.progressionHandlers = [];
|
|
59
|
+
this._progression = null;
|
|
47
60
|
if (endAction == "loop")
|
|
48
61
|
endAction = "restart";
|
|
49
62
|
if (autoplay !== false) {
|
|
@@ -78,8 +91,10 @@ export class Timeline {
|
|
|
78
91
|
* Listenable: this point will emit a PointEvent whenever a `seek()` reaches or passes it
|
|
79
92
|
*/
|
|
80
93
|
point(position) {
|
|
81
|
-
if (position > this._endPosition)
|
|
94
|
+
if (position > this._endPosition) {
|
|
82
95
|
this._endPosition = position;
|
|
96
|
+
this.progressionHandlers.slice().forEach(h => h(this._currentTime / position));
|
|
97
|
+
}
|
|
83
98
|
const handlers = [];
|
|
84
99
|
const data = {
|
|
85
100
|
handlers,
|
|
@@ -113,9 +128,9 @@ export class Timeline {
|
|
|
113
128
|
: start;
|
|
114
129
|
const startPosition = startPoint.position;
|
|
115
130
|
const duration = optionalDuration ?? this._endPosition - startPosition;
|
|
116
|
-
const endPosition = startPosition + duration;
|
|
117
|
-
if (endPosition > this._endPosition)
|
|
118
|
-
|
|
131
|
+
// const endPosition = startPosition + duration;
|
|
132
|
+
//if (endPosition > this._endPosition) this._endPosition = endPosition;
|
|
133
|
+
// ^ leave this to range's point() calls
|
|
119
134
|
const handlers = [];
|
|
120
135
|
const range = {
|
|
121
136
|
position: startPosition,
|
|
@@ -168,7 +183,7 @@ export class Timeline {
|
|
|
168
183
|
}
|
|
169
184
|
if (this.smoothSeeker !== null) {
|
|
170
185
|
this.smoothSeeker.pause();
|
|
171
|
-
// ensure any awaits are resolved for the previous seek
|
|
186
|
+
// ensure any awaits are resolved for the previous seek
|
|
172
187
|
this.smoothSeeker.seek(this.smoothSeeker.end);
|
|
173
188
|
this.smoothSeeker = null;
|
|
174
189
|
}
|
|
@@ -209,7 +224,6 @@ export class Timeline {
|
|
|
209
224
|
throw e;
|
|
210
225
|
}
|
|
211
226
|
this._currentTime = toPosition;
|
|
212
|
-
this.positionHandlers.slice().forEach(h => h(toPosition));
|
|
213
227
|
this.seeking = false;
|
|
214
228
|
}
|
|
215
229
|
seekPoints(to) {
|
|
@@ -218,27 +232,27 @@ export class Timeline {
|
|
|
218
232
|
const pointsBetween = this.points.filter(direction > 0
|
|
219
233
|
? p => p.position > from && p.position <= to
|
|
220
234
|
: p => p.position <= from && p.position > to);
|
|
235
|
+
const eventData = {
|
|
236
|
+
direction
|
|
237
|
+
};
|
|
221
238
|
pointsBetween.slice().forEach(p => {
|
|
222
239
|
this.seekRanges(p.position);
|
|
223
240
|
this._currentTime = p.position;
|
|
224
|
-
const eventData = {
|
|
225
|
-
direction
|
|
226
|
-
};
|
|
227
241
|
p.handlers.slice().forEach(h => h(eventData));
|
|
228
242
|
});
|
|
229
243
|
}
|
|
230
244
|
seekRanges(to) {
|
|
231
|
-
const fromTime = this._currentTime;
|
|
245
|
+
const fromTime = Math.min(this._currentTime, to);
|
|
246
|
+
const toTime = Math.max(this._currentTime, to);
|
|
232
247
|
this.ranges.slice().forEach((range) => {
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
if (Math.min(position, end) <= Math.max(to, fromTime)
|
|
237
|
-
&& Math.min(to, fromTime) <= Math.max(position, end)) {
|
|
248
|
+
const rangeEnd = range.position + range.duration;
|
|
249
|
+
const overlaps = fromTime <= rangeEnd && toTime >= range.position;
|
|
250
|
+
if (overlaps) {
|
|
238
251
|
let progress = clamp((to - range.position) / range.duration, 0, 1);
|
|
239
252
|
range.handlers.slice().forEach(h => h(progress));
|
|
240
253
|
}
|
|
241
254
|
});
|
|
255
|
+
this.progressionHandlers.slice().forEach(h => h(fromTime / this._endPosition));
|
|
242
256
|
}
|
|
243
257
|
sortEntries(direction) {
|
|
244
258
|
this.currentSortDirection = direction;
|
|
@@ -340,6 +354,18 @@ export class Timeline {
|
|
|
340
354
|
return this._currentTime;
|
|
341
355
|
}
|
|
342
356
|
}
|
|
357
|
+
class TimelineProgressionEmitter extends RangeProgression {
|
|
358
|
+
constructor(handlers) {
|
|
359
|
+
super((handler) => {
|
|
360
|
+
const unique = (n) => handler(n);
|
|
361
|
+
handlers.push(unique);
|
|
362
|
+
return () => {
|
|
363
|
+
const idx = handlers.indexOf(unique);
|
|
364
|
+
handlers.splice(idx, 1);
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
343
369
|
const sortEvents = (a, b) => {
|
|
344
370
|
return a.position - b.position;
|
|
345
371
|
};
|