@xtia/timeline 1.0.6 → 1.0.7

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
@@ -4,6 +4,7 @@
4
4
 
5
5
  **Timeline** is a general‑purpose, environment-agnostic choreography engine that lets you orchestrate any sequence of value changes; numbers, vectors, colour tokens, custom blendable objects, or arbitrary data structures.
6
6
 
7
+ * [API Reference](#reference)
7
8
 
8
9
  ## Basic Use:
9
10
 
@@ -33,8 +34,9 @@ timeline
33
34
  );
34
35
 
35
36
  // use an easing function
36
- typingRange
37
+ timeline
37
38
  .end
39
+ .delta(500)
38
40
  .range(3000)
39
41
  .ease("bounce")
40
42
  .tween("50%", "0%")
@@ -174,6 +176,7 @@ timeline
174
176
  timeline
175
177
  .range(0, 2000)
176
178
  .tween("--------", "########")
179
+ .dedupe()
177
180
  .listen(v => document.title = v);
178
181
  ```
179
182
 
@@ -260,10 +263,12 @@ loadingTimeline
260
263
  .tween("0%", "100%");
261
264
  .listen(v => progressBar.style.width = v);
262
265
 
266
+ // and do something when they're loaded
263
267
  loadingTimeline
264
268
  .end
265
269
  .listen(startGame);
266
270
 
271
+ // to drive it, just seek forward by 1 for each loaded resource
267
272
  resourceUrls.forEach(url => {
268
273
  preload(url).then(
269
274
  () => loadingTimeline.currentTime++
@@ -279,7 +284,7 @@ timeline.seek(timeline.end, 400, "overshootIn");
279
284
 
280
285
  ## Backward-compatibility
281
286
 
282
- Despite the massive overhaul, the previous API is present and expanded and upgrading to 1.0.0 should be frictionless in the vast majority of cases.
287
+ Despite the massive overhaul, the previous API is present and expanded and upgrading to 1.0.0 should be frictionless in the vast majority of cases.
283
288
 
284
289
  #### Breaking changes
285
290
 
@@ -288,7 +293,7 @@ Despite the massive overhaul, the previous API is present and expanded and upgr
288
293
  #### Mitigation
289
294
 
290
295
  * `timeline.tween()` now accepts TimelinePoint as a starting position, and provides an overload that replaces the `duration: number` parameter with `end: TimelinePoint`.
291
- * Should you encounter a case where this change still causes issue, eg `tl.tween(0, tl.end / 2, ...)`, `tl.end.position` is equivalent to the old API's `tl.end`.
296
+ * Should you encounter a case where this change still causes issue, eg `timeline.tween(0, timeline.end / 2, ...)`, `timeline.end.position` is equivalent to the old API's `timeline.end`.
292
297
 
293
298
  #### Enhancements (non-breaking)
294
299
 
@@ -299,4 +304,402 @@ Despite the massive overhaul, the previous API is present and expanded and upgr
299
304
 
300
305
  * `timeline.position` will be replaced with `timeline.currentTime` to be consistent with other seekable concepts.
301
306
  * `"loop"` endAction is now `"restart"` to disambiguate from new looping strategies.
302
- * `timeline.step()` is redundant now that `currentTime` is writable; use `timeline.currentTime += delta` instead.
307
+ * `timeline.step()` is redundant now that `currentTime` is writable; use `timeline.currentTime += delta` instead.
308
+
309
+
310
+
311
+ ## Reference
312
+
313
+ ### `Timeline` class
314
+
315
+ A self-contained collection of points and ranges that trigger events as the Timeline seeks to and through them.
316
+
317
+ #### Properties
318
+
319
+ ##### `currentTime: number`
320
+
321
+ Reads or sets the Timeline's current time position. Setting this property will perform a `seek()`, triggering any listener that is passed or landed on.
322
+
323
+ ##### `timeScale: number`
324
+
325
+ Controls the speed at which a Timeline will progress when driven by the `play()` method (including by autoplay).
326
+
327
+ ##### `isPlaying: boolean`
328
+
329
+ Returns true if the Timeline is actively being driven by the `play()` method (including by autoplay).
330
+
331
+ ##### `end: `[`TimelinePoint`](#timelinepoint-interface)
332
+
333
+ Returns the **current** final point in the Timeline.
334
+
335
+ ##### `start: `[`TimelinePoint`](#timelinepoint-interface)
336
+
337
+ Returns a point representing position 0.
338
+
339
+ #### Methods
340
+
341
+ ##### `point(position): `[`TimelinePoint`](#timelinepoint-interface)
342
+
343
+ Returns a point that represents a specific position on the Timeline.
344
+
345
+ If `position` is greater than that Timeline's end-position, the end-position will be extended to `position`.
346
+
347
+ *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.
348
+
349
+ ##### `range(start, duration): `[`TimelineRange`](#timelinerange-interface)
350
+
351
+ Returns a range that represents a section of the Timeline.
352
+
353
+ If the end of the range is beyond the Timeline's end-position, the end-position will be extended to the end of the range.
354
+
355
+ If `duration` is omitted, the range will extend from `start` to the **current** end-position of the Timeline.
356
+
357
+ If `start` is omitted, the range will start at 0 and represent the full **current** range of the Timeline.
358
+
359
+ ##### `seek(toPosition): void`
360
+
361
+ 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.
362
+
363
+ ##### `seek(toPosition, duration, easer?): Promise<void>`
364
+
365
+ Performs an interruptable 'smooth seek' to a specified position, lasting `duration` milliseconds, with optional easing.
366
+
367
+ Returns a Promise that will be resolved when the smooth seek is completed (or is interrupted by another seek\*).
368
+
369
+ \* Resolution on interruption is not finalised in the library's design and the effect should be considered exceptional; relying on it is not recommended. Future versions might reject the promise when its seek is interrupted.
370
+
371
+ ##### `play(): void`
372
+
373
+ Begins playing through the Timeline, from its current position, at (1000 x `timeScale`) units per second, updating 60 times per second.
374
+
375
+ ##### `play(fps): void`
376
+
377
+ Begins playing through the Timeline, from its current position, at (1000 x `timeScale`) units per second, updating `fps` times per second.
378
+
379
+ ##### `tween<T>(start, duration, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
380
+
381
+ Creates a [`TimelineRange`](#timelinerange-interface) and attaches a tweening listener.
382
+
383
+ Equivalent to
384
+
385
+ ```ts
386
+ timeline
387
+ .range(start, duration)
388
+ .ease(easer)
389
+ .tween(from, to)
390
+ .listen(apply);
391
+ ```
392
+
393
+ Returns a [`ChainingInterface`](#chaininginterface-interface) representing the point at which the tween ends.
394
+
395
+ ##### `tween<T>(start, end, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
396
+
397
+ As above, but if the second argument is a [`TimelinePoint`](#timelinepoint-interface), it will specify when on the Timeline the tween will *end*.
398
+
399
+ ##### `at(position, apply, reverse?): `[`ChainingInterface`](#chaininginterface-interface)
400
+
401
+ Creates a [`TimelinePoint`](#timelinepoint-interface) and attaches a listener that will trigger when the Timeline seeks past or to that point.
402
+
403
+ 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.
404
+
405
+
406
+
407
+
408
+ ### `TimelinePoint` interface
409
+
410
+ Represents a single point on a [`Timeline`](#timeline-class).
411
+
412
+ ##### Inherits [`Emitter<PointEvent>`](#emittert-interface)
413
+
414
+ Listeners will be invoked with a [`PointEvent`](#pointevent-interface) when a seek passes or lands on the point.
415
+
416
+ *Note*, during a point event, the parent Timeline's `currentTime` property will return that point's position, even if the Timeline is configured with a [*wrap* end action](#autoplay-and-looping-strategies) and its true position is beyond its end. For deterministic consistency, ranges will emit values for the point's position before the point emits.
417
+
418
+ #### Properties
419
+
420
+ ##### `position: number`
421
+
422
+ This point's position on the Timeline.
423
+
424
+ #### Methods
425
+
426
+ ##### `range(duration): TimelineRange`
427
+
428
+ Creates a [`TimelineRange`](#timelinerange-interface) on the Timeline to which the point belongs, of the specified duration.
429
+
430
+ ##### `to(endPoint): TimelineRange`
431
+
432
+ Creates a [`TimelineRange`](#timelinerange-interface) on the Timeline to which the point belongs, ending at the specified point.
433
+
434
+ ##### `delta(timeOffset): TimelinePoint`
435
+
436
+ Creates a `TimelinePoint` at an offset from the this point.
437
+
438
+
439
+
440
+
441
+ ### `PointEvent` interface
442
+
443
+ Provides information relevant to [`TimelinePoint`](#timelinepoint-interface) events.
444
+
445
+ #### Properties
446
+
447
+ ##### `direction: -1 | 1`
448
+
449
+ Provides the direction of the seek that triggered a point event. `direction === 1` indicates that the seek moved forward and `direction === -1` indicates that the seek was moving backwards.
450
+
451
+ Allows point listeners to undo effects when the Timeline is reversed.
452
+
453
+ ```ts
454
+ timeline
455
+ .point(4000)
456
+ .listen(
457
+ event => element.classList.toggle(
458
+ "visible",
459
+ event.direction > 0
460
+ )
461
+ );
462
+ ```
463
+
464
+
465
+
466
+
467
+ ### `TimelineRange` interface
468
+
469
+ Represents a fixed-length, fixed position section of a [`Timeline`](#timeline-class).
470
+
471
+ ##### Inherits [`RangeProgression`](#rangeprogression-interface)
472
+
473
+ Emits a normalised progression (0..1) of the range when the parent Timeline seeks over or into it.
474
+
475
+ #### Properties
476
+
477
+ ##### `start: `[`TimelinePoint`](#timelinepoint-interface)
478
+
479
+ The point on the Timeline at which this range starts.
480
+
481
+ ##### `end: `[`TimelinePoint`](#timelinepoint-interface)
482
+
483
+ The point on the Timeline at which this range ends.
484
+
485
+ ##### `duration: number`
486
+
487
+ The length of the range.
488
+
489
+ #### Methods
490
+
491
+ ##### `bisect(position?): [TimelineRange, TimelineRange]`
492
+
493
+ Creates two ranges representing two distinct sections of the parent. `position` is relative to the parent's start.
494
+
495
+ ##### `spread(count): `[`TimelinePoint`](#timelinepoint-interface)[]
496
+
497
+ Creates and returns `count` points spread evenly over the range.
498
+
499
+ ##### `play(easer?): Promise<void>`
500
+
501
+ Instructs the Timeline to which this range belongs to play through the represented range. This playthrough counts as a smooth seek for seek interruption purposes.
502
+
503
+ Returns a Promise that will be resolved when the range playthrough completes.
504
+
505
+ ##### `grow(delta, anchor?): TimelineRange`
506
+
507
+ 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.
508
+
509
+ ##### `grow(delta, anchor?): TimelineRange`
510
+
511
+ 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.
512
+
513
+ ##### `contains(point)`
514
+
515
+ Returns true if the given [`TimelinePoint`](#timelinepoint-interface) sits within this range.
516
+
517
+
518
+
519
+
520
+ ### `RangeProgression` interface
521
+
522
+ Represents a step in an immutable [`TimelineRange`](#timelinerange-interface) event transformation pipeline.
523
+
524
+ ##### Inherits [`Emitter<number>`](#emittert-interface)
525
+
526
+ Listeners will be invoked when a seek passes or lands within a range.
527
+
528
+ #### Methods
529
+
530
+ ##### `ease(easer?): RangeProgression`
531
+
532
+ Creates an emitter that applies an easing function to parent emissions.
533
+
534
+ ##### `tween<T>(from, to): `[`Emitter<T>`](#emittert-interface)
535
+
536
+ Creates an emitter blends two values, biased by progression emitted by the parent.
537
+
538
+ `T` may be `string`, `number`, `number[]` or an object type that includes
539
+
540
+ ```ts
541
+ blend(from: this, to: this, progress: number): this
542
+ ```
543
+
544
+ ##### `snap(steps): RangeProgression`
545
+
546
+ Creates an emitter that quantises progression emitted by the parent to the nearest of `steps` discrete values.
547
+
548
+ ##### `threshold(threshold): RangeProgression`
549
+
550
+ Creates an emitter that emits 0 when the parent emits a value below `threshold` and 1 when a parent emission is equal to or greater than `threshold`.
551
+
552
+ ```ts
553
+ emittedValue = parentEmission < threshold ? 0 : 1
554
+ ```
555
+
556
+ ##### `clamp(min?, max?): RangeProgression`
557
+
558
+ Creates an emitter that clamps progression between `min` and `max`.
559
+
560
+ ##### `repeat(count): RangeProgression`
561
+
562
+ Creates an emitter that multiplies progression and wraps at 1, thereby mapping to a repeating scale.
563
+
564
+ ##### `tap(cb): RangeProgression`
565
+
566
+ Creates an emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
567
+
568
+ ##### `filter(check: (value) => boolean): RangeProgression`
569
+
570
+ Creates an emitter that selectively discards parent emissions.
571
+
572
+ If `check(value)` returns true, the value will be emitted.
573
+
574
+ ##### `dedupe(): RangeProgression`
575
+
576
+ Creates an emitter that discards emitted values that are the same as the last value emitted by the new emitter
577
+
578
+ ##### `offset(delta): RangeProgression`
579
+
580
+ Creates an emitter that offsets its parent's values by the given delta, wrapping at 1
581
+
582
+ ##### `fork(cb: (branch) => void): RangeProgression`
583
+
584
+ Immediately invokes `cb` with this emitter and returns this emitter for further chaining.
585
+
586
+ Allows branching without breaking a composition chain, eg:
587
+
588
+ ```ts
589
+ range
590
+ .tween("0%", "100%")
591
+ .fork(branch => {
592
+ branch
593
+ .map(s => `Loading: ${s}`)
594
+ .listen(s => document.title = s)
595
+ })
596
+ .listen(v => progressBar.style.width = v);
597
+ ```
598
+
599
+
600
+
601
+
602
+ ### `Emitter<T>` interface
603
+
604
+ #### Methods
605
+
606
+ ##### `listen(handler: Handler<T>): UnsubscribeFunc`
607
+
608
+ Attaches a handler to the emitter and returns a function that will unsubscribe the handler.
609
+
610
+ ##### `map<R>(mapFunc: (value: T) => R): Emitter<R>`
611
+
612
+ Creates an emitter that performs an arbitrary transformation.
613
+
614
+ ##### `filter(check: (value: T) => boolean): Emitter<T>`
615
+
616
+ Creates an emitter that selectively discards parent emissions.
617
+
618
+ If `check(value)` returns true, the value will be emitted.
619
+
620
+ ##### `dedupe(compare?: (a: T, b: T) => boolean): Emitter<T>`
621
+
622
+ Creates an emitter that discards emitted values that are the same as the last value emitted by the new emitter
623
+
624
+ ##### `tap(cb: Handler<T>): Emitter<T>`
625
+
626
+ Creates an emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
627
+
628
+ ##### `fork(cb: (branch: Emitter<T>) => void): Emitter<T>`
629
+
630
+ Immediately invokes `cb` with this emitter and returns this emitter for further chaining.
631
+
632
+ Allows branching without breaking a composition chain, eg:
633
+
634
+ ```ts
635
+ range
636
+ .tween("0%", "100%")
637
+ .fork(branch => {
638
+ branch
639
+ .map(s => `Loading: ${s}`)
640
+ .listen(s => document.title = s)
641
+ })
642
+ .listen(v => progressBar.style.width = v);
643
+ ```
644
+
645
+
646
+
647
+
648
+ ### `animate(duration)` function
649
+
650
+ Creates and returns a [`TimelineRange`](#timelinerange-interface) that will automatically play over `duration` milliseconds.
651
+
652
+ ### `ChainingInterface` interface
653
+
654
+ Conveys composable sequential tweens and events with the simplified API. Each instance represents a specific point on the parent Timeline.
655
+
656
+ ```ts
657
+ timeline
658
+ .tween(0, 1000, doThing, 0, 100)
659
+ .thenWait(500)
660
+ .then(doOtherThing)
661
+ .thenWait(250)
662
+ .thenTween(2000, dothing, 100, 0);
663
+ ```
664
+
665
+ #### Properties
666
+
667
+ ##### `end: `[`TimelinePoint`](#timelinepoint-interface)
668
+
669
+ The point on the Timeline at which the effect of the previous chained call ends.
670
+
671
+ #### Methods
672
+
673
+ ##### `thenTween(duration, apply, from, to, easer): ChainingInterface`
674
+
675
+ Adds a tween, beginning at the point the interface represents. Returns a new `ChainingInterface` representing the end of the new tween.
676
+
677
+ ##### `then(action: () => void): ChainingInterface`
678
+
679
+ Adds a point event at the point the interface represents.
680
+
681
+ ##### `thenWait(duration): ChainingInterface`
682
+
683
+ Creates a new `ChainingInterface` by offsetting the parent by `duration`.
684
+
685
+
686
+
687
+ ### `easers` const
688
+
689
+ The following easers are provided:
690
+
691
+ `linear`, `easeIn`, `easeIn4`, `easeOut`, `easeOut4`, `circleIn`, `circleIn4`, `circleOut`, `circleOut4`, `easeInOut`, `elastic`, `overshootIn`, `sine`, `invert`, `bounce`, `noise`, `pingpong`
692
+
693
+ Methods that accept an easing function accept both `(progress: number) => number` and any of the names above.
694
+
695
+ ```ts
696
+ timeline
697
+ .tween(s, e, a, f, t, v => Math.sqrt(v))
698
+ .thenTween(s, e, a, f, t, c, "elastic");
699
+
700
+ timeline
701
+ .range(0, 1000)
702
+ .ease("circleOut")
703
+ .ease(easers.easeIn)
704
+ // ...
705
+ ```
package/index.js CHANGED
@@ -1,8 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.easers = exports.animate = exports.Timeline = void 0;
3
+ exports.easers = exports.RangeProgression = exports.Emitter = exports.TimelineRange = exports.TimelinePoint = exports.animate = exports.Timeline = void 0;
4
4
  var timeline_1 = require("./internal/timeline");
5
5
  Object.defineProperty(exports, "Timeline", { enumerable: true, get: function () { return timeline_1.Timeline; } });
6
6
  Object.defineProperty(exports, "animate", { enumerable: true, get: function () { return timeline_1.animate; } });
7
+ var point_1 = require("./internal/point");
8
+ Object.defineProperty(exports, "TimelinePoint", { enumerable: true, get: function () { return point_1.TimelinePoint; } });
9
+ var range_1 = require("./internal/range");
10
+ Object.defineProperty(exports, "TimelineRange", { enumerable: true, get: function () { return range_1.TimelineRange; } });
11
+ var emitters_1 = require("./internal/emitters");
12
+ Object.defineProperty(exports, "Emitter", { enumerable: true, get: function () { return emitters_1.Emitter; } });
13
+ Object.defineProperty(exports, "RangeProgression", { enumerable: true, get: function () { return emitters_1.RangeProgression; } });
7
14
  var easing_1 = require("./internal/easing");
8
15
  Object.defineProperty(exports, "easers", { enumerable: true, get: function () { return easing_1.easers; } });
@@ -1,18 +1,19 @@
1
1
  import { Easer, easers } from "./easing";
2
- import { Blendable } from "./tween";
3
- import { OptionalIfKeyIn } from "./utils";
4
- /** @internal */
5
- export declare function createEmitter<T>(listen: ListenFunc<T>): Emitter<T>;
6
- /** @internal */
7
- export declare function createEmitter<T, API extends object>(onListen: ListenFunc<T>, api: OptionalIfKeyIn<API, Emitter<T>>): Emitter<T> & API;
8
- /** @internal */
9
- export declare function createProgressEmitter<API extends object>(listen: ListenFunc<number>, api: Omit<API, keyof RangeProgression>): RangeProgression & API;
10
- /** @internal */
11
- export declare function createProgressEmitter(listen: ListenFunc<number>): RangeProgression;
2
+ import { Tweenable } from "./tween";
12
3
  type Handler<T> = (value: T) => void;
13
- type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
4
+ export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
14
5
  export type UnsubscribeFunc = () => void;
15
- export interface Emitter<T> {
6
+ export declare class Emitter<T> {
7
+ protected onListen: ListenFunc<T>;
8
+ protected constructor(onListen: ListenFunc<T>);
9
+ /**
10
+ * Used by tap() to create a clone of an Emitter with a redirected onListen
11
+ * Should be overridden in all Emitter subclasses
12
+ * @see {@link TimelineRange.redirect}
13
+ * @param listen
14
+ * @returns {this}
15
+ */
16
+ protected redirect: (listen: ListenFunc<T>) => Emitter<T>;
16
17
  /**
17
18
  * Registers a function to receive emitted values
18
19
  * @param handler
@@ -50,7 +51,7 @@ export interface Emitter<T> {
50
51
  * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
51
52
  * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
52
53
  */
53
- tap(cb: Handler<T>): Emitter<T>;
54
+ tap(cb: Handler<T>): this;
54
55
  /**
55
56
  * Immediately passes this emitter to a callback and returns this emitter
56
57
  *
@@ -69,9 +70,10 @@ export interface Emitter<T> {
69
70
  * ```
70
71
  * @param cb
71
72
  */
72
- fork(cb: (branch: Emitter<T>) => void): Emitter<T>;
73
+ fork(cb: (branch: this) => void): this;
73
74
  }
74
- export interface RangeProgression extends Emitter<number> {
75
+ export declare class RangeProgression extends Emitter<number> {
76
+ protected redirect: (listen: ListenFunc<number>) => RangeProgression;
75
77
  /**
76
78
  * Creates a chainable progress emitter that applies an easing function to its parent's emitted values
77
79
  *
@@ -79,6 +81,7 @@ export interface RangeProgression extends Emitter<number> {
79
81
  * @returns Listenable: emits eased progression values
80
82
  */
81
83
  ease(easer?: Easer | keyof typeof easers): RangeProgression;
84
+ ease(easer?: undefined): RangeProgression;
82
85
  /**
83
86
  * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
84
87
  *
@@ -119,7 +122,7 @@ export interface RangeProgression extends Emitter<number> {
119
122
  * @param to Value to interpolate to
120
123
  * @returns Listenable: emits interpolated values
121
124
  */
122
- tween<T extends Blendable | number[]>(from: T, to: T): Emitter<T>;
125
+ tween<T extends Tweenable>(from: T, to: T): Emitter<T>;
123
126
  /**
124
127
  * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
125
128
  *
@@ -161,18 +164,6 @@ export interface RangeProgression extends Emitter<number> {
161
164
  * @returns Listenable: emits scaled and repeating values
162
165
  */
163
166
  repeat(count: number): RangeProgression;
164
- /**
165
- * Creates a chainable progress emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
166
- *
167
- * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
168
- * All listeners attached to the returned emitter receive the same values as the parent emitter.
169
- *
170
- * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
171
- *
172
- * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
173
- * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
174
- */
175
- tap(cb: (value: number) => void): RangeProgression;
176
167
  /**
177
168
  * Creates a chainable progress emitter that selectively forwards emissions along the chain
178
169
  * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
@@ -202,6 +193,5 @@ export interface RangeProgression extends Emitter<number> {
202
193
  * @returns Listenable: emits offset values
203
194
  */
204
195
  offset(delta: number): RangeProgression;
205
- fork(cb: (branch: RangeProgression) => void): RangeProgression;
206
196
  }
207
197
  export {};
@@ -1,108 +1,254 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createEmitter = createEmitter;
4
- exports.createProgressEmitter = createProgressEmitter;
3
+ exports.RangeProgression = exports.Emitter = void 0;
5
4
  const easing_1 = require("./easing");
6
5
  const tween_1 = require("./tween");
7
6
  const utils_1 = require("./utils");
8
- /** @internal */
9
- function createEmitter(listen, api) {
10
- const methods = {
11
- listen: (handler) => listen((value) => {
7
+ class Emitter {
8
+ constructor(onListen) {
9
+ this.onListen = onListen;
10
+ /**
11
+ * Used by tap() to create a clone of an Emitter with a redirected onListen
12
+ * Should be overridden in all Emitter subclasses
13
+ * @see {@link TimelineRange.redirect}
14
+ * @param listen
15
+ * @returns {this}
16
+ */
17
+ this.redirect = (listen) => new Emitter(listen);
18
+ }
19
+ /**
20
+ * Registers a function to receive emitted values
21
+ * @param handler
22
+ * @returns A function to deregister the handler
23
+ */
24
+ listen(handler) {
25
+ return this.onListen((value) => {
12
26
  handler(value);
13
- }),
14
- map: (mapFunc) => createEmitter(handler => listen((value) => {
27
+ });
28
+ }
29
+ /**
30
+ * Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
31
+ * @param mapFunc
32
+ * @returns Listenable: emits transformed values
33
+ */
34
+ map(mapFunc) {
35
+ return new Emitter(handler => this.onListen((value) => {
15
36
  handler(mapFunc(value));
16
- })),
17
- filter: (filterFunc) => createEmitter(handler => listen((value) => {
18
- if (filterFunc(value))
37
+ }));
38
+ }
39
+ /**
40
+ * Creates a chainable emitter that selectively forwards emissions along the chain
41
+ * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
42
+ * @returns Listenable: emits values that pass the filter
43
+ */
44
+ filter(check) {
45
+ return new Emitter(handler => this.onListen((value) => {
46
+ if (check(value))
19
47
  handler(value);
20
- })),
21
- dedupe: (compare) => {
22
- let previous = null;
23
- return createEmitter(handler => {
24
- const filteredHandler = (value) => {
25
- if (!previous || (compare
26
- ? !compare(previous.value, value)
27
- : (previous.value !== value))) {
28
- handler(value);
29
- previous = { value };
30
- }
31
- };
32
- return listen(filteredHandler);
33
- }, api ?? {});
34
- },
35
- tap: (cb) => createTap((createEmitter), listen, cb),
36
- fork: (cb) => {
37
- cb(emitter);
38
- return emitter;
39
- }
40
- };
41
- const emitter = (0, utils_1.prototypify)(methods, api ?? {});
42
- return emitter;
43
- }
44
- /** @internal */
45
- function createProgressEmitter(listen, api) {
46
- const methods = {
47
- ease: (easer) => {
48
- const easerFunc = typeof easer == "string"
49
- ? easing_1.easers[easer]
50
- : easer;
51
- return createProgressEmitter(easer ? (handler => listen((progress) => {
52
- handler(easerFunc(progress));
53
- })) : listen);
54
- },
55
- tween: (from, to) => createEmitter(handler => listen(progress => handler((0, tween_1.tweenValue)(from, to, progress)))),
56
- snap: (steps) => {
57
- if (!Number.isInteger(steps) || steps <= 0) {
58
- throw new RangeError('snap(steps) requires a positive integer');
48
+ }));
49
+ }
50
+ /**
51
+ * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
52
+ * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
53
+ *
54
+ * If no `compare` function is provided, values will be compared via `===`
55
+ * @returns Listenable: emits non-repeating values
56
+ */
57
+ dedupe(compare) {
58
+ let previous = null;
59
+ return new Emitter(handler => {
60
+ const filteredHandler = (value) => {
61
+ if (!previous || (compare
62
+ ? !compare(previous.value, value)
63
+ : (previous.value !== value))) {
64
+ handler(value);
65
+ previous = { value };
66
+ }
67
+ };
68
+ return this.onListen(filteredHandler);
69
+ });
70
+ }
71
+ /**
72
+ * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
73
+ *
74
+ * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
75
+ * All listeners attached to the returned emitter receive the same values as the parent emitter.
76
+ *
77
+ * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
78
+ *
79
+ * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
80
+ * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
81
+ */
82
+ tap(cb) {
83
+ const listeners = [];
84
+ let parentUnsubscribe = null;
85
+ const tappedListen = (handler) => {
86
+ listeners.push(handler);
87
+ if (listeners.length === 1) {
88
+ parentUnsubscribe = this.onListen(value => {
89
+ cb(value);
90
+ listeners.slice().forEach(fn => fn(value));
91
+ });
59
92
  }
60
- return createProgressEmitter(handler => listen(progress => {
61
- const snapped = Math.round(progress * steps) / steps;
62
- handler((0, utils_1.clamp)(snapped, 0, 1));
63
- }));
64
- },
65
- threshold: (threshold) => createProgressEmitter(handler => listen(progress => {
93
+ return () => {
94
+ const idx = listeners.indexOf(handler);
95
+ listeners.splice(idx, 1);
96
+ if (listeners.length === 0 && parentUnsubscribe) {
97
+ parentUnsubscribe();
98
+ parentUnsubscribe = null;
99
+ }
100
+ };
101
+ };
102
+ return this.redirect(tappedListen);
103
+ }
104
+ /**
105
+ * Immediately passes this emitter to a callback and returns this emitter
106
+ *
107
+ * Allows branching without breaking a composition chain
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * range
112
+ * .tween("0%", "100%")
113
+ * .fork(branch => {
114
+ * branch
115
+ * .map(s => `Loading: ${s}`)
116
+ * .listen(s => document.title = s)
117
+ * })
118
+ * .listen(v => progressBar.style.width = v);
119
+ * ```
120
+ * @param cb
121
+ */
122
+ fork(cb) {
123
+ cb(this);
124
+ return this;
125
+ }
126
+ }
127
+ exports.Emitter = Emitter;
128
+ class RangeProgression extends Emitter {
129
+ constructor() {
130
+ super(...arguments);
131
+ this.redirect = (listen) => new RangeProgression(listen);
132
+ }
133
+ ease(easer) {
134
+ if (!easer)
135
+ return this;
136
+ const easerFunc = typeof easer == "string"
137
+ ? easing_1.easers[easer]
138
+ : easer;
139
+ return new RangeProgression(easer ? (handler => this.onListen((progress) => {
140
+ handler(easerFunc(progress));
141
+ })) : h => this.onListen(h));
142
+ }
143
+ tween(from, to) {
144
+ return new Emitter(handler => this.onListen(progress => handler((0, tween_1.tweenValue)(from, to, progress))));
145
+ }
146
+ /**
147
+ * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
148
+ *
149
+ * @param steps – positive integer (e.g. 10 → 0, .1, .2 … 1)
150
+ * @throws RangeError if steps is not a positive integer
151
+ * @returns Listenable: emits quantised progression values
152
+ */
153
+ snap(steps) {
154
+ if (!Number.isInteger(steps) || steps <= 0) {
155
+ throw new RangeError('snap(steps) requires a positive integer');
156
+ }
157
+ return new RangeProgression(handler => this.onListen(progress => {
158
+ const snapped = Math.round(progress * steps) / steps;
159
+ handler((0, utils_1.clamp)(snapped, 0, 1));
160
+ }));
161
+ }
162
+ /**
163
+ * Creates a chainable progress emitter that emits `1` when the incoming progress value is greater‑than‑or‑equal to the supplied `threshold`, otherwise emits `0`
164
+ *
165
+ * @param threshold the cut‑off value
166
+ * @returns Listenable: emits 0 or 1 after comparing progress with a threshold
167
+ */
168
+ threshold(threshold) {
169
+ return new RangeProgression(handler => this.onListen(progress => {
66
170
  handler(progress >= threshold ? 1 : 0);
67
- })),
68
- clamp: (min = 0, max = 1) => createProgressEmitter(handler => listen(progress => handler((0, utils_1.clamp)(progress, min, max)))),
69
- repeat: (repetitions) => {
70
- repetitions = Math.max(0, repetitions);
71
- return createProgressEmitter(handler => listen(progress => {
72
- const out = (progress * repetitions) % 1;
73
- handler(out);
74
- }));
75
- },
76
- tap: (cb) => createTap(createProgressEmitter, listen, cb),
77
- filter: (filterFunc) => createProgressEmitter(handler => listen((value) => {
78
- if (filterFunc(value))
171
+ }));
172
+ }
173
+ /**
174
+ * Creates a chainable progress emitter that clamps incoming values
175
+ * @param min default 0
176
+ * @param max default 1
177
+ * @returns Listenable: emits clamped progression values
178
+ */
179
+ clamp(min = 0, max = 1) {
180
+ return new RangeProgression(handler => this.onListen(progress => handler((0, utils_1.clamp)(progress, min, max))));
181
+ }
182
+ /**
183
+ * Creates a chainable progress emitter that maps incoming values to a repeating linear scale
184
+ *
185
+ * ```plain
186
+ * count=2
187
+ *‎ 1
188
+ *‎ | / /
189
+ *‎ o| / /
190
+ *‎ u| / /
191
+ *‎ t| / /
192
+ *‎ | / /
193
+ *‎ |/_____/_____
194
+ *‎ 0 in 1
195
+ * ```
196
+ *
197
+ * @param count Number of repetitions
198
+ * @returns Listenable: emits scaled and repeating values
199
+ */
200
+ repeat(count) {
201
+ count = Math.max(0, count);
202
+ return new RangeProgression(handler => this.onListen(progress => {
203
+ const out = (progress * count) % 1;
204
+ handler(out);
205
+ }));
206
+ }
207
+ /**
208
+ * Creates a chainable progress emitter that selectively forwards emissions along the chain
209
+ * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
210
+ * @returns Listenable: emits values that pass the filter
211
+ */
212
+ filter(check) {
213
+ return new RangeProgression(handler => this.onListen((value) => {
214
+ if (check(value))
79
215
  handler(value);
80
- })),
81
- offset: (delta) => createProgressEmitter(handler => listen(value => handler((value + delta) % 1))),
82
- };
83
- const baseEmitter = createEmitter(listen, methods);
84
- const emitter = (0, utils_1.prototypify)(baseEmitter, api ?? {});
85
- return emitter;
86
- }
87
- function createTap(create, parentListen, cb) {
88
- const listeners = [];
89
- let parentUnsubscribe = null;
90
- const tappedListen = (handler) => {
91
- listeners.push(handler);
92
- if (listeners.length === 1) {
93
- parentUnsubscribe = parentListen(value => {
94
- cb(value);
95
- listeners.slice().forEach(fn => fn(value));
216
+ }));
217
+ }
218
+ /**
219
+ * Creates a chainable progress emitter that discards emitted values that are the same as the last value emitted by the new emitter
220
+ * @returns Listenable: emits non-repeating values
221
+ */
222
+ dedupe() {
223
+ let previous = null;
224
+ return new RangeProgression(handler => {
225
+ return this.onListen((value) => {
226
+ if (!previous === null || previous !== value) {
227
+ handler(value);
228
+ previous = value;
229
+ }
96
230
  });
97
- }
98
- return () => {
99
- const idx = listeners.indexOf(handler);
100
- listeners.splice(idx, 1);
101
- if (listeners.length === 0 && parentUnsubscribe) {
102
- parentUnsubscribe();
103
- parentUnsubscribe = null;
104
- }
105
- };
106
- };
107
- return create(tappedListen);
231
+ });
232
+ }
233
+ /**
234
+ * Creates a chainable progress emitter that offsets its parent's values by the given delta, wrapping at 1
235
+ *
236
+ * ```plain
237
+ *‎ 1
238
+ *‎ | /
239
+ *‎ o| /
240
+ *‎ u|/ __ delta=.5
241
+ *‎ t| /
242
+ *‎ | /
243
+ *‎ |___/__
244
+ *‎ 0 in 1
245
+ * ```
246
+ *
247
+ * @param delta
248
+ * @returns Listenable: emits offset values
249
+ */
250
+ offset(delta) {
251
+ return new RangeProgression(handler => this.onListen(value => handler((value + delta) % 1)));
252
+ }
108
253
  }
254
+ exports.RangeProgression = RangeProgression;
@@ -1,9 +1,22 @@
1
- import { Emitter } from "./emitters";
1
+ import { Emitter, ListenFunc } from "./emitters";
2
2
  import { TimelineRange } from "./range";
3
+ import { Timeline } from "./timeline";
3
4
  export type PointEvent = {
4
5
  direction: -1 | 1;
5
6
  };
6
- export interface TimelinePoint extends Emitter<PointEvent> {
7
+ export declare class TimelinePoint extends Emitter<PointEvent> {
8
+ private timeline;
9
+ /**
10
+ * The point's absolute position on the Timeline
11
+ */
12
+ readonly position: number;
13
+ /** @internal Manual construction of TimelinePoint is outside of the API contract and subject to undocumented change */
14
+ constructor(onListen: ListenFunc<PointEvent>, timeline: Timeline,
15
+ /**
16
+ * The point's absolute position on the Timeline
17
+ */
18
+ position: number);
19
+ protected redirect: (listen: ListenFunc<PointEvent>) => TimelinePoint;
7
20
  /**
8
21
  * Creates a range on the Timeline, with a given duration, starting at this point
9
22
  * @param duration
@@ -22,8 +35,4 @@ export interface TimelinePoint extends Emitter<PointEvent> {
22
35
  * @returns Listenable: emits a PointEvent when the point is reached or passed by a Timeline seek
23
36
  */
24
37
  delta(timeOffset: number): TimelinePoint;
25
- /**
26
- * The point's absolute position on the Timeline
27
- */
28
- readonly position: number;
29
38
  }
package/internal/point.js CHANGED
@@ -1,2 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TimelinePoint = void 0;
4
+ const emitters_1 = require("./emitters");
5
+ class TimelinePoint extends emitters_1.Emitter {
6
+ /** @internal Manual construction of TimelinePoint is outside of the API contract and subject to undocumented change */
7
+ constructor(onListen, timeline,
8
+ /**
9
+ * The point's absolute position on the Timeline
10
+ */
11
+ position) {
12
+ super(onListen);
13
+ this.timeline = timeline;
14
+ this.position = position;
15
+ this.redirect = (listen) => new TimelinePoint(listen, this.timeline, this.position);
16
+ }
17
+ /**
18
+ * Creates a range on the Timeline, with a given duration, starting at this point
19
+ * @param duration
20
+ * @returns Listenable: emits normalised (0..1) range progression
21
+ */
22
+ range(duration) {
23
+ return this.timeline.range(this.position, duration);
24
+ }
25
+ /**
26
+ * Creates a range on the Timeline, with a given end point, starting at this point
27
+ * @param endPoint
28
+ * @returns Listenable: emits normalised (0..1) range progression
29
+ */
30
+ to(endPoint) {
31
+ const endPosition = typeof endPoint == "number"
32
+ ? endPoint
33
+ : endPoint.position;
34
+ return this.timeline.range(this.position, endPosition - this.position);
35
+ }
36
+ /**
37
+ * Creates a point on the Timeline at an offset position from this one
38
+ * @param timeOffset
39
+ * @returns Listenable: emits a PointEvent when the point is reached or passed by a Timeline seek
40
+ */
41
+ delta(timeOffset) {
42
+ return this.timeline.point(this.position + timeOffset);
43
+ }
44
+ }
45
+ exports.TimelinePoint = TimelinePoint;
@@ -1,7 +1,18 @@
1
1
  import { Easer, easers } from "./easing";
2
- import { RangeProgression } from "./emitters";
2
+ import { ListenFunc, RangeProgression } from "./emitters";
3
3
  import { TimelinePoint } from "./point";
4
- export interface TimelineRange extends RangeProgression {
4
+ import { Timeline } from "./timeline";
5
+ export declare class TimelineRange extends RangeProgression {
6
+ private timeline;
7
+ private startPosition;
8
+ /** The duration of this range */
9
+ readonly duration: number;
10
+ private endPosition;
11
+ /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
12
+ constructor(onListen: ListenFunc<number>, timeline: Timeline, startPosition: number,
13
+ /** The duration of this range */
14
+ duration: number);
15
+ protected redirect: (listen: ListenFunc<number>) => TimelineRange;
5
16
  /**
6
17
  * Creates two ranges by seperating one at a given point
7
18
  * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
@@ -41,10 +52,9 @@ export interface TimelineRange extends RangeProgression {
41
52
  * @returns true if the provided point is within the range
42
53
  */
43
54
  contains(point: TimelinePoint): boolean;
55
+ contains(range: TimelineRange): boolean;
44
56
  /** The point on the Timeline at which this range begins */
45
57
  readonly start: TimelinePoint;
46
58
  /** The point on the Timeline at which this range ends */
47
59
  readonly end: TimelinePoint;
48
- /** The duration of this range */
49
- readonly duration: number;
50
60
  }
package/internal/range.js CHANGED
@@ -1,2 +1,100 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TimelineRange = void 0;
4
+ const emitters_1 = require("./emitters");
5
+ const point_1 = require("./point");
6
+ const utils_1 = require("./utils");
7
+ class TimelineRange extends emitters_1.RangeProgression {
8
+ /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
9
+ constructor(onListen, timeline, startPosition,
10
+ /** The duration of this range */
11
+ duration) {
12
+ super(onListen);
13
+ this.timeline = timeline;
14
+ this.startPosition = startPosition;
15
+ this.duration = duration;
16
+ this.redirect = (listen) => new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
17
+ this.start = timeline.point(startPosition);
18
+ this.end = timeline.point(startPosition + duration);
19
+ this.endPosition = startPosition + duration;
20
+ }
21
+ /**
22
+ * Creates two ranges by seperating one at a given point
23
+ * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
24
+ *
25
+ * Must be greater than 0 and less than the range's duration
26
+ * @returns Tuple of two ranges
27
+ */
28
+ bisect(position = this.duration / 2) {
29
+ return [
30
+ this.timeline.range(position, this.startPosition),
31
+ this.timeline.range(position + this.startPosition, this.duration - this.startPosition),
32
+ ];
33
+ }
34
+ /**
35
+ * Creates a series of evenly-spread points across the range, excluding the range's start and end
36
+ * @param count Number of Points to return
37
+ * @returns Array(count) of points
38
+ */
39
+ spread(count) {
40
+ const delta = this.duration / (count + 1);
41
+ return [
42
+ ...Array(count).fill(0).map((_, idx) => this.timeline.point(idx * delta + this.startPosition + delta))
43
+ ];
44
+ }
45
+ /**
46
+ * Progresses the Timeline across the range
47
+ * @param easer
48
+ */
49
+ play(easer) {
50
+ this.timeline.pause();
51
+ this.timeline.currentTime = this.startPosition;
52
+ return this.timeline.seek(this.startPosition + this.duration, this.duration, easer);
53
+ }
54
+ /**
55
+ * Creates a new range representing a direct expansion of this one
56
+ * @param delta Amount to grow by (in time units)
57
+ * @param anchor Normalised position at which to expand (0 being the start, expanding right, 1 being the end, expanding left, 0.5 expanding evenly)
58
+ * @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
59
+ */
60
+ grow(delta, anchor = 0) {
61
+ const clampedAnchor = (0, utils_1.clamp)(anchor, 0, 1);
62
+ const leftDelta = -delta * (1 - clampedAnchor);
63
+ const rightDelta = delta * clampedAnchor;
64
+ const newStart = this.startPosition + leftDelta;
65
+ const newEnd = this.startPosition + this.duration + rightDelta;
66
+ if (newEnd < newStart) {
67
+ const mid = (newStart + newEnd) / 2;
68
+ return this.timeline.range(mid, 0);
69
+ }
70
+ return this.timeline.range(newStart, newEnd - newStart);
71
+ }
72
+ /**
73
+ * Creates a new range representing a multiplicative expansion of this one
74
+ * @param factor Size multiplier
75
+ * @param anchor Normalised position at which to expand (0 being the start, expanding right, 1 being the end, expanding left, 0.5 expanding evenly)
76
+ * @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
77
+ */
78
+ scale(factor, anchor = 0) {
79
+ if (factor <= 0) {
80
+ throw new RangeError('scale factor must be > 0');
81
+ }
82
+ const clampedAnchor = (0, utils_1.clamp)(anchor, 0, 1);
83
+ const oldLen = this.endPosition - this.startPosition;
84
+ const pivot = this.startPosition + oldLen * clampedAnchor;
85
+ const newStart = pivot - (pivot - this.startPosition) * factor;
86
+ const newEnd = pivot + (this.endPosition - pivot) * factor;
87
+ if (newEnd < newStart) {
88
+ const mid = (newStart + newEnd) / 2;
89
+ return this.timeline.range(mid, 0);
90
+ }
91
+ return this.timeline.range(newStart, newEnd - newStart);
92
+ }
93
+ contains(target) {
94
+ const [targetStart, targetEnd] = target instanceof point_1.TimelinePoint
95
+ ? [target.position, target.position]
96
+ : [target.startPosition, target.startPosition + target.duration];
97
+ return targetStart >= this.startPosition && targetEnd < this.endPosition;
98
+ }
99
+ }
100
+ exports.TimelineRange = TimelineRange;
@@ -128,9 +128,9 @@ export declare class Timeline {
128
128
  * @deprecated Use timeline.position += n
129
129
  */
130
130
  step(delta: number): void;
131
- tween<T extends Tweenable>(start: number | TimelinePoint, duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer): ChainingInterface;
131
+ tween<T extends Tweenable>(start: number | TimelinePoint, duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer | keyof typeof easers): ChainingInterface;
132
132
  tween<T extends Tweenable>(start: number | TimelinePoint, end: TimelinePoint, // ease migration for tl.tween(0, tl.end, ...)
133
- apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer): ChainingInterface;
133
+ apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer | keyof typeof easers): ChainingInterface;
134
134
  at(position: number | TimelinePoint, action?: () => void, reverse?: boolean | (() => void)): ChainingInterface;
135
135
  private createChainingInterface;
136
136
  /**
@@ -139,7 +139,7 @@ export declare class Timeline {
139
139
  get position(): number;
140
140
  }
141
141
  export interface ChainingInterface {
142
- thenTween(duration: number, apply: (v: number) => void, from?: number, to?: number, easer?: Easer): ChainingInterface;
142
+ thenTween<T extends Tweenable>(duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer: Easer): ChainingInterface;
143
143
  then(action: () => void): ChainingInterface;
144
144
  thenWait(duration: number): ChainingInterface;
145
145
  readonly end: TimelinePoint;
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Timeline = void 0;
4
4
  exports.animate = animate;
5
- const emitters_1 = require("./emitters");
5
+ const point_1 = require("./point");
6
+ const range_1 = require("./range");
6
7
  const utils_1 = require("./utils");
7
8
  const default_fps = 60;
8
9
  const EndAction = {
@@ -88,7 +89,7 @@ class Timeline {
88
89
  handlers,
89
90
  position,
90
91
  };
91
- return (0, emitters_1.createEmitter)(handler => {
92
+ const addHandler = (handler) => {
92
93
  if (this.seeking)
93
94
  throw new Error("Can't add a listener while seeking");
94
95
  // we're adding and removing points and ranges to the internal registry according to whether any subscriptions are active, to allow obsolete points and ranges to be garbage-collected
@@ -107,17 +108,8 @@ class Timeline {
107
108
  this.points.splice(idx, 1);
108
109
  }
109
110
  };
110
- }, {
111
- delta: t => this.point(position + t),
112
- range: duration => this.range(position, duration),
113
- to: target => {
114
- const targetPosition = typeof target == "number"
115
- ? target
116
- : target.position;
117
- return this.range(position, targetPosition - position);
118
- },
119
- position,
120
- });
111
+ };
112
+ return new point_1.TimelinePoint(addHandler, this, position);
121
113
  }
122
114
  range(start = 0, optionalDuration) {
123
115
  const startPoint = typeof start == "number"
@@ -153,58 +145,7 @@ class Timeline {
153
145
  }
154
146
  };
155
147
  };
156
- return (0, emitters_1.createProgressEmitter)(addHandler, {
157
- duration,
158
- start: this.point(startPosition),
159
- end: this.point(startPosition + duration),
160
- bisect: (position = duration / 2) => {
161
- return [
162
- this.range(startPosition, position),
163
- this.range(startPosition + position, duration - position),
164
- ];
165
- },
166
- spread: (count) => {
167
- const delta = duration / (count + 1);
168
- return [
169
- ...Array(count).fill(0).map((_, idx) => this.point(idx * delta + startPosition + delta))
170
- ];
171
- },
172
- play: (easer) => {
173
- this.pause();
174
- this.currentTime = startPosition;
175
- return this.seek(startPosition + duration, duration, easer);
176
- },
177
- grow: (delta, anchor = 0) => {
178
- const clampedAnchor = (0, utils_1.clamp)(anchor, 0, 1);
179
- const leftDelta = -delta * (1 - clampedAnchor);
180
- const rightDelta = delta * clampedAnchor;
181
- const newStart = startPosition + leftDelta;
182
- const newEnd = endPosition + rightDelta;
183
- if (newEnd < newStart) {
184
- const mid = (newStart + newEnd) / 2;
185
- return this.range(mid, 0);
186
- }
187
- return this.range(newStart, newEnd - newStart);
188
- },
189
- scale: (factor, anchor = 0.5) => {
190
- if (factor <= 0) {
191
- throw new RangeError('scale factor must be > 0');
192
- }
193
- const clampedAnchor = (0, utils_1.clamp)(anchor, 0, 1);
194
- const oldLen = endPosition - startPosition;
195
- const pivot = startPosition + oldLen * clampedAnchor;
196
- const newStart = pivot - (pivot - startPosition) * factor;
197
- const newEnd = pivot + (endPosition - pivot) * factor;
198
- if (newEnd < newStart) {
199
- const mid = (newStart + newEnd) / 2;
200
- return this.range(mid, 0);
201
- }
202
- return this.range(newStart, newEnd - newStart);
203
- },
204
- contains: point => {
205
- return point.position >= startPosition && point.position < endPosition;
206
- }
207
- });
148
+ return new range_1.TimelineRange(addHandler, this, startPosition, duration);
208
149
  }
209
150
  getWrappedPosition(n) {
210
151
  if (this.endAction.type !== EndAction.wrap)
@@ -385,7 +326,7 @@ class Timeline {
385
326
  }
386
327
  createChainingInterface(position) {
387
328
  return {
388
- thenTween: (duration, apply, from = 0, to = 1, easer) => {
329
+ thenTween: (duration, apply, from, to, easer) => {
389
330
  return this.tween(position, duration, apply, from, to, easer);
390
331
  },
391
332
  then: (action) => this.at(position, action),
@@ -2,5 +2,3 @@
2
2
  export declare const clamp: (value: number, min: number, max: number) => number;
3
3
  /** @internal */
4
4
  export type Widen<T> = T extends number ? number : T extends string ? string : T;
5
- export type OptionalIfKeyIn<T, U> = Omit<T, keyof U> & Partial<Pick<T, Extract<keyof T, keyof U>>>;
6
- export declare function prototypify<Prototype extends object, Members extends object>(proto: Prototype, members: Members): Prototype & Members;
package/internal/utils.js CHANGED
@@ -1,14 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.clamp = void 0;
4
- exports.prototypify = prototypify;
5
4
  /** @internal */
6
5
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
7
6
  exports.clamp = clamp;
8
- function prototypify(proto, members) {
9
- const propertyDescriptor = Object.fromEntries(Object.entries(members).map(([key, value]) => [
10
- key,
11
- { value }
12
- ]));
13
- return Object.create(proto, propertyDescriptor);
14
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"