@xtia/timeline 1.1.2 → 1.1.4

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
@@ -6,6 +6,7 @@ Timeline is a type-safe, seekable, deterministic choreography system that can co
6
6
 
7
7
  * [API Reference](#reference)
8
8
  * [Playground](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
9
+ * [Intro by HL](https://codepen.io/H-L-the-lessful/full/vELdyvB)
9
10
 
10
11
  ## Basic Use:
11
12
 
@@ -40,7 +41,7 @@ timeline
40
41
  timeline
41
42
  .range(1000, 2000)
42
43
  .tween(0, 255)
43
- .listen(value => microcontroller.setPWM(value))
44
+ .apply(value => microcontroller.setPWM(value))
44
45
 
45
46
  // make it go
46
47
  timeline.play();
@@ -113,13 +114,11 @@ const urlEmitter = filenameEmitter
113
114
 
114
115
  ```
115
116
 
116
- Range objects also provide a `play()` method that instructs the Timeline to play through that particular range:
117
+ Range objects also be passed to `Timeline`'s `play()` method to play through that particular range:
117
118
 
118
119
  ```ts
119
- // play through the first two seconds of the Timeline
120
- await timeline
121
- .range(0, 2000)
122
- .play();
120
+ // play through the first 5 seconds of the Timeline at 1000 units/s
121
+ await timeline.play(firstFiveSeconds);
123
122
  ```
124
123
 
125
124
  Custom easers can be passed to `ease()` as `(progress: number) => number`:
@@ -356,6 +355,27 @@ Despite the massive overhaul, the previous API is present and expanded and upgra
356
355
 
357
356
  ## Reference
358
357
 
358
+ ### Contents
359
+
360
+ #### Functions
361
+
362
+ * [`animate`](#animateduration-function)
363
+
364
+ #### Classes
365
+
366
+ * [`Timeline`](#timeline-class)
367
+ * [`TimelinePoint`](#timelinepoint-class)
368
+ * [`TimelineRange`](#timelinerange-class)
369
+ * [`RangeProgression`](#rangeprogression-class)
370
+ * [`Emitter<T>`](#emittert-class)
371
+
372
+ #### Interfaces
373
+
374
+ * [`PointEvent`](#pointevent-interface)
375
+ * [`ChainingInterface`](#chaininginterface-interface)
376
+
377
+
378
+
359
379
  ### `Timeline` class
360
380
 
361
381
  A self-contained collection of points and ranges that trigger events as the Timeline seeks to and through them.
@@ -374,17 +394,17 @@ Controls the speed at which a Timeline will progress when driven by the `play()`
374
394
 
375
395
  Returns true if the Timeline is actively being driven by the `play()` method (including by autoplay).
376
396
 
377
- ##### `end: `[`TimelinePoint`](#timelinepoint-interface)
397
+ ##### `end: `[`TimelinePoint`](#timelinepoint-class)
378
398
 
379
399
  Returns the **current** final point in the Timeline.
380
400
 
381
- ##### `start: `[`TimelinePoint`](#timelinepoint-interface)
401
+ ##### `start: `[`TimelinePoint`](#timelinepoint-class)
382
402
 
383
403
  Returns a point representing position 0.
384
404
 
385
405
  #### Methods
386
406
 
387
- ##### `point(position): `[`TimelinePoint`](#timelinepoint-interface)
407
+ ##### `point(position): `[`TimelinePoint`](#timelinepoint-class)
388
408
 
389
409
  Returns a point that represents a specific position on the Timeline.
390
410
 
@@ -392,7 +412,7 @@ If `position` is greater than that Timeline's end-position, the end-position wil
392
412
 
393
413
  *Note*, for deterministic consistency, points will be triggered if a forward-moving seek lands exactly on the point's position (or passes it entirely), while a backward-moving seek will trigger points that are passed or moved from.
394
414
 
395
- ##### `range(start, duration): `[`TimelineRange`](#timelinerange-interface)
415
+ ##### `range(start, duration): `[`TimelineRange`](#timelinerange-class)
396
416
 
397
417
  Returns a range that represents a section of the Timeline.
398
418
 
@@ -404,7 +424,9 @@ If `start` is omitted, the range will start at 0 and represent the full **curren
404
424
 
405
425
  ##### `seek(toPosition): void`
406
426
 
407
- Sets the Timeline's internal position (`currentTime`), triggering in chronological order listeners attached to any [`TimelinePoint`](#timelinepoint-interface) or [`TimelineRange`](#timelinerange-interface) that are passed or landed on.
427
+ Sets the Timeline's internal position (`currentTime`), triggering in chronological order listeners attached to any [`TimelinePoint`](#timelinepoint-class) or [`TimelineRange`](#timelinerange-class) that are passed or landed on.
428
+
429
+ `toPosition` may be a number or a [`TimelinePoint`](#timelinepoint-class).
408
430
 
409
431
  ##### `seek(toPosition, duration, easer?): Promise<void>`
410
432
 
@@ -422,9 +444,13 @@ Begins playing through the Timeline, from its current position, at (1000 × `tim
422
444
 
423
445
  Begins playing through the Timeline, from its current position, at (1000 × `timeScale`) units per second, updating `fps` times per second.
424
446
 
447
+ ##### `play(range, easer?): Promise<void>`
448
+
449
+ If a [`TimelineRange`](#timelinerange-class) is passed, the Timeline will play through that range at 1000 units per second, following the rules of a [smooth seek](#seektoposition-duration-easer-promisevoid).
450
+
425
451
  ##### `tween<T>(start, duration, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
426
452
 
427
- Creates a [`TimelineRange`](#timelinerange-interface) and attaches a tweening listener.
453
+ Creates a [`TimelineRange`](#timelinerange-class) and attaches a tweening listener.
428
454
 
429
455
  Equivalent to
430
456
 
@@ -440,22 +466,24 @@ Returns a [`ChainingInterface`](#chaininginterface-interface) representing the p
440
466
 
441
467
  ##### `tween<T>(start, end, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
442
468
 
443
- As above, but if the second argument is a [`TimelinePoint`](#timelinepoint-interface), it will specify when on the Timeline the tween will *end*.
469
+ As above, but if the second argument is a [`TimelinePoint`](#timelinepoint-class), it will specify when on the Timeline the tween will *end*.
444
470
 
445
471
  ##### `at(position, apply, reverse?): `[`ChainingInterface`](#chaininginterface-interface)
446
472
 
447
- Creates a [`TimelinePoint`](#timelinepoint-interface) and attaches a listener that will trigger when the Timeline seeks past or to that point.
473
+ Creates a [`TimelinePoint`](#timelinepoint-class) and attaches a listener that will trigger when the Timeline seeks past or to that point.
448
474
 
449
475
  If `reverse` is a function, that will be called instead of `apply` when the seek that triggered the event was moving backwards. If `reverse` is `true`, `apply` will be called regardless of which direction the seek moved. If `reverse` is false or omitted, this listener will ignore backward-moving seeks.
450
476
 
451
477
 
452
478
 
453
479
 
454
- ### `TimelinePoint` interface
480
+ ### `TimelinePoint` class
455
481
 
456
482
  Represents a single point on a [`Timeline`](#timeline-class).
457
483
 
458
- ##### Inherits [`Emitter<PointEvent>`](#emittert-interface)
484
+ This class is not meant to be constructed directly; instances are created with [`Timeline.point()`](#pointposition-timelinepoint).
485
+
486
+ ##### Inherits [`Emitter<PointEvent>`](#emittert-class)
459
487
 
460
488
  Listeners will be invoked with a [`PointEvent`](#pointevent-interface) when a seek passes or lands on the point.
461
489
 
@@ -471,11 +499,11 @@ This point's position on the Timeline.
471
499
 
472
500
  ##### `range(duration): TimelineRange`
473
501
 
474
- Creates a [`TimelineRange`](#timelinerange-interface) on the Timeline to which the point belongs, of the specified duration.
502
+ Creates a [`TimelineRange`](#timelinerange-class) on the Timeline to which the point belongs, of the specified duration.
475
503
 
476
504
  ##### `to(endPoint): TimelineRange`
477
505
 
478
- Creates a [`TimelineRange`](#timelinerange-interface) on the Timeline to which the point belongs, ending at the specified point.
506
+ Creates a [`TimelineRange`](#timelinerange-class) on the Timeline to which the point belongs, ending at the specified point.
479
507
 
480
508
  ##### `delta(timeOffset): TimelinePoint`
481
509
 
@@ -520,7 +548,7 @@ point
520
548
 
521
549
  ### `PointEvent` interface
522
550
 
523
- Provides information relevant to [`TimelinePoint`](#timelinepoint-interface) events.
551
+ Provides information relevant to [`TimelinePoint`](#timelinepoint-class) events.
524
552
 
525
553
  #### Properties
526
554
 
@@ -544,21 +572,23 @@ timeline
544
572
 
545
573
 
546
574
 
547
- ### `TimelineRange` interface
575
+ ### `TimelineRange` class
548
576
 
549
577
  Represents a fixed-length, fixed position section of a [`Timeline`](#timeline-class).
550
578
 
551
- ##### Inherits [`RangeProgression`](#rangeprogression-interface)
579
+ This class is not meant to be constructed directly; instances are created with [`Timeline.range()`](#rangestart-duration-timelinerange).
580
+
581
+ ##### Inherits [`RangeProgression`](#rangeprogression-class)
552
582
 
553
583
  Emits a normalised progression (0..1) of the range when the parent Timeline seeks over or into it.
554
584
 
555
585
  #### Properties
556
586
 
557
- ##### `start: `[`TimelinePoint`](#timelinepoint-interface)
587
+ ##### `start: `[`TimelinePoint`](#timelinepoint-class)
558
588
 
559
589
  The point on the Timeline at which this range starts.
560
590
 
561
- ##### `end: `[`TimelinePoint`](#timelinepoint-interface)
591
+ ##### `end: `[`TimelinePoint`](#timelinepoint-class)
562
592
 
563
593
  The point on the Timeline at which this range ends.
564
594
 
@@ -572,7 +602,7 @@ The length of the range.
572
602
 
573
603
  Creates two ranges representing two distinct sections of the parent. `position` is relative to the parent's start.
574
604
 
575
- ##### `spread(count): `[`TimelinePoint`](#timelinepoint-interface)[]
605
+ ##### `spread(count): `[`TimelinePoint`](#timelinepoint-class)[]
576
606
 
577
607
  Creates and returns `count` points spread evenly over the range.
578
608
 
@@ -600,7 +630,7 @@ Creates a new range by offsetting the parent by a given time delta.
600
630
 
601
631
  ##### `contains(point): boolean`
602
632
 
603
- Returns true if the given [`TimelinePoint`](#timelinepoint-interface) sits within this range.
633
+ Returns true if the given [`TimelinePoint`](#timelinepoint-class) sits within this range.
604
634
 
605
635
  ##### `overlaps(range): boolean`
606
636
 
@@ -609,11 +639,13 @@ Returns true if the given range overlaps with this range.
609
639
 
610
640
 
611
641
 
612
- ### `RangeProgression` interface
642
+ ### `RangeProgression` class
613
643
 
614
- Represents a step in an immutable [`TimelineRange`](#timelinerange-interface) event transformation pipeline.
644
+ Represents a step in an immutable [`TimelineRange`](#timelinerange-class) event transformation pipeline.
615
645
 
616
- ##### Inherits [`Emitter<number>`](#emittert-interface)
646
+ This class is not meant to be constructed directly; instances are created by various transformation methods of [`TimelineRange`](#timelinerange-class).
647
+
648
+ ##### Inherits [`Emitter<number>`](#emittert-class)
617
649
 
618
650
  Listeners will be invoked when a seek passes or lands within a range.
619
651
 
@@ -623,7 +655,7 @@ Listeners will be invoked when a seek passes or lands within a range.
623
655
 
624
656
  Creates an emitter that applies an easing function to parent emissions.
625
657
 
626
- ##### `tween<T>(from, to): `[`Emitter<T>`](#emittert-interface)
658
+ ##### `tween<T>(from, to): `[`Emitter<T>`](#emittert-class)
627
659
 
628
660
  Creates an emitter blends two values, biased by progression emitted by the parent.
629
661
 
@@ -637,7 +669,7 @@ blend(from: this, to: this, progress: number): this
637
669
 
638
670
  Creates an emitter that quantises progression emitted by the parent to the nearest of `steps` discrete values.
639
671
 
640
- ##### `sample<T>(values: ArrayLike<T>): `[`Emitter<T>`](#emittert-interface)
672
+ ##### `sample<T>(values: ArrayLike<T>): `[`Emitter<T>`](#emittert-class)
641
673
 
642
674
  Creates an emitter that emits values from an array according to progression.
643
675
 
@@ -705,7 +737,7 @@ range
705
737
 
706
738
 
707
739
 
708
- ### `Emitter<T>` interface
740
+ ### `Emitter<T>` class
709
741
 
710
742
  #### Methods
711
743
 
@@ -713,6 +745,8 @@ range
713
745
 
714
746
  Attaches a handler to the emitter and returns a function that will unsubscribe the handler.
715
747
 
748
+ This class is not meant to be constructed directly; instances are created by transformation methods.
749
+
716
750
  ##### `map<R>(mapFunc: (value: T) => R): Emitter<R>`
717
751
 
718
752
  Creates an emitter that performs an arbitrary transformation.
@@ -753,7 +787,7 @@ range
753
787
 
754
788
  ### `animate(duration)` function
755
789
 
756
- Creates and returns a [`TimelineRange`](#timelinerange-interface) that will automatically play over `duration` milliseconds.
790
+ Creates and returns a [`TimelineRange`](#timelinerange-class) that will automatically play over `duration` milliseconds.
757
791
 
758
792
  ### `ChainingInterface` interface
759
793
 
@@ -770,7 +804,7 @@ timeline
770
804
 
771
805
  #### Properties
772
806
 
773
- ##### `end: `[`TimelinePoint`](#timelinepoint-interface)
807
+ ##### `end: `[`TimelinePoint`](#timelinepoint-class)
774
808
 
775
809
  The point on the Timeline at which the effect of the previous chained call ends.
776
810
 
@@ -14,7 +14,7 @@ export declare class Emitter<T> {
14
14
  * @param listen
15
15
  * @returns {this}
16
16
  */
17
- protected redirect: (listen: ListenFunc<T>) => Emitter<T>;
17
+ protected redirect(listen: ListenFunc<T>): Emitter<T>;
18
18
  /**
19
19
  * Compatibility alias for `apply()` - registers a function to receive emitted values
20
20
  * @param handler
@@ -80,7 +80,7 @@ export declare class Emitter<T> {
80
80
  fork(cb: (branch: this) => void): this;
81
81
  }
82
82
  export declare class RangeProgression extends Emitter<number> {
83
- protected redirect: (listen: ListenFunc<number>) => RangeProgression;
83
+ protected redirect(listen: ListenFunc<number>): RangeProgression;
84
84
  /**
85
85
  * Creates a chainable progress emitter that applies an easing function to its parent's emitted values
86
86
  *
@@ -4,15 +4,17 @@ import { clamp } from "./utils";
4
4
  export class Emitter {
5
5
  constructor(onListen) {
6
6
  this.onListen = onListen;
7
- /**
8
- * Used by tap() to create a clone of an Emitter with a redirected onListen
9
- *
10
- * Should be overridden in all Emitter subclasses
11
- * @see {@link TimelineRange.redirect}
12
- * @param listen
13
- * @returns {this}
14
- */
15
- this.redirect = (listen) => new Emitter(listen);
7
+ }
8
+ /**
9
+ * Used by tap() to create a clone of an Emitter with a redirected onListen
10
+ *
11
+ * Should be overridden in all Emitter subclasses
12
+ * @see {@link TimelineRange.redirect}
13
+ * @param listen
14
+ * @returns {this}
15
+ */
16
+ redirect(listen) {
17
+ return new Emitter(listen);
16
18
  }
17
19
  /**
18
20
  * Compatibility alias for `apply()` - registers a function to receive emitted values
@@ -133,9 +135,8 @@ export class Emitter {
133
135
  }
134
136
  }
135
137
  export class RangeProgression extends Emitter {
136
- constructor() {
137
- super(...arguments);
138
- this.redirect = (listen) => new RangeProgression(listen);
138
+ redirect(listen) {
139
+ return new RangeProgression(listen);
139
140
  }
140
141
  ease(easer) {
141
142
  if (!easer)
@@ -17,7 +17,7 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
17
17
  * The point's absolute position on the Timeline
18
18
  */
19
19
  position: number);
20
- protected redirect: (listen: ListenFunc<PointEvent>) => TimelinePoint;
20
+ protected redirect(listen: ListenFunc<PointEvent>): TimelinePoint;
21
21
  /**
22
22
  * Creates a range on the Timeline, with a given duration, starting at this point
23
23
  * @param duration
@@ -38,19 +38,31 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
38
38
  delta(timeOffset: number): TimelinePoint;
39
39
  /**
40
40
  * Seeks the parent Timeline to this point
41
+ * @deprecated Use timeline.seek(point)
41
42
  */
42
43
  seek(): void;
44
+ /**
45
+ * Smooth-seeks the parent Timeline to this point
46
+ * @deprecated Use timeline.seek(point)
47
+ */
43
48
  seek(duration: number, easer?: Easer): Promise<void>;
44
49
  /**
45
50
  * Creates an emitter that only emits on forward-moving seeks
46
- * @returns
51
+ * @returns Listenable: emits forward-seeking point events
47
52
  */
48
53
  forwardOnly(): Emitter<PointEvent>;
49
54
  /**
50
55
  * Creates an emitter that only emits on backward-moving seeks
51
- * @returns
56
+ * @returns Listenable: emits backward-seeking point events
52
57
  */
53
58
  reverseOnly(): Emitter<PointEvent>;
59
+ filter(check: (event: PointEvent) => boolean): Emitter<PointEvent>;
60
+ /**
61
+ * Creates an emitter that forwards events emitted by seeks of a specific direction
62
+ * @param allow Direction to allow
63
+ * @returns Listenable: emits point events that match the given direction
64
+ */
65
+ filter(allow: -1 | 1): Emitter<PointEvent>;
54
66
  /**
55
67
  * Creates a Promise that will be resolved when the Timeline first seeks to/past this point
56
68
  *
@@ -79,4 +91,9 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
79
91
  * @returns A function to deregister both handlers
80
92
  */
81
93
  applyDirectional(apply: () => void, revert: () => void): UnsubscribeFunc;
94
+ /**
95
+ * Creates an emitter that forwards point events whose direction differs from the previous emission
96
+ * @returns Listenable: emits non-repeating point events
97
+ */
98
+ dedupe(): Emitter<PointEvent>;
82
99
  }
package/internal/point.js CHANGED
@@ -9,7 +9,9 @@ export class TimelinePoint extends Emitter {
9
9
  super(onListen);
10
10
  this.timeline = timeline;
11
11
  this.position = position;
12
- this.redirect = (listen) => new TimelinePoint(listen, this.timeline, this.position);
12
+ }
13
+ redirect(listen) {
14
+ return new TimelinePoint(listen, this.timeline, this.position);
13
15
  }
14
16
  /**
15
17
  * Creates a range on the Timeline, with a given duration, starting at this point
@@ -43,27 +45,28 @@ export class TimelinePoint extends Emitter {
43
45
  }
44
46
  /**
45
47
  * Creates an emitter that only emits on forward-moving seeks
46
- * @returns
48
+ * @returns Listenable: emits forward-seeking point events
47
49
  */
48
50
  forwardOnly() {
49
- return new Emitter(handler => {
50
- return this.onListen((ev) => {
51
- if (ev.direction > 0)
52
- handler(ev);
53
- });
54
- });
51
+ return this.filter(1);
55
52
  }
56
53
  /**
57
54
  * Creates an emitter that only emits on backward-moving seeks
58
- * @returns
55
+ * @returns Listenable: emits backward-seeking point events
59
56
  */
60
57
  reverseOnly() {
61
- return new Emitter(handler => {
62
- return this.onListen((ev) => {
63
- if (ev.direction < 0)
58
+ return this.filter(-1);
59
+ }
60
+ filter(arg) {
61
+ return new Emitter(typeof arg == "number"
62
+ ? handler => this.onListen(ev => {
63
+ if (ev.direction === arg)
64
64
  handler(ev);
65
- });
66
- });
65
+ })
66
+ : handler => this.onListen((ev) => {
67
+ if (arg(ev))
68
+ handler(ev);
69
+ }));
67
70
  }
68
71
  /**
69
72
  * Creates a Promise that will be resolved when the Timeline first seeks to/past this point
@@ -104,4 +107,17 @@ export class TimelinePoint extends Emitter {
104
107
  ? apply()
105
108
  : revert());
106
109
  }
110
+ /**
111
+ * Creates an emitter that forwards point events whose direction differs from the previous emission
112
+ * @returns Listenable: emits non-repeating point events
113
+ */
114
+ dedupe() {
115
+ let previous = 0;
116
+ return new Emitter(handler => this.onListen(event => {
117
+ if (event.direction !== previous) {
118
+ handler(event);
119
+ previous = event.direction;
120
+ }
121
+ }));
122
+ }
107
123
  }
@@ -16,7 +16,7 @@ export declare class TimelineRange extends RangeProgression {
16
16
  constructor(onListen: ListenFunc<number>, timeline: Timeline, startPosition: number,
17
17
  /** The duration of this range */
18
18
  duration: number);
19
- protected redirect: (listen: ListenFunc<number>) => TimelineRange;
19
+ protected redirect(listen: ListenFunc<number>): TimelineRange;
20
20
  /**
21
21
  * Creates two ranges by seperating one at a given point
22
22
  * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
@@ -48,6 +48,7 @@ export declare class TimelineRange extends RangeProgression {
48
48
  * Progresses the Timeline across the range at 1000 units per second
49
49
  * @param easer Optional easing function
50
50
  * @returns Promise, resolved when the end is reached
51
+ * @deprecated Use timeline.play(range, easer?)
51
52
  */
52
53
  play(easer?: Easer | keyof typeof easers): Promise<void>;
53
54
  /**
@@ -69,7 +70,7 @@ export declare class TimelineRange extends RangeProgression {
69
70
  * @param point The point to check
70
71
  * @returns true if the provided point is within the range
71
72
  */
72
- contains(point: TimelinePoint): boolean;
73
+ contains(point: TimelinePoint | number): boolean;
73
74
  /**
74
75
  * Checks if a range is fully within this range
75
76
  * @param range The range to check
package/internal/range.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { RangeProgression } from "./emitters";
2
2
  import { TimelinePoint } from "./point";
3
- import { clamp } from "./utils";
4
3
  export class TimelineRange extends RangeProgression {
5
4
  /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
6
5
  constructor(onListen, timeline, startPosition,
@@ -14,11 +13,13 @@ export class TimelineRange extends RangeProgression {
14
13
  this.timeline = timeline;
15
14
  this.startPosition = startPosition;
16
15
  this.duration = duration;
17
- this.redirect = (listen) => new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
18
16
  this.endPosition = startPosition + duration;
19
17
  this.end = timeline.point(this.endPosition);
20
18
  this.start = timeline.point(startPosition);
21
19
  }
20
+ redirect(listen) {
21
+ return new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
22
+ }
22
23
  /**
23
24
  * Creates two ranges by seperating one at a given point
24
25
  * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
@@ -28,8 +29,8 @@ export class TimelineRange extends RangeProgression {
28
29
  */
29
30
  bisect(position = this.duration / 2) {
30
31
  return [
31
- this.timeline.range(position, this.startPosition),
32
- this.timeline.range(position + this.startPosition, this.duration - this.startPosition),
32
+ this.timeline.range(this.startPosition, position),
33
+ this.timeline.range(position + this.startPosition, this.duration - position),
33
34
  ];
34
35
  }
35
36
  /**
@@ -65,11 +66,12 @@ export class TimelineRange extends RangeProgression {
65
66
  * Progresses the Timeline across the range at 1000 units per second
66
67
  * @param easer Optional easing function
67
68
  * @returns Promise, resolved when the end is reached
69
+ * @deprecated Use timeline.play(range, easer?)
68
70
  */
69
71
  play(easer) {
70
72
  this.timeline.pause();
71
73
  this.timeline.currentTime = this.startPosition;
72
- return this.timeline.seek(this.startPosition + this.duration, this.duration, easer);
74
+ return this.timeline.seek(this.end, this.duration, easer);
73
75
  }
74
76
  /**
75
77
  * Creates a new range representing a direct expansion of this one
@@ -78,16 +80,7 @@ export class TimelineRange extends RangeProgression {
78
80
  * @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
79
81
  */
80
82
  grow(delta, anchor = 0) {
81
- const clampedAnchor = clamp(anchor, 0, 1);
82
- const leftDelta = -delta * (1 - clampedAnchor);
83
- const rightDelta = delta * clampedAnchor;
84
- const newStart = this.startPosition + leftDelta;
85
- const newEnd = this.startPosition + this.duration + rightDelta;
86
- if (newEnd < newStart) {
87
- const mid = (newStart + newEnd) / 2;
88
- return this.timeline.range(mid, 0);
89
- }
90
- return this.timeline.range(newStart, newEnd - newStart);
83
+ return this.timeline.range(this.startPosition - (delta * anchor), this.duration + delta);
91
84
  }
92
85
  /**
93
86
  * Creates a new range representing a multiplicative expansion of this one
@@ -99,18 +92,12 @@ export class TimelineRange extends RangeProgression {
99
92
  if (factor <= 0) {
100
93
  throw new RangeError('Scale factor must be > 0');
101
94
  }
102
- const clampedAnchor = clamp(anchor, 0, 1);
103
- const oldLen = this.endPosition - this.startPosition;
104
- const pivot = this.startPosition + oldLen * clampedAnchor;
105
- const newStart = pivot - (pivot - this.startPosition) * factor;
106
- const newEnd = pivot + (this.endPosition - pivot) * factor;
107
- if (newEnd < newStart) {
108
- const mid = (newStart + newEnd) / 2;
109
- return this.timeline.range(mid, 0);
110
- }
111
- return this.timeline.range(newStart, newEnd - newStart);
95
+ return this.grow((factor - 1) * this.duration, anchor);
112
96
  }
113
97
  contains(target) {
98
+ if (typeof target == "number") {
99
+ return target >= this.startPosition && target < this.endPosition;
100
+ }
114
101
  const [targetStart, targetEnd] = target instanceof TimelinePoint
115
102
  ? [target.position, target.position]
116
103
  : [target.startPosition, target.startPosition + target.duration];
@@ -10,12 +10,16 @@ declare const EndAction: {
10
10
  readonly wrap: 2;
11
11
  readonly restart: 3;
12
12
  };
13
+ type Period = {
14
+ asMilliseconds: number;
15
+ };
13
16
  /**
14
17
  * Creates an autoplaying Timeline and returns a range from it
15
- * @param duration Animation duration, in milliseconds
18
+ * @param durationMs Animation duration, in milliseconds
16
19
  * @returns Object representing a range on a single-use, autoplaying Timeline
17
20
  */
18
- export declare function animate(duration: number): TimelineRange;
21
+ export declare function animate(durationMs: number): TimelineRange;
22
+ export declare function animate(period: Period): TimelineRange;
19
23
  export declare class Timeline {
20
24
  /**
21
25
  * Multiplies the speed at which `play()` progresses through the Timeline
@@ -89,12 +93,12 @@ export declare class Timeline {
89
93
  * Defines a range on this Timeline
90
94
  *
91
95
  * @param start The position on this Timeline at which the range starts
92
- * @param duration Length of the resulting range - if omitted, the range will end at the Timeline's **current** final position
96
+ * @param duration Length of the resulting range
93
97
  * @returns A range on the Timeline
94
98
  *
95
99
  * Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
96
100
  */
97
- range(start: number | TimelinePoint, duration?: number): TimelineRange;
101
+ range(start: number | TimelinePoint, duration: number): TimelineRange;
98
102
  /**
99
103
  * Creates an observable range from position 0 to the Timeline's **current** final position
100
104
  */
@@ -114,7 +118,8 @@ export declare class Timeline {
114
118
  * @param easer Optional easing function for the smooth-seek process
115
119
  * @returns A promise, resolved when the smooth-seek process finishes
116
120
  */
117
- seek(toPosition: number | TimelinePoint, duration: number, easer?: Easer | keyof typeof easers): Promise<void>;
121
+ seek(toPosition: number | TimelinePoint, durationMs: number, easer?: Easer | keyof typeof easers): Promise<void>;
122
+ seek(toPosition: number | TimelinePoint, duration: Period, easer?: Easer | keyof typeof easers): Promise<void>;
118
123
  private seekDirect;
119
124
  private seekPoints;
120
125
  private seekRanges;
@@ -124,6 +129,16 @@ export declare class Timeline {
124
129
  */
125
130
  play(): void;
126
131
  play(fps: number): void;
132
+ /**
133
+ * Performs a smooth-seek through a range at (1000 × this.timeScale) units per second
134
+ */
135
+ play(range: TimelineRange, easer?: Easer): Promise<void>;
136
+ /**
137
+ * Stops normal progression instigated by play()
138
+ *
139
+ * Does not affect ongoing smooth-seek operations or play(range)
140
+ *
141
+ */
127
142
  pause(): void;
128
143
  /**
129
144
  * Progresses the Timeline by 1 unit
@@ -148,7 +163,7 @@ export declare class Timeline {
148
163
  get position(): number;
149
164
  }
150
165
  export interface ChainingInterface {
151
- thenTween<T extends Tweenable>(duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer: Easer): ChainingInterface;
166
+ thenTween<T extends Tweenable>(duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer): ChainingInterface;
152
167
  then(action: () => void): ChainingInterface;
153
168
  thenWait(duration: number): ChainingInterface;
154
169
  fork(fn: (chain: ChainingInterface) => void): ChainingInterface;
@@ -9,13 +9,11 @@ const EndAction = {
9
9
  wrap: 2,
10
10
  restart: 3,
11
11
  };
12
- /**
13
- * Creates an autoplaying Timeline and returns a range from it
14
- * @param duration Animation duration, in milliseconds
15
- * @returns Object representing a range on a single-use, autoplaying Timeline
16
- */
17
- export function animate(duration) {
18
- return new Timeline(true).range(0, duration);
12
+ export function animate(durationMs) {
13
+ return new Timeline(true)
14
+ .range(0, typeof durationMs == "number"
15
+ ? durationMs
16
+ : durationMs.asMilliseconds);
19
17
  }
20
18
  export class Timeline {
21
19
  get currentTime() { return this._currentTime; }
@@ -132,7 +130,7 @@ export class Timeline {
132
130
  //if (endPosition > this._endPosition) this._endPosition = endPosition;
133
131
  // ^ leave this to range's point() calls
134
132
  const handlers = [];
135
- const range = {
133
+ const rangeData = {
136
134
  position: startPosition,
137
135
  duration,
138
136
  handlers,
@@ -141,22 +139,28 @@ export class Timeline {
141
139
  if (this.seeking)
142
140
  throw new Error("Can't add a listener while seeking");
143
141
  if (handlers.length == 0) {
144
- this.ranges.push(range);
142
+ this.ranges.push(rangeData);
145
143
  this.currentSortDirection = 0;
146
144
  }
147
145
  handlers.push(handler);
146
+ // if currentTime is in this range, apply immediately
147
+ if (range.contains(this._currentTime)) {
148
+ let progress = clamp((this._currentTime - startPosition) / duration, 0, 1);
149
+ handler(progress);
150
+ }
148
151
  return () => {
149
152
  const idx = handlers.indexOf(handler);
150
153
  if (idx === -1)
151
154
  throw new Error("Internal error: attempting to remove a non-present handler");
152
155
  handlers.splice(idx, 1);
153
156
  if (handlers.length == 0) {
154
- const idx = this.ranges.indexOf(range);
157
+ const idx = this.ranges.indexOf(rangeData);
155
158
  this.ranges.splice(idx, 1);
156
159
  }
157
160
  };
158
161
  };
159
- return new TimelineRange(addHandler, this, startPosition, duration);
162
+ const range = new TimelineRange(addHandler, this, startPosition, duration);
163
+ return range;
160
164
  }
161
165
  getWrappedPosition(n) {
162
166
  if (this.endAction.type !== EndAction.wrap)
@@ -175,26 +179,36 @@ export class Timeline {
175
179
  return loopStart + remainder;
176
180
  }
177
181
  seek(to, duration = 0, easer) {
182
+ const durationMs = typeof duration == "number"
183
+ ? duration
184
+ : duration.asMilliseconds;
178
185
  const toPosition = typeof to == "number"
179
186
  ? to
180
187
  : to.position;
181
188
  if (this.seeking) {
182
- throw new Error("Can't seek while seeking");
189
+ throw new Error("Can't seek while a seek event is processed");
183
190
  }
184
191
  if (this.smoothSeeker !== null) {
185
192
  this.smoothSeeker.pause();
186
- // ensure any awaits are resolved for the previous seek
187
- this.smoothSeeker.end.seek();
193
+ // ensure any awaits are resolved for the interrupted seek
194
+ const interruptPosition = this._currentTime;
195
+ this.smoothSeeker.seek(this.smoothSeeker.end);
188
196
  this.smoothSeeker = null;
197
+ // and jump back to where we were interrupted
198
+ this.seek(interruptPosition);
189
199
  }
190
- if (duration === 0) {
200
+ if (durationMs === 0) {
191
201
  this.seekDirect(toPosition);
192
202
  return Promise.resolve();
193
203
  }
194
204
  const seeker = new Timeline(true);
195
205
  this.smoothSeeker = seeker;
196
- seeker.range(0, duration).ease(easer).tween(this.currentTime, toPosition).apply(v => this.seekDirect(v));
197
- return new Promise(r => seeker.end.apply(() => r()));
206
+ seeker
207
+ .range(0, durationMs)
208
+ .ease(easer)
209
+ .tween(this.currentTime, toPosition)
210
+ .apply(v => this.seekDirect(v));
211
+ return seeker.end.promise();
198
212
  }
199
213
  seekDirect(toPosition) {
200
214
  const fromPosition = this._currentTime;
@@ -242,6 +256,8 @@ export class Timeline {
242
256
  });
243
257
  }
244
258
  seekRanges(to) {
259
+ if (this._currentTime === to)
260
+ return;
245
261
  const fromTime = Math.min(this._currentTime, to);
246
262
  const toTime = Math.max(this._currentTime, to);
247
263
  this.ranges.slice().forEach((range) => {
@@ -252,7 +268,7 @@ export class Timeline {
252
268
  range.handlers.slice().forEach(h => h(progress));
253
269
  }
254
270
  });
255
- this.progressionHandlers.slice().forEach(h => h(fromTime / this._endPosition));
271
+ this.progressionHandlers.slice().forEach(h => h(toTime / this._endPosition));
256
272
  }
257
273
  sortEntries(direction) {
258
274
  this.currentSortDirection = direction;
@@ -263,9 +279,18 @@ export class Timeline {
263
279
  ? sortTweens
264
280
  : sortReverse);
265
281
  }
266
- play(fps = default_fps) {
282
+ play(arg = default_fps, easer) {
267
283
  if (this.interval !== null)
268
284
  this.pause();
285
+ if (this.smoothSeeker) {
286
+ this.smoothSeeker.pause();
287
+ this.smoothSeeker.seek(this.smoothSeeker.end);
288
+ this.smoothSeeker = null;
289
+ }
290
+ if (arg instanceof TimelineRange) {
291
+ this.seek(arg.start);
292
+ return this.seek(arg.end, arg.duration / this.timeScale, easer);
293
+ }
269
294
  let previousTime = Date.now();
270
295
  this.interval = setInterval(() => {
271
296
  const newTime = Date.now();
@@ -276,7 +301,7 @@ export class Timeline {
276
301
  this.currentTime += delta;
277
302
  return;
278
303
  }
279
- // overshot; perform endAction
304
+ // overshot; perform restart/pause endAction
280
305
  if (this.endAction.type == EndAction.restart) {
281
306
  const loopRange = this.endAction.at.to(this._endPosition);
282
307
  const loopLen = loopRange.duration;
@@ -303,8 +328,14 @@ export class Timeline {
303
328
  return;
304
329
  }
305
330
  this.currentTime += delta;
306
- }, 1000 / fps);
331
+ }, 1000 / arg);
307
332
  }
333
+ /**
334
+ * Stops normal progression instigated by play()
335
+ *
336
+ * Does not affect ongoing smooth-seek operations or play(range)
337
+ *
338
+ */
308
339
  pause() {
309
340
  if (this.interval === null)
310
341
  return;
@@ -1,5 +1,5 @@
1
1
  /** @internal */
2
- export type Tweenable = number | number[] | string | string[] | Blendable | Blendable[];
2
+ export type Tweenable = number | number[] | string | string[] | Blendable | Date | Blendable[];
3
3
  /** @internal */
4
4
  export interface Blendable {
5
5
  blend(target: this, progress: number): this;
package/internal/tween.js CHANGED
@@ -18,7 +18,11 @@ export function createTween(from, to) {
18
18
  }
19
19
  switch (typeof from) {
20
20
  case "number": return progress => blendNumbers(from, to, progress);
21
- case "object": return progress => from.blend(to, progress);
21
+ case "object": {
22
+ if (from instanceof Date)
23
+ return progress => new Date(blendNumbers(from.getTime(), to.getTime(), progress));
24
+ return progress => from.blend(to, progress);
25
+ }
22
26
  case "string": return createStringTween(from, to);
23
27
  default: throw new Error("Invalid tweening type");
24
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"