@xtia/timeline 1.1.5 → 1.1.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.
@@ -6,15 +6,7 @@ export type UnsubscribeFunc = () => void;
6
6
  export declare class Emitter<T> {
7
7
  protected onListen: ListenFunc<T>;
8
8
  protected constructor(onListen: ListenFunc<T>);
9
- /**
10
- * Used by tap() to create a clone of an Emitter with a redirected onListen
11
- *
12
- * Should be overridden in all Emitter subclasses
13
- * @see {@link TimelineRange.redirect}
14
- * @param listen
15
- * @returns {this}
16
- */
17
- protected redirect(listen: ListenFunc<T>): Emitter<T>;
9
+ protected createTransformListen<R = T>(handler: (value: T, emit: (value: R) => void) => void): (fn: (v: R) => void) => UnsubscribeFunc;
18
10
  /**
19
11
  * Compatibility alias for `apply()` - registers a function to receive emitted values
20
12
  * @param handler
@@ -58,7 +50,7 @@ export declare class Emitter<T> {
58
50
  * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
59
51
  * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
60
52
  */
61
- tap(cb: Handler<T>): this;
53
+ tap(cb: Handler<T>): Emitter<T>;
62
54
  /**
63
55
  * Immediately passes this emitter to a callback and returns this emitter
64
56
  *
@@ -79,7 +71,6 @@ export declare class Emitter<T> {
79
71
  fork(cb: (branch: this) => void): this;
80
72
  }
81
73
  export declare class RangeProgression extends Emitter<number> {
82
- protected redirect(listen: ListenFunc<number>): RangeProgression;
83
74
  /**
84
75
  * Creates a chainable progress emitter that applies an easing function to its parent's emitted values
85
76
  *
@@ -91,7 +82,7 @@ export declare class RangeProgression extends Emitter<number> {
91
82
  /**
92
83
  * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
93
84
  *
94
- * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
85
+ * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, progression: number): this` method
95
86
  *
96
87
  * @param from Value to interpolate from
97
88
  * @param to Value to interpolate to
@@ -101,7 +92,7 @@ export declare class RangeProgression extends Emitter<number> {
101
92
  /**
102
93
  * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
103
94
  *
104
- * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
95
+ * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, progression: number): this` method
105
96
  *
106
97
  * #### String interpolation
107
98
  * * If the strings contain tweenable tokens (numbers, colour codes) and are otherwise identical, those tokens are interpolated
@@ -122,7 +113,7 @@ export declare class RangeProgression extends Emitter<number> {
122
113
  /**
123
114
  * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
124
115
  *
125
- * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
116
+ * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, progression: number): this` method
126
117
  *
127
118
  * @param from Value to interpolate from
128
119
  * @param to Value to interpolate to
@@ -190,12 +181,13 @@ export declare class RangeProgression extends Emitter<number> {
190
181
  * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
191
182
  * @returns Listenable: emits values that pass the filter
192
183
  */
193
- filter(check: (value: number) => boolean): RangeProgression;
184
+ filter(check: (progress: number) => boolean): RangeProgression;
194
185
  /**
195
186
  * Creates a chainable progress emitter that discards emitted values that are the same as the last value emitted by the new emitter
196
187
  * @returns Listenable: emits non-repeating values
197
188
  */
198
189
  dedupe(): RangeProgression;
190
+ private _dedupe?;
199
191
  /**
200
192
  * Creates a chainable progress emitter that offsets its parent's values by the given delta, wrapping at 1
201
193
  *
@@ -214,6 +206,18 @@ export declare class RangeProgression extends Emitter<number> {
214
206
  * @returns Listenable: emits offset values
215
207
  */
216
208
  offset(delta: number): RangeProgression;
209
+ /**
210
+ * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
211
+ *
212
+ * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
213
+ * All listeners attached to the returned emitter receive the same values as the parent emitter.
214
+ *
215
+ * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
216
+ *
217
+ * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
218
+ * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
219
+ */
220
+ tap(cb: Handler<number>): RangeProgression;
217
221
  }
