@xtia/timeline 1.0.8 → 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(
@@ -100,8 +100,8 @@ range
100
100
  .map(n => `animation-frame-${n}.png`)
101
101
  .listen(filename => img.src = filename);
102
102
 
103
- // each step in the chain is a 'pure', independent emitter that emits
104
- // a transformation of its parent's emissions
103
+ // each step in a chain is a 'pure', independent emitter that emits a
104
+ // transformation of its parent's emissions
105
105
  const filenameEmitter = range
106
106
  .tween(0, 3)
107
107
  .map(Math.floor)
@@ -198,7 +198,7 @@ range
198
198
  import { RGBA } from "@xtia/rgba";
199
199
  range
200
200
  .tween(RGBA.parse("#c971a7"), RGBA.parse("#fff"))
201
- .listen(v => element.style.background = v.hexCode);
201
+ .listen(v => element.style.background = v);
202
202
 
203
203
  import { Angle } from "@xtia/mezr";
204
204
  range
@@ -414,15 +414,15 @@ Performs an interruptable 'smooth seek' to a specified position, lasting `durati
414
414
 
415
415
  Returns a Promise that will be resolved when the smooth seek is completed (or is interrupted by another seek\*).
416
416
 
417
- \* 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.
418
418
 
419
419
  ##### `play(): void`
420
420
 
421
- 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.
422
422
 
423
423
  ##### `play(fps): void`
424
424
 
425
- 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.
426
426
 
427
427
  ##### `tween<T>(start, duration, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
428
428
 
@@ -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
@@ -6,6 +6,7 @@ export class Emitter {
6
6
  this.onListen = onListen;
7
7
  /**
8
8
  * Used by tap() to create a clone of an Emitter with a redirected onListen
9
+ *
9
10
  * Should be overridden in all Emitter subclasses
10
11
  * @see {@link TimelineRange.redirect}
11
12
  * @param listen
@@ -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;
@@ -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
@@ -11,9 +11,9 @@ export class TimelineRange extends RangeProgression {
11
11
  this.startPosition = startPosition;
12
12
  this.duration = duration;
13
13
  this.redirect = (listen) => new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
14
- this.start = timeline.point(startPosition);
15
14
  this.endPosition = startPosition + duration;
16
15
  this.end = timeline.point(this.endPosition);
16
+ this.start = timeline.point(startPosition);
17
17
  }
18
18
  /**
19
19
  * Creates two ranges by seperating one at a given point
@@ -93,4 +93,11 @@ export class TimelineRange extends RangeProgression {
93
93
  : [target.startPosition, target.startPosition + target.duration];
94
94
  return targetStart >= this.startPosition && targetEnd < this.endPosition;
95
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
+ }
96
103
  }
@@ -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,3 +1,4 @@
1
+ import { RangeProgression } from "./emitters";
1
2
  import { TimelinePoint } from "./point";
2
3
  import { TimelineRange } from "./range";
3
4
  import { clamp } from "./utils";
@@ -10,7 +11,7 @@ const EndAction = {
10
11
  };
11
12
  /**
12
13
  * Creates an autoplaying Timeline and returns a range from it
13
- * @param duration
14
+ * @param duration Animation duration, in milliseconds
14
15
  * @returns Object representing a range on a single-use, autoplaying Timeline
15
16
  */
16
17
  export function animate(duration) {
@@ -27,6 +28,17 @@ export class Timeline {
27
28
  get end() {
28
29
  return this.point(this._endPosition);
29
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
+ }
30
42
  constructor(autoplay = false, endAction = "pause") {
31
43
  /**
32
44
  * Multiplies the speed at which `play()` progresses through the Timeline
@@ -43,7 +55,8 @@ export class Timeline {
43
55
  this.smoothSeeker = null;
44
56
  this.seeking = false;
45
57
  this.start = this.point(0);
46
- this.positionHandlers = [];
58
+ this.progressionHandlers = [];
59
+ this._progression = null;
47
60
  if (endAction == "loop")
48
61
  endAction = "restart";
49
62
  if (autoplay !== false) {
@@ -78,8 +91,10 @@ export class Timeline {
78
91
  * Listenable: this point will emit a PointEvent whenever a `seek()` reaches or passes it
79
92
  */
80
93
  point(position) {
81
- if (position > this._endPosition)
94
+ if (position > this._endPosition) {
82
95
  this._endPosition = position;
96
+ this.progressionHandlers.slice().forEach(h => h(this._currentTime / position));
97
+ }
83
98
  const handlers = [];
84
99
  const data = {
85
100
  handlers,
@@ -113,9 +128,9 @@ export class Timeline {
113
128
  : start;
114
129
  const startPosition = startPoint.position;
115
130
  const duration = optionalDuration ?? this._endPosition - startPosition;
116
- const endPosition = startPosition + duration;
117
- if (endPosition > this._endPosition)
118
- this._endPosition = endPosition;
131
+ // const endPosition = startPosition + duration;
132
+ //if (endPosition > this._endPosition) this._endPosition = endPosition;
133
+ // ^ leave this to range's point() calls
119
134
  const handlers = [];
120
135
  const range = {
121
136
  position: startPosition,
@@ -168,7 +183,7 @@ export class Timeline {
168
183
  }
169
184
  if (this.smoothSeeker !== null) {
170
185
  this.smoothSeeker.pause();
171
- // ensure any awaits are resolved for the previous seek?
186
+ // ensure any awaits are resolved for the previous seek
172
187
  this.smoothSeeker.seek(this.smoothSeeker.end);
173
188
  this.smoothSeeker = null;
174
189
  }
@@ -209,7 +224,6 @@ export class Timeline {
209
224
  throw e;
210
225
  }
211
226
  this._currentTime = toPosition;
212
- this.positionHandlers.slice().forEach(h => h(toPosition));
213
227
  this.seeking = false;
214
228
  }
215
229
  seekPoints(to) {
@@ -218,27 +232,25 @@ export class Timeline {
218
232
  const pointsBetween = this.points.filter(direction > 0
219
233
  ? p => p.position > from && p.position <= to
220
234
  : p => p.position <= from && p.position > to);
235
+ const eventData = {
236
+ direction
237
+ };
221
238
  pointsBetween.slice().forEach(p => {
222
239
  this.seekRanges(p.position);
223
240
  this._currentTime = p.position;
224
- const eventData = {
225
- direction
226
- };
227
241
  p.handlers.slice().forEach(h => h(eventData));
228
242
  });
229
243
  }
230
244
  seekRanges(to) {
231
- const fromTime = this._currentTime;
245
+ const seekRange = this.point(Math.min(this._currentTime, to))
246
+ .to(Math.max(this._currentTime, to));
232
247
  this.ranges.slice().forEach((range) => {
233
- const { duration, position } = range;
234
- const end = position + duration;
235
- // filter ranges that overlap seeked range
236
- if (Math.min(position, end) <= Math.max(to, fromTime)
237
- && Math.min(to, fromTime) <= Math.max(position, end)) {
248
+ if (seekRange.overlaps(range)) {
238
249
  let progress = clamp((to - range.position) / range.duration, 0, 1);
239
250
  range.handlers.slice().forEach(h => h(progress));
240
251
  }
241
252
  });
253
+ this.progressionHandlers.slice().forEach(h => h(this._currentTime / this._endPosition));
242
254
  }
243
255
  sortEntries(direction) {
244
256
  this.currentSortDirection = direction;
@@ -340,6 +352,18 @@ export class Timeline {
340
352
  return this._currentTime;
341
353
  }
342
354
  }
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
+ }
343
367
  const sortEvents = (a, b) => {
344
368
  return a.position - b.position;
345
369
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"