@xtia/timeline 1.1.0 → 1.1.2

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
@@ -1,8 +1,8 @@
1
1
  # Timeline
2
2
 
3
- ### A Type‑Safe Choreography Engine for Deterministic Timelines
3
+ ### Not Just Another Animation Library
4
4
 
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.
5
+ Timeline is a type-safe, seekable, deterministic choreography system that can control state transitions in any environment, whether that's a simple or complex CSS animation, managing a microcontroller's output, or synchronising complex hardware sequences.
6
6
 
7
7
  * [API Reference](#reference)
8
8
  * [Playground](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
@@ -17,7 +17,7 @@ import { Timeline } from "@xtia/timeline";
17
17
  // create a Timeline
18
18
  const timeline = new Timeline();
19
19
 
20
- // over the first second, fade the body's background colour
20
+ // over the first second, fade an element's background colour
21
21
  timeline
22
22
  .range(0, 1000)
23
23
  .tween("#646", "#000")
@@ -29,20 +29,18 @@ timeline
29
29
  const message = "Hi, planet!";
30
30
  timeline
31
31
  .range(500, 2000)
32
+ .ease("easeOut")
32
33
  .tween(0, message.length)
33
34
  .map(n => message.substring(0, n))
34
35
  .apply(
35
36
  s => element.textContent = s
36
37
  );
37
38
 
38
- // use an easing function
39
+ // control anything:
39
40
  timeline
40
- .range(0, 3000)
41
- .ease("bounce")
42
- .tween("50%", "0%")
43
- .apply(
44
- value => element.style.marginLeft = value
45
- );
41
+ .range(1000, 2000)
42
+ .tween(0, 255)
43
+ .listen(value => microcontroller.setPWM(value))
46
44
 
47
45
  // make it go
48
46
  timeline.play();
@@ -129,7 +127,7 @@ Custom easers can be passed to `ease()` as `(progress: number) => number`:
129
127
  ```ts
130
128
  timeline
131
129
  .range(0, 1000)
132
- .ease(n => Math.sqrt(n))
130
+ .ease(n => n * n)
133
131
  .tween(/*...*/);
134
132
  ```
135
133
 
@@ -491,6 +489,33 @@ Seeks the parent Timeline to this point.
491
489
 
492
490
  Smooth-seeks the parent Timeline to this point over a specified duration and resolves the returned Promise on completion.
493
491
 
492
+ ##### `promise(): Promise<-1 | 1>`
493
+
494
+ Creates a `Promise` that will be resolved when the Timeline first seeks to/past this point.
495
+
496
+ The resolved value indicates the direction of the seek that triggered resolution.
497
+
498
+ ##### `forwardOnly(): Emitter<PointEvent>`
499
+
500
+ Creates an emitter that forwards emissions triggered by forward-moving seeks.
501
+
502
+ ##### `reverseOnly(): Emitter<PointEvent>`
503
+
504
+ Creates an emitter that forwards emissions triggered by backward-moving seeks.
505
+
506
+ ##### `applyDirectional(apply, revert): UnsubscribeFunc`
507
+
508
+ Registers an emission handler that calls one function for forward seeks to or past the point, and another for backward seeks from or past the point.
509
+
510
+ ```ts
511
+ point
512
+ .applyDirectional(
513
+ () => element.classList.add("faded"),
514
+ () => element.classList.remove("faded"),
515
+ );
516
+ ```
517
+
518
+
494
519
 
495
520
 
496
521
  ### `PointEvent` interface
@@ -565,10 +590,22 @@ Creates a new range on the parent Timeline. The location and duration of the new
565
590
 
566
591
  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.
567
592
 
568
- ##### `contains(point)`
593
+ ##### `subdivide(n): TimelineRange[]`
594
+
595
+ Creates the specified number of ranges, each of `(parent.duration / count)` duration, spread evenly over this range.
596
+
597
+ ##### `shift(delta): TimelineRange`
598
+
599
+ Creates a new range by offsetting the parent by a given time delta.
600
+
601
+ ##### `contains(point): boolean`
569
602
 
570
603
  Returns true if the given [`TimelinePoint`](#timelinepoint-interface) sits within this range.
571
604
 
605
+ ##### `overlaps(range): boolean`
606
+
607
+ Returns true if the given range overlaps with this range.
608
+
572
609
 
573
610
 
574
611
 
@@ -634,9 +671,20 @@ If `check(value)` returns true, the value will be emitted.
634
671
 
635
672
  Creates an emitter that discards emitted values that are the same as the last value emitted by the new emitter
636
673
 
674
+ ##### `sample<T>(items): T`
675
+
676
+ Creates a chainable emitter that takes a value from an array according to progression.
677
+
678
+ ```ts
679
+ range
680
+ .sample(["a", "b", "c"])
681
+ .apply(v => console.log(v));
682
+ // logs 'b' when a seek lands halfway through range
683
+ ```
684
+
637
685
  ##### `offset(delta): RangeProgression`
638
686
 
639
- Creates an emitter that offsets its parent's values by the given delta, wrapping at 1
687
+ Creates an emitter that offsets its parent's values by the given delta, wrapping at 1.
640
688
 
641
689
  ##### `fork(cb: (branch) => void): RangeProgression`
642
690
 
@@ -657,7 +705,6 @@ range
657
705
 
658
706
 
659
707
 
660
-
661
708
  ### `Emitter<T>` interface
662
709
 
663
710
  #### Methods
@@ -1,5 +1,5 @@
1
1
  import { Easer } from "./easing";
2
- import { Emitter, ListenFunc } from "./emitters";
2
+ import { Emitter, ListenFunc, UnsubscribeFunc } from "./emitters";
3
3
  import { TimelineRange } from "./range";
4
4
  import { Timeline } from "./timeline";
5
5
  export type PointEvent = {
@@ -41,4 +41,42 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
41
41
  */
42
42
  seek(): void;
43
43
  seek(duration: number, easer?: Easer): Promise<void>;
44
+ /**
45
+ * Creates an emitter that only emits on forward-moving seeks
46
+ * @returns
47
+ */
48
+ forwardOnly(): Emitter<PointEvent>;
49
+ /**
50
+ * Creates an emitter that only emits on backward-moving seeks
51
+ * @returns
52
+ */
53
+ reverseOnly(): Emitter<PointEvent>;
54
+ /**
55
+ * Creates a Promise that will be resolved when the Timeline first seeks to/past this point
56
+ *
57
+ * The resolved value indicates the direction of the seek that triggered resolution
58
+ *
59
+ * @returns A Promise, resolved when the point is triggered by a seek
60
+ */
61
+ promise(): Promise<1 | -1>;
62
+ /**
63
+ * Registers a pair of functions to handle seeks that reach or pass this point, depending on seek direction
64
+ *
65
+ * @example
66
+ * ```
67
+ * point
68
+ * .applyDirectional(
69
+ * () => element.classList.add("faded"),
70
+ * () => element.classList.remove("faded"),
71
+ * );
72
+ * ```
73
+ *
74
+ * Note, for deterministic consistency, a forward-seek triggers points when it
75
+ * *passes or reaches* them, while a backward-seek triggers points when it
76
+ * *passes or departs from* them.
77
+ * @param apply Handler for forward-moving seeks that pass or reach this point
78
+ * @param revert Handler for backward-moving seeks that pass this point
79
+ * @returns A function to deregister both handlers
80
+ */
81
+ applyDirectional(apply: () => void, revert: () => void): UnsubscribeFunc;
44
82
  }
package/internal/point.js CHANGED
@@ -41,4 +41,67 @@ export class TimelinePoint extends Emitter {
41
41
  seek(duration = 0, easer) {
42
42
  return this.timeline.seek(this.position, duration, easer);
43
43
  }
44
+ /**
45
+ * Creates an emitter that only emits on forward-moving seeks
46
+ * @returns
47
+ */
48
+ forwardOnly() {
49
+ return new Emitter(handler => {
50
+ return this.onListen((ev) => {
51
+ if (ev.direction > 0)
52
+ handler(ev);
53
+ });
54
+ });
55
+ }
56
+ /**
57
+ * Creates an emitter that only emits on backward-moving seeks
58
+ * @returns
59
+ */
60
+ reverseOnly() {
61
+ return new Emitter(handler => {
62
+ return this.onListen((ev) => {
63
+ if (ev.direction < 0)
64
+ handler(ev);
65
+ });
66
+ });
67
+ }
68
+ /**
69
+ * Creates a Promise that will be resolved when the Timeline first seeks to/past this point
70
+ *
71
+ * The resolved value indicates the direction of the seek that triggered resolution
72
+ *
73
+ * @returns A Promise, resolved when the point is triggered by a seek
74
+ */
75
+ promise() {
76
+ return new Promise(resolve => {
77
+ let remove = this.apply((ev) => {
78
+ remove();
79
+ resolve(ev.direction);
80
+ });
81
+ });
82
+ }
83
+ /**
84
+ * Registers a pair of functions to handle seeks that reach or pass this point, depending on seek direction
85
+ *
86
+ * @example
87
+ * ```
88
+ * point
89
+ * .applyDirectional(
90
+ * () => element.classList.add("faded"),
91
+ * () => element.classList.remove("faded"),
92
+ * );
93
+ * ```
94
+ *
95
+ * Note, for deterministic consistency, a forward-seek triggers points when it
96
+ * *passes or reaches* them, while a backward-seek triggers points when it
97
+ * *passes or departs from* them.
98
+ * @param apply Handler for forward-moving seeks that pass or reach this point
99
+ * @param revert Handler for backward-moving seeks that pass this point
100
+ * @returns A function to deregister both handlers
101
+ */
102
+ applyDirectional(apply, revert) {
103
+ return this.onListen(eventData => eventData.direction > 0
104
+ ? apply()
105
+ : revert());
106
+ }
44
107
  }
@@ -151,6 +151,7 @@ export interface ChainingInterface {
151
151
  thenTween<T extends Tweenable>(duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer: Easer): ChainingInterface;
152
152
  then(action: () => void): ChainingInterface;
153
153
  thenWait(duration: number): ChainingInterface;
154
+ fork(fn: (chain: ChainingInterface) => void): ChainingInterface;
154
155
  readonly end: TimelinePoint;
155
156
  }
156
157
  export {};
@@ -330,12 +330,12 @@ export class Timeline {
330
330
  reverse = action;
331
331
  if (action)
332
332
  point.apply(reverse
333
- ? (event => event.direction < 0 ? reverse() : action)
333
+ ? (event => event.direction < 0 ? reverse() : action())
334
334
  : action);
335
335
  return this.createChainingInterface(point.position);
336
336
  }
337
337
  createChainingInterface(position) {
338
- return {
338
+ const chain = {
339
339
  thenTween: (duration, apply, from, to, easer) => {
340
340
  return this.tween(position, duration, apply, from, to, easer);
341
341
  },
@@ -344,8 +344,13 @@ export class Timeline {
344
344
  this.point(position + delay);
345
345
  return this.createChainingInterface(position + delay);
346
346
  },
347
+ fork: fn => {
348
+ fn(chain);
349
+ return chain;
350
+ },
347
351
  end: this.point(position),
348
352
  };
353
+ return chain;
349
354
  }
350
355
  /**
351
356
  * @deprecated use `timeline.currentTime`
package/internal/tween.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { clamp } from "./utils";
2
- const tokenTypes = {
3
- none: 0,
4
- number: 1,
5
- colour: 2,
6
- };
2
+ var TokenTypes;
3
+ (function (TokenTypes) {
4
+ TokenTypes[TokenTypes["none"] = 0] = "none";
5
+ TokenTypes[TokenTypes["number"] = 1] = "number";
6
+ TokenTypes[TokenTypes["colour"] = 2] = "colour";
7
+ })(TokenTypes || (TokenTypes = {}));
8
+ ;
7
9
  export function createTween(from, to) {
8
10
  if (from === to)
9
11
  return () => from;
@@ -39,9 +41,9 @@ function createStringTween(from, to) {
39
41
  const fromToken = chunk.token;
40
42
  const toToken = toChunks[i].token;
41
43
  const prefix = chunk.prefix;
42
- if (chunk.type === tokenTypes.none)
44
+ if (chunk.type === TokenTypes.none)
43
45
  return () => prefix;
44
- if (chunk.type === tokenTypes.colour) {
46
+ if (chunk.type === TokenTypes.colour) {
45
47
  const fromColour = parseColour(fromToken);
46
48
  const toColour = parseColour(toToken);
47
49
  return progress => prefix + blendColours(fromColour, toColour, progress);
@@ -142,13 +144,13 @@ function tokenise(s) {
142
144
  // trailing literal after the last token – stored as a final chunk
143
145
  const tail = s.slice(lastIdx);
144
146
  if (tail.length) {
145
- chunks.push({ prefix: tail, token: "", type: tokenTypes.none });
147
+ chunks.push({ prefix: tail, token: "", type: TokenTypes.none });
146
148
  }
147
149
  return chunks;
148
150
  }
149
151
  ;
150
152
  function getTokenType(token) {
151
153
  if (token.startsWith("#"))
152
- return tokenTypes.colour;
153
- return tokenTypes.number;
154
+ return TokenTypes.colour;
155
+ return TokenTypes.number;
154
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"