@xtia/timeline 1.0.7 → 1.0.9

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
@@ -5,6 +5,7 @@
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
7
  * [API Reference](#reference)
8
+ * [Playground](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
8
9
 
9
10
  ## Basic Use:
10
11
 
@@ -21,7 +22,7 @@ timeline
21
22
  .range(0, 1000)
22
23
  .tween("#646", "#000")
23
24
  .listen(
24
- value => document.body.style.backgroundColor = value
25
+ value => element.style.background = value
25
26
  );
26
27
 
27
28
  // add another tween to make a slow typing effect
@@ -29,15 +30,14 @@ const message = "Hi, planet!";
29
30
  timeline
30
31
  .range(500, 2000)
31
32
  .tween(0, message.length)
33
+ .map(n => message.substring(0, n))
32
34
  .listen(
33
- n => element.textContent = message.substring(0, n)
35
+ s => element.textContent = s
34
36
  );
35
37
 
36
38
  // use an easing function
37
39
  timeline
38
- .end
39
- .delta(500)
40
- .range(3000)
40
+ .range(0, 3000)
41
41
  .ease("bounce")
42
42
  .tween("50%", "0%")
43
43
  .listen(
@@ -85,27 +85,41 @@ asPercent
85
85
  n => progressBar.style.width = n
86
86
  );
87
87
 
88
- // apply easing (creates a *new* emitter)
88
+ // apply easing
89
89
  const eased = firstFiveSeconds.ease("easeInOut");
90
90
  eased.listen(
91
91
  v => console.log(`Eased value: ${v}`)
92
92
  );
93
93
 
94
- // combine them
95
- const frames = eased
94
+ // chain them
95
+ range
96
96
  .tween(0, 30)
97
97
  .map(Math.floor)
98
98
  .dedupe()
99
99
  .tap(n => console.log("Showing frame #", n))
100
100
  .map(n => `animation-frame-${n}.png`)
101
101
  .listen(filename => img.src = filename);
102
+
103
+ // each step in a chain is a 'pure', independent emitter that emits a
104
+ // transformation of its parent's emissions
105
+ const filenameEmitter = range
106
+ .tween(0, 3)
107
+ .map(Math.floor)
108
+ .dedupe()
109
+ .map(n => `animation-frame-${n}.png`);
110
+
111
+ // filenameEmitter will emit filenames as the Timeline passes through 'range'.
112
+ // it can be listened directly or further transformed
113
+ const urlEmitter = filenameEmitter
114
+ .map(filename => `http://www.example.com/${filename}`);
115
+
102
116
  ```
103
117
 
104
118
  Range objects also provide a `play()` method that instructs the Timeline to play through that particular range:
105
119
 
106
120
  ```ts
107
121
  // play through the first two seconds of the Timeline
108
- timeline
122
+ await timeline
109
123
  .range(0, 2000)
110
124
  .play();
111
125
  ```
@@ -158,7 +172,41 @@ timeline
158
172
 
159
173
  ## More on tweening
160
174
 
161
- Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(from: this, to: this): this`.
175
+ Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(from: this, to: this): this`, by the progression value emitted by their parent.
176
+
177
+ ```ts
178
+ const range = timeline.range(0, 2000);
179
+
180
+ // numbers
181
+ range
182
+ .ease("overshootIn")
183
+ .tween(300, 500)
184
+ .listen(v => element.scrollTop = v);
185
+
186
+ // number arrays
187
+ range
188
+ .tween([0, 180], [360, 180])
189
+ .listen((angles) => pieChart.setValues(angles));
190
+
191
+ // strings
192
+ range
193
+ .tween("#000000", "#ff00ff")
194
+ .listen(v => element.style.color = v);
195
+
196
+ // blendable objects
197
+ // (T extends { blend(from: this, to: this): this })
198
+ import { RGBA } from "@xtia/rgba";
199
+ range
200
+ .tween(RGBA.parse("#c971a7"), RGBA.parse("#fff"))
201
+ .listen(v => element.style.background = v);
202
+
203
+ import { Angle } from "@xtia/mezr";
204
+ range
205
+ .tween(Angle.degrees(45), Angle.turns(.5))
206
+ .map(a => `rotate(${a.asDegrees}deg)`)
207
+ .listen(v => element.style.transform = v);
208
+
209
+ ```
162
210
 
163
211
  #### String interpolation
164
212
  * If the strings contain tweenable tokens (numbers, colour codes) and are otherwise identical, those tokens are interpolated
@@ -180,7 +228,7 @@ timeline
180
228
  .listen(v => document.title = v);
181
229
  ```
182
230
 
183
- You can try out the [shadow tweening example at StackBlitz](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
231
+ Try out the [shadow tweening example at StackBlitz](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
184
232
 
185
233
  ## Autoplay and Looping Strategies
186
234
 
@@ -190,13 +238,13 @@ To create a Timeline that immediately starts playing, pass `true` to its constru
190
238
  // immediately fade in an element
191
239
  new Timeline(true)
192
240
  .range(0, 1000)
193
- .tween(v => element.style.opacity = v);
241
+ .listen(v => element.style.opacity = v);
194
242
 
195
243
  // note, an `animate(duration)` function is exported for
196
244
  // disposable, single-use animations such as this:
197
245
  import { animate } from "@xtia/timeline";
198
246
  animate(1000)
199
- .tween(v => element.style.opacity = v);
247
+ .listen(v => element.style.opacity = v);
200
248
  ```
201
249
 
202
250
  Normally a Timeline will simply stop playing when it reaches the end. This can be changed by passing a second argument (`endAction`) to the constructor.
@@ -279,7 +327,7 @@ resourceUrls.forEach(url => {
279
327
  We can pass a second argument to `seek()` to perform a 'smooth seek' over the given duration. A third argument can provide an easing function for the smooth seek process:
280
328
 
281
329
  ```ts
282
- timeline.seek(timeline.end, 400, "overshootIn");
330
+ await timeline.seek(timeline.end, 400, "overshootIn");
283
331
  ```
284
332
 
285
333
  ## Backward-compatibility
@@ -366,15 +414,15 @@ Performs an interruptable 'smooth seek' to a specified position, lasting `durati
366
414
 
367
415
  Returns a Promise that will be resolved when the smooth seek is completed (or is interrupted by another seek\*).
368
416
 
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.
417
+ \* If a smooth seek is interrupted by another seek, the interrupted seek will immediately complete before the new seek is applied, to ensure any resulting state reflects expectations set by the first seek.
370
418
 
371
419
  ##### `play(): void`
372
420
 
373
- Begins playing through the Timeline, from its current position, at (1000 x `timeScale`) units per second, updating 60 times per second.
421
+ Begins playing through the Timeline, from its current position, at (1000 × `timeScale`) units per second, updating 60 times per second.
374
422
 
375
423
  ##### `play(fps): void`
376
424
 
377
- Begins playing through the Timeline, from its current position, at (1000 x `timeScale`) units per second, updating `fps` times per second.
425
+ Begins playing through the Timeline, from its current position, at (1000 × `timeScale`) units per second, updating `fps` times per second.
378
426
 
379
427
  ##### `tween<T>(start, duration, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
380
428
 
package/index.js CHANGED
@@ -1,15 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.easers = exports.RangeProgression = exports.Emitter = exports.TimelineRange = exports.TimelinePoint = exports.animate = exports.Timeline = void 0;
4
- var timeline_1 = require("./internal/timeline");
5
- Object.defineProperty(exports, "Timeline", { enumerable: true, get: function () { return timeline_1.Timeline; } });
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; } });
14
- var easing_1 = require("./internal/easing");
15
- Object.defineProperty(exports, "easers", { enumerable: true, get: function () { return easing_1.easers; } });
1
+ export { Timeline, animate } from "./internal/timeline";
2
+ export { TimelinePoint } from "./internal/point";
3
+ export { TimelineRange } from "./internal/range";
4
+ export { Emitter, RangeProgression } from "./internal/emitters";
5
+ export { easers } from "./internal/easing";
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.easers = void 0;
4
1
  const overshoot = 1.70158;
5
- exports.easers = {
2
+ export const easers = {
6
3
  linear: (x) => x,
7
4
  easeIn: (x) => x * x,
8
5
  easeIn4: (x) => Math.pow(x, 4),
@@ -1,5 +1,5 @@
1
1
  import { Easer, easers } from "./easing";
2
- import { Tweenable } from "./tween";
2
+ import { BlendableWith, Tweenable } from "./tween";
3
3
  type Handler<T> = (value: T) => void;
4
4
  export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
5
5
  export type UnsubscribeFunc = () => void;
@@ -8,6 +8,7 @@ export declare class Emitter<T> {
8
8
  protected constructor(onListen: ListenFunc<T>);
9
9
  /**
10
10
  * Used by tap() to create a clone of an Emitter with a redirected onListen
11
+ *
11
12
  * Should be overridden in all Emitter subclasses
12
13
  * @see {@link TimelineRange.redirect}
13
14
  * @param listen
@@ -123,6 +124,7 @@ export declare class RangeProgression extends Emitter<number> {
123
124
  * @returns Listenable: emits interpolated values
124
125
  */
125
126
  tween<T extends Tweenable>(from: T, to: T): Emitter<T>;
127
+ tween<T extends BlendableWith<T, R>, R>(from: T, to: R): Emitter<T>;
126
128
  /**
127
129
  * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
128
130
  *
@@ -182,7 +184,7 @@ export declare class RangeProgression extends Emitter<number> {
182
184
  *‎ 1
183
185
  *‎ | /
184
186
  *‎ o| /
185
- *‎ u|/ __ delta=.5
187
+ *‎ u|/ __ delta=.5
186
188
  *‎ t| /
187
189
  *‎ | /
188
190
  *‎ |___/__
@@ -1,14 +1,12 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RangeProgression = exports.Emitter = void 0;
4
- const easing_1 = require("./easing");
5
- const tween_1 = require("./tween");
6
- const utils_1 = require("./utils");
7
- class Emitter {
1
+ import { easers } from "./easing";
2
+ import { tweenValue } from "./tween";
3
+ import { clamp } from "./utils";
4
+ export class Emitter {
8
5
  constructor(onListen) {
9
6
  this.onListen = onListen;
10
7
  /**
11
8
  * Used by tap() to create a clone of an Emitter with a redirected onListen
9
+ *
12
10
  * Should be overridden in all Emitter subclasses
13
11
  * @see {@link TimelineRange.redirect}
14
12
  * @param listen
@@ -124,8 +122,7 @@ class Emitter {
124
122
  return this;
125
123
  }
126
124
  }
127
- exports.Emitter = Emitter;
128
- class RangeProgression extends Emitter {
125
+ export class RangeProgression extends Emitter {
129
126
  constructor() {
130
127
  super(...arguments);
131
128
  this.redirect = (listen) => new RangeProgression(listen);
@@ -134,14 +131,14 @@ class RangeProgression extends Emitter {
134
131
  if (!easer)
135
132
  return this;
136
133
  const easerFunc = typeof easer == "string"
137
- ? easing_1.easers[easer]
134
+ ? easers[easer]
138
135
  : easer;
139
136
  return new RangeProgression(easer ? (handler => this.onListen((progress) => {
140
137
  handler(easerFunc(progress));
141
138
  })) : h => this.onListen(h));
142
139
  }
143
140
  tween(from, to) {
144
- return new Emitter(handler => this.onListen(progress => handler((0, tween_1.tweenValue)(from, to, progress))));
141
+ return new Emitter(handler => this.onListen(progress => handler(tweenValue(from, to, progress))));
145
142
  }
146
143
  /**
147
144
  * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
@@ -156,7 +153,7 @@ class RangeProgression extends Emitter {
156
153
  }
157
154
  return new RangeProgression(handler => this.onListen(progress => {
158
155
  const snapped = Math.round(progress * steps) / steps;
159
- handler((0, utils_1.clamp)(snapped, 0, 1));
156
+ handler(clamp(snapped, 0, 1));
160
157
  }));
161
158
  }
162
159
  /**
@@ -177,7 +174,7 @@ class RangeProgression extends Emitter {
177
174
  * @returns Listenable: emits clamped progression values
178
175
  */
179
176
  clamp(min = 0, max = 1) {
180
- return new RangeProgression(handler => this.onListen(progress => handler((0, utils_1.clamp)(progress, min, max))));
177
+ return new RangeProgression(handler => this.onListen(progress => handler(clamp(progress, min, max))));
181
178
  }
182
179
  /**
183
180
  * Creates a chainable progress emitter that maps incoming values to a repeating linear scale
@@ -237,7 +234,7 @@ class RangeProgression extends Emitter {
237
234
  *‎ 1
238
235
  *‎ | /
239
236
  *‎ o| /
240
- *‎ u|/ __ delta=.5
237
+ *‎ u|/ __ delta=.5
241
238
  *‎ t| /
242
239
  *‎ | /
243
240
  *‎ |___/__
@@ -251,4 +248,3 @@ class RangeProgression extends Emitter {
251
248
  return new RangeProgression(handler => this.onListen(value => handler((value + delta) % 1)));
252
249
  }
253
250
  }
254
- exports.RangeProgression = RangeProgression;
@@ -2,7 +2,7 @@ import { Emitter, ListenFunc } from "./emitters";
2
2
  import { TimelineRange } from "./range";
3
3
  import { Timeline } from "./timeline";
4
4
  export type PointEvent = {
5
- direction: -1 | 1;
5
+ readonly direction: -1 | 1;
6
6
  };
7
7
  export declare class TimelinePoint extends Emitter<PointEvent> {
8
8
  private timeline;
package/internal/point.js CHANGED
@@ -1,8 +1,5 @@
1
- "use strict";
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 {
1
+ import { Emitter } from "./emitters";
2
+ export class TimelinePoint extends Emitter {
6
3
  /** @internal Manual construction of TimelinePoint is outside of the API contract and subject to undocumented change */
7
4
  constructor(onListen, timeline,
8
5
  /**
@@ -42,4 +39,3 @@ class TimelinePoint extends emitters_1.Emitter {
42
39
  return this.timeline.point(this.position + timeOffset);
43
40
  }
44
41
  }
45
- exports.TimelinePoint = TimelinePoint;
@@ -8,6 +8,10 @@ export declare class TimelineRange extends RangeProgression {
8
8
  /** The duration of this range */
9
9
  readonly duration: number;
10
10
  private endPosition;
11
+ /** The point on the Timeline at which this range begins */
12
+ readonly start: TimelinePoint;
13
+ /** The point on the Timeline at which this range ends */
14
+ readonly end: TimelinePoint;
11
15
  /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
12
16
  constructor(onListen: ListenFunc<number>, timeline: Timeline, startPosition: number,
13
17
  /** The duration of this range */
@@ -52,9 +56,15 @@ export declare class TimelineRange extends RangeProgression {
52
56
  * @returns true if the provided point is within the range
53
57
  */
54
58
  contains(point: TimelinePoint): boolean;
59
+ /**
60
+ * Checks if a range is fully within this range
61
+ * @param range The range to check
62
+ * @returns true if the provided range is within the parent
63
+ */
55
64
  contains(range: TimelineRange): boolean;
56
- /** The point on the Timeline at which this range begins */
57
- readonly start: TimelinePoint;
58
- /** The point on the Timeline at which this range ends */
59
- readonly end: TimelinePoint;
65
+ overlaps(range: TimelineRange): boolean;
66
+ overlaps(range: {
67
+ position: number;
68
+ duration: number;
69
+ }): boolean;
60
70
  }
package/internal/range.js CHANGED
@@ -1,10 +1,7 @@
1
- "use strict";
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 {
1
+ import { RangeProgression } from "./emitters";
2
+ import { TimelinePoint } from "./point";
3
+ import { clamp } from "./utils";
4
+ export class TimelineRange extends RangeProgression {
8
5
  /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
9
6
  constructor(onListen, timeline, startPosition,
10
7
  /** The duration of this range */
@@ -14,9 +11,9 @@ class TimelineRange extends emitters_1.RangeProgression {
14
11
  this.startPosition = startPosition;
15
12
  this.duration = duration;
16
13
  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
14
  this.endPosition = startPosition + duration;
15
+ this.end = timeline.point(this.endPosition);
16
+ this.start = timeline.point(startPosition);
20
17
  }
21
18
  /**
22
19
  * Creates two ranges by seperating one at a given point
@@ -58,7 +55,7 @@ class TimelineRange extends emitters_1.RangeProgression {
58
55
  * @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
59
56
  */
60
57
  grow(delta, anchor = 0) {
61
- const clampedAnchor = (0, utils_1.clamp)(anchor, 0, 1);
58
+ const clampedAnchor = clamp(anchor, 0, 1);
62
59
  const leftDelta = -delta * (1 - clampedAnchor);
63
60
  const rightDelta = delta * clampedAnchor;
64
61
  const newStart = this.startPosition + leftDelta;
@@ -79,7 +76,7 @@ class TimelineRange extends emitters_1.RangeProgression {
79
76
  if (factor <= 0) {
80
77
  throw new RangeError('scale factor must be > 0');
81
78
  }
82
- const clampedAnchor = (0, utils_1.clamp)(anchor, 0, 1);
79
+ const clampedAnchor = clamp(anchor, 0, 1);
83
80
  const oldLen = this.endPosition - this.startPosition;
84
81
  const pivot = this.startPosition + oldLen * clampedAnchor;
85
82
  const newStart = pivot - (pivot - this.startPosition) * factor;
@@ -91,10 +88,16 @@ class TimelineRange extends emitters_1.RangeProgression {
91
88
  return this.timeline.range(newStart, newEnd - newStart);
92
89
  }
93
90
  contains(target) {
94
- const [targetStart, targetEnd] = target instanceof point_1.TimelinePoint
91
+ const [targetStart, targetEnd] = target instanceof TimelinePoint
95
92
  ? [target.position, target.position]
96
93
  : [target.startPosition, target.startPosition + target.duration];
97
94
  return targetStart >= this.startPosition && targetEnd < this.endPosition;
98
95
  }
96
+ overlaps(range) {
97
+ const [start, end] = range instanceof TimelineRange
98
+ ? [range.startPosition, range.endPosition]
99
+ : [range.position, range.position + range.duration];
100
+ return Math.min(this.startPosition, this.endPosition) <= Math.max(start, end) &&
101
+ Math.max(this.startPosition, this.endPosition) >= Math.min(start, end);
102
+ }
99
103
  }
100
- exports.TimelineRange = TimelineRange;
@@ -1,4 +1,5 @@
1
1
  import { Easer, easers } from "./easing";
2
+ import { RangeProgression } from "./emitters";
2
3
  import { TimelinePoint } from "./point";
3
4
  import { TimelineRange } from "./range";
4
5
  import { Tweenable } from "./tween";
@@ -11,7 +12,7 @@ declare const EndAction: {
11
12
  };
12
13
  /**
13
14
  * Creates an autoplaying Timeline and returns a range from it
14
- * @param duration
15
+ * @param duration Animation duration, in milliseconds
15
16
  * @returns Object representing a range on a single-use, autoplaying Timeline
16
17
  */
17
18
  export declare function animate(duration: number): TimelineRange;
@@ -36,14 +37,22 @@ export declare class Timeline {
36
37
  private smoothSeeker;
37
38
  private seeking;
38
39
  readonly start: TimelinePoint;
39
- private positionHandlers;
40
+ private progressionHandlers;
41
+ private _progression;
42
+ /**
43
+ * Listenable: emits a progression value (0..1) when the Timeline's internal
44
+ * position changes, and when the Timeline's total duration is extended
45
+ *
46
+ * **Experimental**
47
+ */
48
+ get progression(): RangeProgression;
40
49
  constructor();
41
50
  /**
42
- * @param autoplay Pass `true` to begin playing at (1000 x this.timeScale) units per second immediately on creation
51
+ * @param autoplay Pass `true` to begin playing at (1000 × this.timeScale) units per second immediately on creation
43
52
  */
44
53
  constructor(autoplay: boolean);
45
54
  /**
46
- * Creates a Timeline that begins playing immediately at (1000 x this.timeScale) units per second
55
+ * Creates a Timeline that begins playing immediately at (1000 × this.timeScale) units per second
47
56
  * @param autoplayFps Specifies frames per second
48
57
  */
49
58
  constructor(autoplayFps: number);
@@ -51,12 +60,12 @@ export declare class Timeline {
51
60
  * @param autoplay If this argument is `true`, the Timeline will begin playing immediately on creation. If the argument is a number, the Timeline will begin playing at the specified frames per second
52
61
  * @param endAction Specifies what should happen when the final position is passed by `play()`/`autoplay`
53
62
  *
54
- * `"pause"`: **(default)** the Timeline will pause at its final position
55
- * `"continue"`: The Timeline will continue progressing beyond its final position
56
- * `"restart"`: The Timeline will seek back to 0 then forward to account for any overshoot and continue progressing
57
- * `"wrap"`: The Timeline's position will continue to increase beyond the final position, but Points and Ranges will be activated as if looping
58
- * `{restartAt: number}`: Like `"restart"` but seeking back to `restartAt` instead of 0
59
- * `{wrapAt: number}`: Like `"wrap"` but as if restarting at `wrapAt` instead of 0
63
+ * * `"pause"`: **(default)** the Timeline will pause at its final position
64
+ * * `"continue"`: The Timeline will continue progressing beyond its final position
65
+ * * `"restart"`: The Timeline will seek back to 0 then forward to account for any overshoot and continue progressing
66
+ * * `"wrap"`: The Timeline's position will continue to increase beyond the final position, but Points and Ranges will be activated as if looping
67
+ * * `{restartAt: number}`: Like `"restart"` but seeking back to `restartAt` instead of 0
68
+ * * `{wrapAt: number}`: Like `"wrap"` but as if restarting at `wrapAt` instead of 0
60
69
  */
61
70
  constructor(autoplay: boolean | number, endAction: {
62
71
  wrapAt: number;
@@ -99,7 +108,7 @@ export declare class Timeline {
99
108
  /**
100
109
  * Smooth-seeks to a specified position
101
110
  *
102
- * Aborts and replaces any on-going smooth-seek process on this Timeline
111
+ * Immediately completes and replaces any ongoing smooth-seek process on this Timeline
103
112
  * @param toPosition
104
113
  * @param duration Duration of the smooth-seek process in milliseconds
105
114
  * @param easer Optional easing function for the smooth-seek process
@@ -111,7 +120,7 @@ export declare class Timeline {
111
120
  private seekRanges;
112
121
  private sortEntries;
113
122
  /**
114
- * Starts progression of the Timeline from its current position at (1000 x this.timeScale) units per second
123
+ * Starts progression of the Timeline from its current position at (1000 × this.timeScale) units per second
115
124
  */
116
125
  play(): void;
117
126
  play(fps: number): void;
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Timeline = void 0;
4
- exports.animate = animate;
5
- const point_1 = require("./point");
6
- const range_1 = require("./range");
7
- const utils_1 = require("./utils");
1
+ import { RangeProgression } from "./emitters";
2
+ import { TimelinePoint } from "./point";
3
+ import { TimelineRange } from "./range";
4
+ import { clamp } from "./utils";
8
5
  const default_fps = 60;
9
6
  const EndAction = {
10
7
  pause: 0,
@@ -14,13 +11,13 @@ const EndAction = {
14
11
  };
15
12
  /**
16
13
  * Creates an autoplaying Timeline and returns a range from it
17
- * @param duration
14
+ * @param duration Animation duration, in milliseconds
18
15
  * @returns Object representing a range on a single-use, autoplaying Timeline
19
16
  */
20
- function animate(duration) {
17
+ export function animate(duration) {
21
18
  return new Timeline(true).range(0, duration);
22
19
  }
23
- class Timeline {
20
+ export class Timeline {
24
21
  get currentTime() { return this._currentTime; }
25
22
  set currentTime(v) {
26
23
  this.seek(v);
@@ -31,6 +28,17 @@ class Timeline {
31
28
  get end() {
32
29
  return this.point(this._endPosition);
33
30
  }
31
+ /**
32
+ * Listenable: emits a progression value (0..1) when the Timeline's internal
33
+ * position changes, and when the Timeline's total duration is extended
34
+ *
35
+ * **Experimental**
36
+ */
37
+ get progression() {
38
+ if (this._progression === null)
39
+ this._progression = new TimelineProgressionEmitter(this.progressionHandlers);
40
+ return this._progression;
41
+ }
34
42
  constructor(autoplay = false, endAction = "pause") {
35
43
  /**
36
44
  * Multiplies the speed at which `play()` progresses through the Timeline
@@ -47,7 +55,8 @@ class Timeline {
47
55
  this.smoothSeeker = null;
48
56
  this.seeking = false;
49
57
  this.start = this.point(0);
50
- this.positionHandlers = [];
58
+ this.progressionHandlers = [];
59
+ this._progression = null;
51
60
  if (endAction == "loop")
52
61
  endAction = "restart";
53
62
  if (autoplay !== false) {
@@ -82,8 +91,10 @@ class Timeline {
82
91
  * Listenable: this point will emit a PointEvent whenever a `seek()` reaches or passes it
83
92
  */
84
93
  point(position) {
85
- if (position > this._endPosition)
94
+ if (position > this._endPosition) {
86
95
  this._endPosition = position;
96
+ this.progressionHandlers.slice().forEach(h => h(this._currentTime / position));
97
+ }
87
98
  const handlers = [];
88
99
  const data = {
89
100
  handlers,
@@ -109,7 +120,7 @@ class Timeline {
109
120
  }
110
121
  };
111
122
  };
112
- return new point_1.TimelinePoint(addHandler, this, position);
123
+ return new TimelinePoint(addHandler, this, position);
113
124
  }
114
125
  range(start = 0, optionalDuration) {
115
126
  const startPoint = typeof start == "number"
@@ -117,9 +128,9 @@ class Timeline {
117
128
  : start;
118
129
  const startPosition = startPoint.position;
119
130
  const duration = optionalDuration ?? this._endPosition - startPosition;
120
- const endPosition = startPosition + duration;
121
- if (endPosition > this._endPosition)
122
- this._endPosition = endPosition;
131
+ // const endPosition = startPosition + duration;
132
+ //if (endPosition > this._endPosition) this._endPosition = endPosition;
133
+ // ^ leave this to range's point() calls
123
134
  const handlers = [];
124
135
  const range = {
125
136
  position: startPosition,
@@ -145,7 +156,7 @@ class Timeline {
145
156
  }
146
157
  };
147
158
  };
148
- return new range_1.TimelineRange(addHandler, this, startPosition, duration);
159
+ return new TimelineRange(addHandler, this, startPosition, duration);
149
160
  }
150
161
  getWrappedPosition(n) {
151
162
  if (this.endAction.type !== EndAction.wrap)
@@ -172,7 +183,7 @@ class Timeline {
172
183
  }
173
184
  if (this.smoothSeeker !== null) {
174
185
  this.smoothSeeker.pause();
175
- // ensure any awaits are resolved for the previous seek?
186
+ // ensure any awaits are resolved for the previous seek
176
187
  this.smoothSeeker.seek(this.smoothSeeker.end);
177
188
  this.smoothSeeker = null;
178
189
  }
@@ -213,7 +224,6 @@ class Timeline {
213
224
  throw e;
214
225
  }
215
226
  this._currentTime = toPosition;
216
- this.positionHandlers.slice().forEach(h => h(toPosition));
217
227
  this.seeking = false;
218
228
  }
219
229
  seekPoints(to) {
@@ -222,27 +232,25 @@ class Timeline {
222
232
  const pointsBetween = this.points.filter(direction > 0
223
233
  ? p => p.position > from && p.position <= to
224
234
  : p => p.position <= from && p.position > to);
235
+ const eventData = {
236
+ direction
237
+ };
225
238
  pointsBetween.slice().forEach(p => {
226
239
  this.seekRanges(p.position);
227
240
  this._currentTime = p.position;
228
- const eventData = {
229
- direction
230
- };
231
241
  p.handlers.slice().forEach(h => h(eventData));
232
242
  });
233
243
  }
234
244
  seekRanges(to) {
235
- const fromTime = this._currentTime;
245
+ const seekRange = this.point(Math.min(this._currentTime, to))
246
+ .to(Math.max(this._currentTime, to));
236
247
  this.ranges.slice().forEach((range) => {
237
- const { duration, position } = range;
238
- const end = position + duration;
239
- // filter ranges that overlap seeked range
240
- if (Math.min(position, end) <= Math.max(to, fromTime)
241
- && Math.min(to, fromTime) <= Math.max(position, end)) {
242
- let progress = (0, utils_1.clamp)((to - range.position) / range.duration, 0, 1);
248
+ if (seekRange.overlaps(range)) {
249
+ let progress = clamp((to - range.position) / range.duration, 0, 1);
243
250
  range.handlers.slice().forEach(h => h(progress));
244
251
  }
245
252
  });
253
+ this.progressionHandlers.slice().forEach(h => h(this._currentTime / this._endPosition));
246
254
  }
247
255
  sortEntries(direction) {
248
256
  this.currentSortDirection = direction;
@@ -344,7 +352,18 @@ class Timeline {
344
352
  return this._currentTime;
345
353
  }
346
354
  }
347
- exports.Timeline = Timeline;
355
+ class TimelineProgressionEmitter extends RangeProgression {
356
+ constructor(handlers) {
357
+ super((handler) => {
358
+ const unique = (n) => handler(n);
359
+ handlers.push(unique);
360
+ return () => {
361
+ const idx = handlers.indexOf(unique);
362
+ handlers.splice(idx, 1);
363
+ };
364
+ });
365
+ }
366
+ }
348
367
  const sortEvents = (a, b) => {
349
368
  return a.position - b.position;
350
369
  };
@@ -4,5 +4,8 @@ export type Tweenable = number | number[] | string | Blendable;
4
4
  export interface Blendable {
5
5
  blend(target: this, progress: number): this;
6
6
  }
7
+ export interface BlendableWith<T, R> {
8
+ blend(target: R, progress: number): T;
9
+ }
7
10
  /** @internal */
8
11
  export declare function tweenValue<T extends Tweenable>(from: T, to: T, progress: number): T;
package/internal/tween.js CHANGED
@@ -1,9 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tweenValue = tweenValue;
4
- const utils_1 = require("./utils");
1
+ import { clamp } from "./utils";
5
2
  /** @internal */
6
- function tweenValue(from, to, progress) {
3
+ export function tweenValue(from, to, progress) {
7
4
  if (Array.isArray(from)) {
8
5
  const toArr = to;
9
6
  if (from.length != toArr.length)
@@ -89,7 +86,7 @@ function parseColour(code) {
89
86
  function blendColours(from, to, bias) {
90
87
  const fromColour = parseColour(from);
91
88
  const toColour = parseColour(to);
92
- const blended = fromColour.map((val, i) => (0, utils_1.clamp)(blendNumbers(val, toColour[i], bias), 0, 255));
89
+ const blended = fromColour.map((val, i) => clamp(blendNumbers(val, toColour[i], bias), 0, 255));
93
90
  return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
94
91
  }
95
92
  const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
package/internal/utils.js CHANGED
@@ -1,6 +1,2 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.clamp = void 0;
4
1
  /** @internal */
5
- const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
6
- exports.clamp = clamp;
2
+ export const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"