218
222
  export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLast?: () => void): {
219
223
  listen: (fn: (v: T) => void) => UnsubscribeFunc;
@@ -5,16 +5,15 @@ export class Emitter {
5
5
  constructor(onListen) {
6
6
  this.onListen = onListen;
7
7
  }
8
- /**
9
- * Used by tap() to create a clone of an Emitter with a redirected onListen
10
- *
11
- * Should be overridden in all Emitter subclasses
12
- * @see {@link TimelineRange.redirect}
13
- * @param listen
14
- * @returns {this}
15
- */
16
- redirect(listen) {
17
- return new Emitter(listen);
8
+ createTransformListen(handler) {
9
+ let parentUnsubscribe = null;
10
+ const { emit, listen } = createListenable(() => parentUnsubscribe = this.onListen(value => {
11
+ handler(value, emit);
12
+ }), () => {
13
+ parentUnsubscribe();
14
+ parentUnsubscribe = null;
15
+ });
16
+ return listen;
18
17
  }
19
18
  /**
20
19
  * Compatibility alias for `apply()` - registers a function to receive emitted values
@@ -22,9 +21,7 @@ export class Emitter {
22
21
  * @returns A function to deregister the handler
23
22
  */
24
23
  listen(handler) {
25
- return this.onListen((value) => {
26
- handler(value);
27
- });
24
+ return this.onListen(handler);
28
25
  }
29
26
  /**
30
27
  * Registers a function to receive emitted values
@@ -32,9 +29,7 @@ export class Emitter {
32
29
  * @returns A function to deregister the handler
33
30
  */
34
31
  apply(handler) {
35
- return this.onListen((value) => {
36
- handler(value);
37
- });
32
+ return this.onListen(handler);
38
33
  }
39
34
  /**
40
35
  * Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
@@ -42,9 +37,8 @@ export class Emitter {
42
37
  * @returns Listenable: emits transformed values
43
38
  */
44
39
  map(mapFunc) {
45
- return new Emitter(handler => this.onListen((value) => {
46
- handler(mapFunc(value));
47
- }));
40
+ const listen = this.createTransformListen((value, emit) => emit(mapFunc(value)));
41
+ return new Emitter(listen);
48
42
  }
49
43
  /**
50
44
  * Creates a chainable emitter that selectively forwards emissions along the chain
@@ -52,10 +46,8 @@ export class Emitter {
52
46
  * @returns Listenable: emits values that pass the filter
53
47
  */
54
48
  filter(check) {
55
- return new Emitter(handler => this.onListen((value) => {
56
- if (check(value))
57
- handler(value);
58
- }));
49
+ const listen = this.createTransformListen((value, emit) => check(value) && emit(value));
50
+ return new Emitter(listen);
59
51
  }
60
52
  /**
61
53
  * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
@@ -66,17 +58,15 @@ export class Emitter {
66
58
  */
67
59
  dedupe(compare) {
68
60
  let previous = null;
69
- return new Emitter(handler => {
70
- const filteredHandler = (value) => {
71
- if (!previous || (compare
72
- ? !compare(previous.value, value)
73
- : (previous.value !== value))) {
74
- handler(value);
75
- previous = { value };
76
- }
77
- };
78
- return this.onListen(filteredHandler);
61
+ const listen = this.createTransformListen((value, emit) => {
62
+ if (!previous || (compare
63
+ ? !compare(previous.value, value)
64
+ : (previous.value !== value))) {
65
+ emit(value);
66
+ previous = { value };
67
+ }
79
68
  });
69
+ return new Emitter(listen);
80
70
  }
81
71
  /**
82
72
  * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
@@ -90,15 +80,11 @@ export class Emitter {
90
80
  * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
91
81
  */
92
82
  tap(cb) {
93
- let parentUnsubscribe = null;
94
- const { emit, listen } = createListenable(() => parentUnsubscribe = this.onListen(value => {
83
+ const listen = this.createTransformListen((value, emit) => {
95
84
  cb(value);
96
85
  emit(value);
97
- }), () => {
98
- parentUnsubscribe();
99
- parentUnsubscribe = null;
100
86
  });
101
- return this.redirect(listen);
87
+ return new Emitter(listen);
102
88
  }
103
89
  /**
104
90
  * Immediately passes this emitter to a callback and returns this emitter
@@ -123,22 +109,19 @@ export class Emitter {
123
109
  }
124
110
  }
125
111
  export class RangeProgression extends Emitter {
126
- redirect(listen) {
127
- return new RangeProgression(listen);
128
- }
129
112
  ease(easer) {
130
- if (!easer)
131
- return this;
132
113
  const easerFunc = typeof easer == "string"
133
114
  ? easers[easer]
134
115
  : easer;
135
- return new RangeProgression(easer ? (handler => this.onListen((progress) => {
136
- handler(easerFunc(progress));
137
- })) : h => this.onListen(h));
116
+ const listen = easerFunc
117
+ ? this.createTransformListen((value, emit) => emit(easerFunc(value)))
118
+ : this.onListen;
119
+ return new RangeProgression(listen);
138
120
  }
139
121
  tween(from, to) {
140
122
  const tween = createTween(from, to);
141
- return new Emitter(handler => this.onListen(progress => handler(tween(progress))));
123
+ const listen = this.createTransformListen((progress, emit) => emit(tween(progress)));
124
+ return new Emitter(listen);
142
125
  }
143
126
  /**
144
127
  * Creates a chainable emitter that takes a value from an array according to progression
@@ -154,11 +137,12 @@ export class RangeProgression extends Emitter {
154
137
  * @returns Listenable: emits the sampled values
155
138
  */
156
139
  sample(source) {
157
- return new Emitter(handler => this.onListen(progress => {
158
- const clampedProgress = clamp(progress);
140
+ const listen = this.createTransformListen((value, emit) => {
141
+ const clampedProgress = clamp(value);
159
142
  const index = Math.floor(clampedProgress * (source.length - 1));
160
- handler(source[index]);
161
- }));
143
+ emit(source[index]);
144
+ });
145
+ return new Emitter(listen);
162
146
  }
163
147
  /**
164
148
  * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
@@ -183,9 +167,8 @@ export class RangeProgression extends Emitter {
183
167
  * @returns Listenable: emits 0 or 1 after comparing progress with a threshold
184
168
  */
185
169
  threshold(threshold) {
186
- return new RangeProgression(handler => this.onListen(progress => {
187
- handler(progress >= threshold ? 1 : 0);
188
- }));
170
+ const listen = this.createTransformListen((value, emit) => emit(value >= threshold ? 1 : 0));
171
+ return new RangeProgression(listen);
189
172
  }
190
173
  /**
191
174
  * Creates a chainable progress emitter that clamps incoming values
@@ -215,11 +198,13 @@ export class RangeProgression extends Emitter {
215
198
  * @returns Listenable: emits scaled and repeating values
216
199
  */
217
200
  repeat(count) {
218
- count = Math.max(0, count);
219
- return new RangeProgression(handler => this.onListen(progress => {
220
- const out = (progress * count) % 1;
221
- handler(out);
222
- }));
201
+ if (count <= 0)
202
+ throw new RangeError("Repeat count must be greater than 0");
203
+ const listen = this.createTransformListen((value, emit) => {
204
+ const out = (value * count) % 1;
205
+ emit(out);
206
+ });
207
+ return new RangeProgression(listen);
223
208
  }
224
209
  /**
225
210
  * Creates a chainable progress emitter that selectively forwards emissions along the chain
@@ -227,25 +212,28 @@ export class RangeProgression extends Emitter {
227
212
  * @returns Listenable: emits values that pass the filter
228
213
  */
229
214
  filter(check) {
230
- return new RangeProgression(handler => this.onListen((value) => {
215
+ const listen = this.createTransformListen((value, emit) => {
231
216
  if (check(value))
232
- handler(value);
233
- }));
217
+ emit(value);
218
+ });
219
+ return new RangeProgression(listen);
234
220
  }
235
221
  /**
236
222
  * Creates a chainable progress emitter that discards emitted values that are the same as the last value emitted by the new emitter
237
223
  * @returns Listenable: emits non-repeating values
238
224
  */
239
225
  dedupe() {
240
- let previous = null;
241
- return new RangeProgression(handler => {
242
- return this.onListen((value) => {
243
- if (!previous === null || previous !== value) {
244
- handler(value);
226
+ if (!this._dedupe) {
227
+ let previous = null;
228
+ const listen = this.createTransformListen((value, emit) => {
229
+ if (previous !== value) {
230
+ emit(value);
245
231
  previous = value;
246
232
  }
247
233
  });
248
- });
234
+ this._dedupe = new RangeProgression(listen);
235
+ }
236
+ return this._dedupe;
249
237
  }
250
238
  /**
251
239
  * Creates a chainable progress emitter that offsets its parent's values by the given delta, wrapping at 1
@@ -267,11 +255,29 @@ export class RangeProgression extends Emitter {
267
255
  offset(delta) {
268
256
  return new RangeProgression(handler => this.onListen(value => handler((value + delta) % 1)));
269
257
  }
258
+ /**
259
+ * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
260
+ *
261
+ * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
262
+ * All listeners attached to the returned emitter receive the same values as the parent emitter.
263
+ *
264
+ * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
265
+ *
266
+ * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
267
+ * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
268
+ */
269
+ tap(cb) {
270
+ const listen = this.createTransformListen((value, emit) => {
271
+ cb(value);
272
+ emit(value);
273
+ });
274
+ return new RangeProgression(listen);
275
+ }
270
276
  }
271
277
  export function createListenable(onAddFirst, onRemoveLast) {
272
278
  const handlers = [];
273
279
  const addListener = (fn) => {
274
- const unique = (v) => fn(v);
280
+ const unique = { fn };
275
281
  handlers.push(unique);
276
282
  if (onAddFirst && handlers.length == 1)
277
283
  onAddFirst();
@@ -286,6 +292,6 @@ export function createListenable(onAddFirst, onRemoveLast) {
286
292
  };
287
293
  return {
288
294
  listen: addListener,
289
- emit: (value) => handlers.forEach(h => h(value)),
295
+ emit: (value) => handlers.forEach(h => h.fn(value)),
290
296
  };
291
297
  }
@@ -17,7 +17,6 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
17
17
  * The point's absolute position on the Timeline
18
18
  */
19
19
  position: number);
20
- protected redirect(listen: ListenFunc<PointEvent>): TimelinePoint;
21
20
  /**
22
21
  * Creates a range on the Timeline, with a given duration, starting at this point
23
22
  * @param duration
@@ -51,11 +50,13 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
51
50
  * @returns Listenable: emits forward-seeking point events
52
51
  */
53
52
  forwardOnly(): Emitter<PointEvent>;
53
+ private _forwardOnly?;
54
54
  /**
55
55
  * Creates an emitter that only emits on backward-moving seeks
56
56
  * @returns Listenable: emits backward-seeking point events
57
57
  */
58
58
  reverseOnly(): Emitter<PointEvent>;
59
+ private _reverseOnly?;
59
60
  filter(check: (event: PointEvent) => boolean): Emitter<PointEvent>;
60
61
  /**
61
62
  * Creates an emitter that forwards events emitted by seeks of a specific direction
@@ -96,4 +97,5 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
96
97
  * @returns Listenable: emits non-repeating point events
97
98
  */
98
99
  dedupe(): Emitter<PointEvent>;
100
+ private _dedupe?;
99
101
  }
package/internal/point.js CHANGED
@@ -10,9 +10,6 @@ export class TimelinePoint extends Emitter {
10
10
  this.timeline = timeline;
11
11
  this.position = position;
12
12
  }
13
- redirect(listen) {
14
- return new TimelinePoint(listen, this.timeline, this.position);
15
- }
16
13
  /**
17
14
  * Creates a range on the Timeline, with a given duration, starting at this point
18
15
  * @param duration
@@ -48,25 +45,30 @@ export class TimelinePoint extends Emitter {
48
45
  * @returns Listenable: emits forward-seeking point events
49
46
  */
50
47
  forwardOnly() {
51
- return this.filter(1);
48
+ if (!this._forwardOnly)
49
+ this._forwardOnly = this.filter(1);
50
+ return this._forwardOnly;
52
51
  }
53
52
  /**
54
53
  * Creates an emitter that only emits on backward-moving seeks
55
54
  * @returns Listenable: emits backward-seeking point events
56
55
  */
57
56
  reverseOnly() {
58
- return this.filter(-1);
57
+ if (!this._reverseOnly)
58
+ this._reverseOnly = this.filter(-1);
59
+ return this._reverseOnly;
59
60
  }
60
61
  filter(arg) {
61
- return new Emitter(typeof arg == "number"
62
- ? handler => this.onListen(ev => {
63
- if (ev.direction === arg)
64
- handler(ev);
65
- })
66
- : handler => this.onListen((ev) => {
67
- if (arg(ev))
68
- handler(ev);
69
- }));
62
+ const listen = this.createTransformListen(typeof arg == "number"
63
+ ? (value, emit) => {
64
+ if (value.direction === arg)
65
+ emit(value);
66
+ }
67
+ : (value, emit) => {
68
+ if (arg(value))
69
+ emit(value);
70
+ });
71
+ return new Emitter(listen);
70
72
  }
71
73
  /**
72
74
  * Creates a Promise that will be resolved when the Timeline first seeks to/past this point
@@ -112,12 +114,16 @@ export class TimelinePoint extends Emitter {
112
114
  * @returns Listenable: emits non-repeating point events
113
115
  */
114
116
  dedupe() {
115
- let previous = 0;
116
- return new Emitter(handler => this.onListen(event => {
117
- if (event.direction !== previous) {
118
- handler(event);
119
- previous = event.direction;
120
- }
121
- }));
117
+ if (!this._dedupe) {
118
+ let previous = 0;
119
+ const listen = this.createTransformListen((value, emit) => {
120
+ if (value.direction !== previous) {
121
+ previous = value.direction;
122
+ emit(value);
123
+ }
124
+ });
125
+ this._dedupe = new Emitter(listen);
126
+ }
127
+ return this._dedupe;
122
128
  }
123
129
  }
@@ -18,7 +18,6 @@ export declare class TimelineRange extends RangeProgression {
18
18
  start: TimelinePoint,
19
19
  /** The point on the Timeline at which this range ends */
20
20
  end: TimelinePoint);
21
- protected redirect(listen: ListenFunc<number>): TimelineRange;
22
21
  /**
23
22
  * Creates two ranges by seperating one at a given point
24
23
  * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
@@ -33,6 +32,18 @@ export declare class TimelineRange extends RangeProgression {
33
32
  * @returns Array(count) of points
34
33
  */
35
34
  spread(count: number): TimelinePoint[];
35
+ /**
36
+ * Creates a series of evenly-spread points across the range, optionally including the range's start and end
37
+ * @param count Number of Points to return, including head and tail
38
+ * @param includeEnds
39
+ */
40
+ spread(count: number, includeEnds: boolean): TimelinePoint[];
41
+ /**
42
+ * Creates a series of evenly-spread points across the range, optionally including the range's start and end
43
+ * @param count Number of Points to return, including head and tail
44
+ * @param includeEnds
45
+ */
46
+ spread(count: number, includeStart: boolean, includeEnd: boolean): TimelinePoint[];
36
47
  /**
37
48
  * Creates the specified number of ranges, each of `(parent.duration / count)` duration, spread
38
49
  * evenly over this range
package/internal/range.js CHANGED
@@ -15,9 +15,6 @@ export class TimelineRange extends RangeProgression {
15
15
  this.endPosition = end.position;
16
16
  this.duration = this.endPosition - this.startPosition;
17
17
  }
18
- redirect(listen) {
19
- return new TimelineRange(listen, this.timeline, this.start, this.end);
20
- }
21
18
  /**
22
19
  * Creates two ranges by seperating one at a given point
23
20
  * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
@@ -34,15 +31,26 @@ export class TimelineRange extends RangeProgression {
34
31
  this.timeline.range(position + this.startPosition, this.duration - position),
35
32
  ];
36
33
  }
37
- /**
38
- * Creates a series of evenly-spread points across the range, excluding the range's start and end
39
- * @param count Number of Points to return
40
- * @returns Array(count) of points
41
- */
42
- spread(count) {
34
+ spread(count, includeStart = false, includeEnd = includeStart) {
35
+ let start = [];
36
+ let end = [];
37
+ if (includeStart) {
38
+ start = [this.start];
39
+ count--;
40
+ }
41
+ if (includeEnd) {
42
+ end = [this.end];
43
+ count--;
44
+ }
45
+ if (count == 0)
46
+ return [...start, ...end];
47
+ if (count < 0)
48
+ throw new Error("Invalid spread count");
43
49
  const delta = this.duration / (count + 1);
44
50
  return [
45
- ...Array(count).fill(0).map((_, idx) => this.timeline.point(idx * delta + this.startPosition + delta))
51
+ ...start,
52
+ ...Array(count).fill(0).map((_, idx) => this.timeline.point(idx * delta + this.startPosition + delta)),
53
+ ...end
46
54
  ];
47
55
  }
48
56
  /**
@@ -27,9 +27,18 @@ export declare class Timeline {
27
27
  * A value of 2 would double progression speed while .25 would slow it to a quarter
28
28
  */
29
29
  timeScale: number;
30
+ /**
31
+ * The current position of this Timeline's 'play head'
32
+ */
30
33
  get currentTime(): number;
31
34
  set currentTime(v: number);
35
+ /**
36
+ * Returns true if this Timeline is currently progressing via `play()`, otherwise false
37
+ */
32
38
  get isPlaying(): boolean;
39
+ /**
40
+ * Returns a fixed point at the current end of the Timeline
41
+ */
33
42
  get end(): TimelinePoint;
34
43
  private _currentTime;
35
44
  private _endPosition;
@@ -40,6 +49,9 @@ export declare class Timeline {
40
49
  private currentSortDirection;
41
50
  private smoothSeeker;
42
51
  private seeking;
52
+ /**
53
+ * A fixed point representing the start of this Timeline (position 0)
54
+ */
43
55
  readonly start: TimelinePoint;
44
56
  private _frameEvents;
45
57
  /**
@@ -48,8 +60,8 @@ export declare class Timeline {
48
60
  apply(handler: () => void): UnsubscribeFunc;
49
61
  private _progression;
50
62
  /**
51
- * Listenable: emits a progression value (0..1) when the Timeline's internal
52
- * position changes, and when the Timeline's total duration is extended
63
+ * Listenable: emits a progression value (0..1), representing progression through the entire Timeline,
64
+ * when the Timeline's internal position changes, and when the Timeline's total duration is extended
53
65
  */
54
66
  get progression(): RangeProgression;
55
67
  constructor();
@@ -102,10 +114,9 @@ export declare class Timeline {
102
114
  */
103
115
  range(start: number | TimelinePoint, duration: number): TimelineRange;
104
116
  /**
105
- * Creates an observable range from position 0 to the Timeline's **current** final position
117
+ * Defines a range from position 0 to the Timeline's **current** final position
106
118
  */
107
119
  range(): TimelineRange;
108
- private getWrappedPosition;
109
120
  /**
110
121
  * Seeks the Timeline to a specified position, triggering in order any point and range subscriptions between its current and new positions
111
122
  * @param toPosition
@@ -123,6 +134,7 @@ export declare class Timeline {
123
134
  seek(toPosition: number | TimelinePoint, durationMs: number, easer?: Easer | keyof typeof easers): Promise<void>;
124
135
  seek(toPosition: number | TimelinePoint, duration: Period, easer?: Easer | keyof typeof easers): Promise<void>;
125
136
  private seekDirect;
137
+ private seekWrapped;
126
138
  private seekPoints;
127
139
  private seekRanges;
128
140
  private sortEntries;
@@ -154,9 +166,29 @@ export declare class Timeline {
154
166
  * @deprecated Use timeline.position += n
155
167
  */
156
168
  step(delta: number): void;
169
+ /**
170
+ * Adds a tweening range to the Timeline
171
+ *
172
+ * **Legacy API**
173
+ * @param start Range's start position
174
+ * @param duration Tween's duration
175
+ * @param apply Function to apply interpolated values
176
+ * @param from Value at start of range
177
+ * @param to Value at end of range
178
+ * @param easer Optional easing function
179
+ */
157
180
  tween<T extends Tweenable>(start: number | TimelinePoint, duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer | keyof typeof easers): ChainingInterface;
158
181
  tween<T extends Tweenable>(start: number | TimelinePoint, end: TimelinePoint, // ease migration for tl.tween(0, tl.end, ...)
159
182
  apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer | keyof typeof easers): ChainingInterface;
183
+ /**
184
+ * Adds an event at a specific position
185
+ *
186
+ * **Legacy API**
187
+ * @param position Position of the event
188
+ * @param action Handler for forward seeking
189
+ * @param reverse Handler for backward seeking
190
+ * @returns A tween/event chaining interface
191
+ */
160
192
  at(position: number | TimelinePoint, action?: () => void, reverse?: boolean | (() => void)): ChainingInterface;
161
193
  private createChainingInterface;
162
194
  /**
@@ -16,13 +16,22 @@ export function animate(durationMs) {
16
16
  : durationMs.asMilliseconds);
17
17
  }
18
18
  export class Timeline {
19
+ /**
20
+ * The current position of this Timeline's 'play head'
21
+ */
19
22
  get currentTime() { return this._currentTime; }
20
23
  set currentTime(v) {
21
24
  this.seek(v);
22
25
  }
26
+ /**
27
+ * Returns true if this Timeline is currently progressing via `play()`, otherwise false
28
+ */
23
29
  get isPlaying() {
24
30
  return this.interval !== null;
25
31
  }
32
+ /**
33
+ * Returns a fixed point at the current end of the Timeline
34
+ */
26
35
  get end() {
27
36
  return this.point(this._endPosition);
28
37
  }
@@ -40,8 +49,8 @@ export class Timeline {
40
49
  return this._frameEvents.listen(handler);
41
50
  }
42
51
  /**
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
52
+ * Listenable: emits a progression value (0..1), representing progression through the entire Timeline,
53
+ * when the Timeline's internal position changes, and when the Timeline's total duration is extended
45
54
  */
46
55
  get progression() {
47
56
  if (this._progression === null) {
@@ -68,6 +77,9 @@ export class Timeline {
68
77
  this.currentSortDirection = 0;
69
78
  this.smoothSeeker = null;
70
79
  this.seeking = false;
80
+ /**
81
+ * A fixed point representing the start of this Timeline (position 0)
82
+ */
71
83
  this.start = this.point(0);
72
84
  this._frameEvents = null;
73
85
  this._progression = null;
@@ -161,22 +173,6 @@ export class Timeline {
161
173
  const range = new TimelineRange(addHandler, this, startPoint, endPoint);
162
174
  return range;
163
175
  }
164
- getWrappedPosition(n) {
165
- if (this.endAction.type !== EndAction.wrap)
166
- return n;
167
- const wrapAt = this.endAction.at?.position ?? 0;
168
- if (wrapAt == 0)
169
- return n % this._endPosition;
170
- if (n <= this._endPosition)
171
- return n;
172
- const loopStart = wrapAt;
173
- const segment = this._endPosition - loopStart;
174
- if (segment <= 0)
175
- return Math.min(n, this._endPosition);
176
- const overflow = n - this._endPosition;
177
- const remainder = overflow % segment;
178
- return loopStart + remainder;
179
- }
180
176
  seek(to, duration, easer) {
181
177
  const durationMs = typeof duration == "object"
182
178
  ? duration.asMilliseconds
@@ -216,31 +212,65 @@ export class Timeline {
216
212
  const fromPosition = this._currentTime;
217
213
  if (toPosition === fromPosition)
218
214
  return;
219
- const loopingTo = this.getWrappedPosition(toPosition);
220
- const loopingFrom = this.getWrappedPosition(fromPosition);
221
- let virtualFrom = loopingFrom;
222
- let virtualTo = loopingTo;
223
215
  const direction = toPosition > fromPosition ? 1 : -1;
224
216
  if (direction !== this.currentSortDirection)
225
217
  this.sortEntries(direction);
226
- if (direction === 1 && loopingTo < loopingFrom) {
227
- virtualFrom = loopingFrom - this._endPosition;
228
- }
229
- else if (direction === -1 && loopingTo > loopingFrom) {
230
- virtualFrom = loopingFrom + this._endPosition;
231
- }
232
218
  this.seeking = true;
233
- this._currentTime = virtualFrom;
234
219
  try {
235
- this.seekPoints(virtualTo);
236
- this.seekRanges(virtualTo);
220
+ // use wrapping logic?
221
+ if (this.endAction.type === EndAction.wrap && (fromPosition > this._endPosition || toPosition > this._endPosition)) {
222
+ this.seekWrapped(toPosition);
223
+ }
224
+ else {
225
+ this.seekPoints(toPosition);
226
+ this.seekRanges(toPosition);
227
+ }
237
228
  }
238
229
  catch (e) {
239
230
  this.pause();
240
231
  throw e;
241
232
  }
233
+ finally {
234
+ this.seeking = false;
235
+ }
236
+ this._currentTime = toPosition;
237
+ }
238
+ seekWrapped(toPosition) {
239
+ const fromPosition = this._currentTime;
240
+ const timelineEnd = this._endPosition;
241
+ const wrapAt = "at" in this.endAction ? this.endAction.at.position : 0;
242
+ const loopLen = timelineEnd - wrapAt;
243
+ const getWrappedPosition = (pos) => ((pos - wrapAt) % loopLen + loopLen) % loopLen + wrapAt;
244
+ const realDelta = toPosition - fromPosition;
245
+ const direction = realDelta >= 0 ? 1 : -1;
246
+ let remaining = Math.abs(realDelta);
247
+ let virtualFrom = getWrappedPosition(fromPosition);
248
+ while (remaining > 0) {
249
+ let virtualTo;
250
+ if (direction > 0) {
251
+ const wrapSize = timelineEnd - virtualFrom;
252
+ virtualTo = remaining <= wrapSize
253
+ ? virtualFrom + remaining
254
+ : timelineEnd;
255
+ }
256
+ else {
257
+ const wrapSize = virtualFrom - wrapAt;
258
+ virtualTo = remaining <= wrapSize
259
+ ? virtualFrom - remaining
260
+ : wrapAt;
261
+ }
262
+ this._currentTime = virtualFrom;
263
+ this.seekPoints(virtualTo);
264
+ remaining -= Math.abs(virtualTo - virtualFrom);
265
+ if (remaining > 0) {
266
+ virtualFrom = direction > 0 ? wrapAt : timelineEnd;
267
+ }
268
+ else {
269
+ virtualFrom = virtualTo;
270
+ }
271
+ }
272
+ this.seekRanges(getWrappedPosition(toPosition));
242
273
  this._currentTime = toPosition;
243
- this.seeking = false;
244
274
  }
245
275
  seekPoints(to) {
246
276
  const from = this._currentTime;
@@ -357,6 +387,15 @@ export class Timeline {
357
387
  this.range(startPosition, duration).ease(easer).tween(from, to).apply(apply);
358
388
  return this.createChainingInterface(startPosition + duration);
359
389
  }
390
+ /**
391
+ * Adds an event at a specific position
392
+ *
393
+ * **Legacy API**
394
+ * @param position Position of the event
395
+ * @param action Handler for forward seeking
396
+ * @param reverse Handler for backward seeking
397
+ * @returns A tween/event chaining interface
398
+ */
360
399
  at(position, action, reverse) {
361
400
  const point = typeof position == "number" ? this.point(position) : position;
362
401
  if (reverse === true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"