@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 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 = fiveSecondsdIn.delta(1000);
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
 
@@ -69,7 +69,7 @@ export declare class Emitter<T> {
69
69
  * ```
70
70
  * @param cb
71
71
  */
72
- fork(cb: (branch: this) => void): this;
72
+ fork(...cb: ((branch: this) => void)[]): this;
73
73
  }
74
74
  export declare class RangeProgression extends Emitter<number> {
75
75
  /**
@@ -105,8 +105,8 @@ export class Emitter {
105
105
  * ```
106
106
  * @param cb
107
107
  */
108
- fork(cb) {
109
- cb(this);
108
+ fork(...cb) {
109
+ cb.forEach(cb => cb(this));
110
110
  return this;
111
111
  }
112
112
  }
@@ -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 (Array.isArray(item)) { // XY
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(emit);
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).map(v => item.get(v)).apply(emit);
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(emit);
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(emit);
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 => tl.seek(t * tl.end.position) };
55
+ return { listen, seek: t => {
56
+ tl.seek(t * tl.end.position);
57
+ emit(lastXY);
58
+ } };
50
59
  }
51
- function createCurve(start, end, control1, control2) {
60
+ function createCurve([startX, startY], [endX, endY], [control1x, control1y], [control2x, control2y]) {
52
61
  return (t) => {
53
- const x = (1 - t) ** 3 * start[0] +
54
- 3 * (1 - t) ** 2 * t * control1[0] +
55
- 3 * (1 - t) * t ** 2 * control2[0] +
56
- t ** 3 * end[0];
57
- const y = (1 - t) ** 3 * start[1] +
58
- 3 * (1 - t) ** 2 * t * control1[1] +
59
- 3 * (1 - t) * t ** 2 * control2[1] +
60
- t ** 3 * end[1];
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
  }
@@ -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 default_fps = 60;
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 ?? default_fps);
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
- let previousTime = null;
368
- let rafId;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"