@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 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 = fiveSecondsdIn.delta(1000);
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(from: this, to: this): this`, by the progression value emitted by their parent.
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(from: this, to: this): this })
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
 
@@ -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.19",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"