@xtia/timeline 1.1.17 → 1.1.19
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 +63 -9
- package/internal/emitters.d.ts +1 -1
- package/internal/emitters.js +2 -2
- package/internal/path.d.ts +2 -1
- package/internal/path.js +25 -15
- package/internal/timeline.js +38 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ Timeline is a type-safe, seekable, deterministic choreography system that can co
|
|
|
7
7
|
* [API Reference](#reference)
|
|
8
8
|
* [Playground](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
|
|
9
9
|
* [Intro by HL](https://codepen.io/H-L-the-lessful/full/vELdyvB)
|
|
10
|
+
* [Sprite sheet demo](https://codepen.io/xtaltia/pen/jEWrXOg)
|
|
11
|
+
* [Web animation benchmark](https://codepen.io/xtaltia/pen/qENNzRw)
|
|
10
12
|
|
|
11
13
|
## Basic Use:
|
|
12
14
|
|
|
@@ -137,7 +139,7 @@ Points represent specific times in the Timeline.
|
|
|
137
139
|
```ts
|
|
138
140
|
const twoSecondsIn = timeline.point(2000);
|
|
139
141
|
const fiveSecondsIn = firstFiveSeconds.end;
|
|
140
|
-
const sixSecondsIn =
|
|
142
|
+
const sixSecondsIn = fiveSecondsIn.delta(1000);
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
Points emit `PointEvent` objects when their position is reached or passed.
|
|
@@ -156,8 +158,8 @@ Directionality can also be leveraged with `point.applyDirectional()`:
|
|
|
156
158
|
|
|
157
159
|
```ts
|
|
158
160
|
twoSecondsIn.applyDirectional(
|
|
159
|
-
parent.append(element), // do
|
|
160
|
-
element.remove() // undo
|
|
161
|
+
() => parent.append(element), // do
|
|
162
|
+
() => element.remove() // undo
|
|
161
163
|
);
|
|
162
164
|
```
|
|
163
165
|
|
|
@@ -178,7 +180,7 @@ timeline
|
|
|
178
180
|
|
|
179
181
|
## More on tweening
|
|
180
182
|
|
|
181
|
-
Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(
|
|
183
|
+
Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(to: this, progression: number): this`, by the progression value emitted by their parent.
|
|
182
184
|
|
|
183
185
|
```ts
|
|
184
186
|
const range = timeline.range(0, 2000);
|
|
@@ -200,7 +202,7 @@ range
|
|
|
200
202
|
.apply(v => element.style.color = v);
|
|
201
203
|
|
|
202
204
|
// blendable objects
|
|
203
|
-
// (T extends { blend(
|
|
205
|
+
// (T extends { blend(to: this, progression: number): this })
|
|
204
206
|
import { RGBA } from "@xtia/rgba";
|
|
205
207
|
range
|
|
206
208
|
.tween(RGBA.parse("#c971a7"), RGBA.parse("#fff"))
|
|
@@ -643,10 +645,6 @@ Returns a Promise that will be resolved when the range playthrough completes.
|
|
|
643
645
|
|
|
644
646
|
##### `grow(delta, anchor?): TimelineRange`
|
|
645
647
|
|
|
646
|
-
Creates a new range on the parent Timeline. The location and duration of the new range are copied from this range and grown from an anchor point, specified as a normalised (0..1) progression of the parent range.
|
|
647
|
-
|
|
648
|
-
##### `grow(delta, anchor?): TimelineRange`
|
|
649
|
-
|
|
650
648
|
Creates a new range on the parent Timeline. The location and duration of the new range are copied from this range and scaled multiplicatively from an anchor point, specified as a normalised (0..1) progression of the parent range.
|
|
651
649
|
|
|
652
650
|
##### `subdivide(n): TimelineRange[]`
|
|
@@ -665,6 +663,62 @@ Returns true if the given [`TimelinePoint`](#timelinepoint-class) sits within th
|
|
|
665
663
|
|
|
666
664
|
Returns true if the given range overlaps with this range.
|
|
667
665
|
|
|
666
|
+
##### `path(steps: Path): Emitter<[number, number]>
|
|
667
|
+
|
|
668
|
+
Creates an emitter that follows a given path, emitting `[x, y]` (`XY`) as its parent range is progressed.
|
|
669
|
+
|
|
670
|
+
Path segments can be expressed as a mix of `[x, y]`, resolver functions (`progress => XY`) and Segment descriptor objects.
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
type LineSegment = {
|
|
674
|
+
type: "line";
|
|
675
|
+
from?: XY;
|
|
676
|
+
to: XY;
|
|
677
|
+
speed?: number;
|
|
678
|
+
ease?: Easer | keyof typeof easers;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
type CurveSegment = {
|
|
682
|
+
type: "curve";
|
|
683
|
+
from?: XY;
|
|
684
|
+
to: XY;
|
|
685
|
+
control1: XY;
|
|
686
|
+
control2: XY;
|
|
687
|
+
speed?: number;
|
|
688
|
+
ease?: Easer | keyof typeof easers;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
type CustomSegment = {
|
|
692
|
+
get: SegmentEvaluator;
|
|
693
|
+
length?: number;
|
|
694
|
+
ease?: Easer | keyof typeof easers;
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
* If the first element is `[x, y]`, it defines the path's starting position.
|
|
699
|
+
* If the first element is a non-custom descriptor object it must include a `from` property.
|
|
700
|
+
* Duration of segments defined as resolver functions, and custom segments without a `length` property, will be estimated by point sampling
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
// simple path with coordinates
|
|
704
|
+
const eg1 = range.path([[0, 0], [100, 50], [200, 0]])
|
|
705
|
+
|
|
706
|
+
// mixed path with curve segments
|
|
707
|
+
const eg2 = range.path([
|
|
708
|
+
[0, 0], // start position
|
|
709
|
+
{
|
|
710
|
+
type: 'curve',
|
|
711
|
+
to: [100, 100],
|
|
712
|
+
control1: [25, 0],
|
|
713
|
+
control2: [75, 100],
|
|
714
|
+
ease: easers.easeOut
|
|
715
|
+
},
|
|
716
|
+
[50, 50] // straight line to final position
|
|
717
|
+
]);
|
|
718
|
+
|
|
719
|
+
eg2.map(([x, y]) => [x + "%", y + "%"])
|
|
720
|
+
.apply(([left, top]) => element.style({ left, top }));
|
|
721
|
+
```
|
|
668
722
|
|
|
669
723
|
|
|
670
724
|
|
package/internal/emitters.d.ts
CHANGED
package/internal/emitters.js
CHANGED
package/internal/path.d.ts
CHANGED
|
@@ -23,7 +23,8 @@ type Segment = StaticSegment | CustomSegment;
|
|
|
23
23
|
type CustomSegment = {
|
|
24
24
|
get: SegmentEvaluator;
|
|
25
25
|
length?: number;
|
|
26
|
-
|
|
26
|
+
ease?: Easer | keyof typeof easers;
|
|
27
|
+
} | SegmentEvaluator;
|
|
27
28
|
type FirstSegment = CustomSegment | (StaticSegment & {
|
|
28
29
|
from: XY;
|
|
29
30
|
});
|
package/internal/path.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Timeline } from "./timeline.js";
|
|
|
3
3
|
export function createPathEmitter(input) {
|
|
4
4
|
const { listen, emit } = createListenable();
|
|
5
5
|
const tl = new Timeline();
|
|
6
|
+
let lastXY = [0, 0];
|
|
6
7
|
const firstItem = input[0];
|
|
7
8
|
let getCurrentPosition;
|
|
8
9
|
let items;
|
|
@@ -17,15 +18,20 @@ export function createPathEmitter(input) {
|
|
|
17
18
|
}
|
|
18
19
|
items.forEach(item => {
|
|
19
20
|
const speed = typeof item === 'object' && !Array.isArray(item) && "speed" in item ? item.speed ?? 1 : 1;
|
|
20
|
-
if (
|
|
21
|
+
if (typeof item == "function") {
|
|
22
|
+
const length = estimateLength(item);
|
|
23
|
+
tl.end.range(length / speed).apply(v => lastXY = item(v));
|
|
24
|
+
getCurrentPosition = () => item(1);
|
|
25
|
+
}
|
|
26
|
+
else if (Array.isArray(item)) { // XY
|
|
21
27
|
const start = getCurrentPosition();
|
|
22
28
|
const length = distance(start, item);
|
|
23
|
-
tl.end.range(length / speed).tween(start, item).apply(
|
|
29
|
+
tl.end.range(length / speed).tween(start, item).apply(v => lastXY = v);
|
|
24
30
|
getCurrentPosition = () => item;
|
|
25
31
|
}
|
|
26
32
|
else if ("get" in item) { // custom segment
|
|
27
33
|
const length = item.length ?? estimateLength(item.get);
|
|
28
|
-
tl.end.range(length / speed).
|
|
34
|
+
tl.end.range(length / speed).ease(item.ease).apply(v => lastXY = item.get(v));
|
|
29
35
|
getCurrentPosition = () => item.get(1);
|
|
30
36
|
}
|
|
31
37
|
else
|
|
@@ -33,7 +39,7 @@ export function createPathEmitter(input) {
|
|
|
33
39
|
case "line": {
|
|
34
40
|
const start = item.from ?? getCurrentPosition();
|
|
35
41
|
const length = distance(start, item.to);
|
|
36
|
-
tl.end.range(length / speed).ease(item.ease).tween(start, item.to).apply(
|
|
42
|
+
tl.end.range(length / speed).ease(item.ease).tween(start, item.to).apply(v => lastXY = v);
|
|
37
43
|
getCurrentPosition = () => item.to;
|
|
38
44
|
break;
|
|
39
45
|
}
|
|
@@ -41,23 +47,27 @@ export function createPathEmitter(input) {
|
|
|
41
47
|
const start = item.from ?? getCurrentPosition();
|
|
42
48
|
const curve = createCurve(start, item.to, item.control1, item.control2);
|
|
43
49
|
const length = estimateLength(curve);
|
|
44
|
-
tl.end.range(length / speed).ease(item.ease).map(curve).apply(
|
|
50
|
+
tl.end.range(length / speed).ease(item.ease).map(curve).apply(v => lastXY = v);
|
|
45
51
|
getCurrentPosition = () => item.to;
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
});
|
|
49
|
-
return { listen, seek: t =>
|
|
55
|
+
return { listen, seek: t => {
|
|
56
|
+
tl.seek(t * tl.end.position);
|
|
57
|
+
emit(lastXY);
|
|
58
|
+
} };
|
|
50
59
|
}
|
|
51
|
-
function createCurve(
|
|
60
|
+
function createCurve([startX, startY], [endX, endY], [control1x, control1y], [control2x, control2y]) {
|
|
52
61
|
return (t) => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
3 *
|
|
56
|
-
t **
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
3 *
|
|
60
|
-
t **
|
|
62
|
+
const ti = 1 - t;
|
|
63
|
+
const x = ti ** 3 * startX +
|
|
64
|
+
3 * ti ** 2 * t * control1x +
|
|
65
|
+
3 * ti * t ** 2 * control2x +
|
|
66
|
+
t ** 3 * endX;
|
|
67
|
+
const y = ti ** 3 * startY +
|
|
68
|
+
3 * ti ** 2 * t * control1y +
|
|
69
|
+
3 * ti * t ** 2 * control2y +
|
|
70
|
+
t ** 3 * endY;
|
|
61
71
|
return [x, y];
|
|
62
72
|
};
|
|
63
73
|
}
|
package/internal/timeline.js
CHANGED
|
@@ -2,9 +2,43 @@ import { createListenable, RangeProgression } from "./emitters.js";
|
|
|
2
2
|
import { TimelinePoint } from "./point.js";
|
|
3
3
|
import { TimelineRange } from "./range.js";
|
|
4
4
|
import { clamp } from "./utils.js";
|
|
5
|
-
const
|
|
5
|
+
const default_interval_fps = 60;
|
|
6
6
|
const requestAnimFrame = globalThis?.requestAnimationFrame;
|
|
7
7
|
const cancelAnimFrame = globalThis?.cancelAnimationFrame;
|
|
8
|
+
const rafController = (() => {
|
|
9
|
+
const timelines = new Map();
|
|
10
|
+
let rafId = null;
|
|
11
|
+
const start = () => {
|
|
12
|
+
let previousTime = null;
|
|
13
|
+
const frame = (currentTime) => {
|
|
14
|
+
if (previousTime === null) {
|
|
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);
|
|
24
|
+
};
|
|
25
|
+
rafId = requestAnimFrame(frame);
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
add: (timeline, stepFn) => {
|
|
29
|
+
timelines.set(timeline, stepFn);
|
|
30
|
+
if (rafId === null)
|
|
31
|
+
start();
|
|
32
|
+
},
|
|
33
|
+
remove: (timeline) => {
|
|
34
|
+
timelines.delete(timeline);
|
|
35
|
+
if (timelines.size === 0) {
|
|
36
|
+
cancelAnimFrame(rafId);
|
|
37
|
+
rafId = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
8
42
|
const EndAction = {
|
|
9
43
|
pause: 0,
|
|
10
44
|
continue: 1,
|
|
@@ -350,7 +384,7 @@ export class Timeline {
|
|
|
350
384
|
this.playWithRAF();
|
|
351
385
|
return;
|
|
352
386
|
}
|
|
353
|
-
this.playWithInterval(arg ??
|
|
387
|
+
this.playWithInterval(arg ?? default_interval_fps);
|
|
354
388
|
}
|
|
355
389
|
playWithInterval(fps) {
|
|
356
390
|
let previousTime = performance.now();
|
|
@@ -364,21 +398,8 @@ export class Timeline {
|
|
|
364
398
|
this._pause = () => clearInterval(interval);
|
|
365
399
|
}
|
|
366
400
|
playWithRAF() {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const frame = (currentTime) => {
|
|
370
|
-
if (previousTime === null) {
|
|
371
|
-
previousTime = currentTime;
|
|
372
|
-
}
|
|
373
|
-
const elapsed = currentTime - previousTime;
|
|
374
|
-
previousTime = currentTime;
|
|
375
|
-
let delta = elapsed * this.timeScale;
|
|
376
|
-
this.next(delta);
|
|
377
|
-
if (this._pause)
|
|
378
|
-
rafId = requestAnimFrame(frame);
|
|
379
|
-
};
|
|
380
|
-
rafId = requestAnimFrame(frame);
|
|
381
|
-
this._pause = () => cancelAnimFrame(rafId);
|
|
401
|
+
rafController.add(this, n => this.next(n));
|
|
402
|
+
this._pause = () => rafController.remove(this);
|
|
382
403
|
}
|
|
383
404
|
next(delta) {
|
|
384
405
|
if (this._currentTime + delta <= this._endPosition) {
|