@xtia/timeline 1.1.17 → 1.1.18
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 +59 -7
- 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
|
@@ -137,7 +137,7 @@ Points represent specific times in the Timeline.
|
|
|
137
137
|
```ts
|
|
138
138
|
const twoSecondsIn = timeline.point(2000);
|
|
139
139
|
const fiveSecondsIn = firstFiveSeconds.end;
|
|
140
|
-
const sixSecondsIn =
|
|
140
|
+
const sixSecondsIn = fiveSecondsIn.delta(1000);
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
Points emit `PointEvent` objects when their position is reached or passed.
|
|
@@ -156,8 +156,8 @@ Directionality can also be leveraged with `point.applyDirectional()`:
|
|
|
156
156
|
|
|
157
157
|
```ts
|
|
158
158
|
twoSecondsIn.applyDirectional(
|
|
159
|
-
parent.append(element), // do
|
|
160
|
-
element.remove() // undo
|
|
159
|
+
() => parent.append(element), // do
|
|
160
|
+
() => element.remove() // undo
|
|
161
161
|
);
|
|
162
162
|
```
|
|
163
163
|
|
|
@@ -643,10 +643,6 @@ Returns a Promise that will be resolved when the range playthrough completes.
|
|
|
643
643
|
|
|
644
644
|
##### `grow(delta, anchor?): TimelineRange`
|
|
645
645
|
|
|
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
646
|
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
647
|
|
|
652
648
|
##### `subdivide(n): TimelineRange[]`
|
|
@@ -665,6 +661,62 @@ Returns true if the given [`TimelinePoint`](#timelinepoint-class) sits within th
|
|
|
665
661
|
|
|
666
662
|
Returns true if the given range overlaps with this range.
|
|
667
663
|
|
|
664
|
+
##### `path(steps: Path): Emitter<[number, number]>
|
|
665
|
+
|
|
666
|
+
Creates an emitter that follows a given path, emitting `[x, y]` (`XY`) as its parent range is progressed.
|
|
667
|
+
|
|
668
|
+
Path segments can be expressed as a mix of `[x, y]`, resolver functions (`progress => XY`) and Segment descriptor objects.
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
type LineSegment = {
|
|
672
|
+
type: "line";
|
|
673
|
+
from?: XY;
|
|
674
|
+
to: XY;
|
|
675
|
+
speed?: number;
|
|
676
|
+
ease?: Easer | keyof typeof easers;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
type CurveSegment = {
|
|
680
|
+
type: "curve";
|
|
681
|
+
from?: XY;
|
|
682
|
+
to: XY;
|
|
683
|
+
control1: XY;
|
|
684
|
+
control2: XY;
|
|
685
|
+
speed?: number;
|
|
686
|
+
ease?: Easer | keyof typeof easers;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
type CustomSegment = {
|
|
690
|
+
get: SegmentEvaluator;
|
|
691
|
+
length?: number;
|
|
692
|
+
ease?: Easer | keyof typeof easers;
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
* If the first element is `[x, y]`, it defines the path's starting position.
|
|
697
|
+
* If the first element is a non-custom descriptor object it must include a `from` property.
|
|
698
|
+
* Duration of segments defined as resolver functions, and custom segments without a `length` property, will be estimated by point sampling
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
// simple path with coordinates
|
|
702
|
+
const eg1 = range.path([[0, 0], [100, 50], [200, 0]])
|
|
703
|
+
|
|
704
|
+
// mixed path with curve segments
|
|
705
|
+
const eg2 = range.path([
|
|
706
|
+
[0, 0], // start position
|
|
707
|
+
{
|
|
708
|
+
type: 'curve',
|
|
709
|
+
to: [100, 100],
|
|
710
|
+
control1: [25, 0],
|
|
711
|
+
control2: [75, 100],
|
|
712
|
+
ease: easers.easeOut
|
|
713
|
+
},
|
|
714
|
+
[50, 50] // straight line to final position
|
|
715
|
+
]);
|
|
716
|
+
|
|
717
|
+
eg2.map(([x, y]) => [x + "%", y + "%"])
|
|
718
|
+
.apply(([left, top]) => element.style({ left, top }));
|
|
719
|
+
```
|
|
668
720
|
|
|
669
721
|
|
|
670
722
|
|
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) {
|