@xtia/timeline 1.1.1 → 1.1.3

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  ### Not Just Another Animation Library
4
4
 
5
- Timeline is a type-safe, 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.
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")
@@ -40,7 +40,7 @@ timeline
40
40
  timeline
41
41
  .range(1000, 2000)
42
42
  .tween(0, 255)
43
- .listen(value => microcontroller.setPWM(value))
43
+ .apply(value => microcontroller.setPWM(value))
44
44
 
45
45
  // make it go
46
46
  timeline.play();
@@ -510,8 +510,8 @@ Registers an emission handler that calls one function for forward seeks to or pa
510
510
  ```ts
511
511
  point
512
512
  .applyDirectional(
513
- element.classList.add("faded"),
514
- element.classList.remove("faded"),
513
+ () => element.classList.add("faded"),
514
+ () => element.classList.remove("faded"),
515
515
  );
516
516
  ```
517
517
 
@@ -38,19 +38,31 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
38
38
  delta(timeOffset: number): TimelinePoint;
39
39
  /**
40
40
  * Seeks the parent Timeline to this point
41
+ * @deprecated Use timeline.seek(point)
41
42
  */
42
43
  seek(): void;
44
+ /**
45
+ * Smooth-seeks the parent Timeline to this point
46
+ * @deprecated Use timeline.seek(point)
47
+ */
43
48
  seek(duration: number, easer?: Easer): Promise<void>;
44
49
  /**
45
50
  * Creates an emitter that only emits on forward-moving seeks
46
- * @returns
51
+ * @returns Listenable: emits forward-seeking point events
47
52
  */
48
53
  forwardOnly(): Emitter<PointEvent>;
49
54
  /**
50
55
  * Creates an emitter that only emits on backward-moving seeks
51
- * @returns
56
+ * @returns Listenable: emits backward-seeking point events
52
57
  */
53
58
  reverseOnly(): Emitter<PointEvent>;
59
+ filter(check: (event: PointEvent) => boolean): Emitter<PointEvent>;
60
+ /**
61
+ * Creates an emitter that forwards events emitted by seeks of a specific direction
62
+ * @param allow Direction to allow
63
+ * @returns Listenable: emits point events that match the given direction
64
+ */
65
+ filter(allow: -1 | 1): Emitter<PointEvent>;
54
66
  /**
55
67
  * Creates a Promise that will be resolved when the Timeline first seeks to/past this point
56
68
  *
@@ -66,8 +78,8 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
66
78
  * ```
67
79
  * point
68
80
  * .applyDirectional(
69
- * element.classList.add("faded"),
70
- * element.classList.remove("faded"),
81
+ * () => element.classList.add("faded"),
82
+ * () => element.classList.remove("faded"),
71
83
  * );
72
84
  * ```
73
85
  *
@@ -79,4 +91,9 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
79
91
  * @returns A function to deregister both handlers
80
92
  */
81
93
  applyDirectional(apply: () => void, revert: () => void): UnsubscribeFunc;
94
+ /**
95
+ * Creates an emitter that forwards point events whose direction differs from the previous emission
96
+ * @returns Listenable: emits non-repeating point events
97
+ */
98
+ dedupe(): Emitter<PointEvent>;
82
99
  }
package/internal/point.js CHANGED
@@ -43,24 +43,30 @@ export class TimelinePoint extends Emitter {
43
43
  }
44
44
  /**
45
45
  * Creates an emitter that only emits on forward-moving seeks
46
- * @returns
46
+ * @returns Listenable: emits forward-seeking point events
47
47
  */
48
48
  forwardOnly() {
49
- return new Emitter(handler => {
50
- return this.onListen((ev) => {
51
- if (ev.direction > 0)
52
- handler(ev);
53
- });
54
- });
49
+ return this.filter(1);
55
50
  }
56
51
  /**
57
52
  * Creates an emitter that only emits on backward-moving seeks
58
- * @returns
53
+ * @returns Listenable: emits backward-seeking point events
59
54
  */
60
55
  reverseOnly() {
56
+ return this.filter(-1);
57
+ }
58
+ filter(arg) {
59
+ if (typeof arg == "number") {
60
+ return new Emitter(handler => {
61
+ return this.onListen((ev) => {
62
+ if (ev.direction === arg)
63
+ handler(ev);
64
+ });
65
+ });
66
+ }
61
67
  return new Emitter(handler => {
62
68
  return this.onListen((ev) => {
63
- if (ev.direction < 0)
69
+ if (arg(ev))
64
70
  handler(ev);
65
71
  });
66
72
  });
@@ -87,8 +93,8 @@ export class TimelinePoint extends Emitter {
87
93
  * ```
88
94
  * point
89
95
  * .applyDirectional(
90
- * element.classList.add("faded"),
91
- * element.classList.remove("faded"),
96
+ * () => element.classList.add("faded"),
97
+ * () => element.classList.remove("faded"),
92
98
  * );
93
99
  * ```
94
100
  *
@@ -104,4 +110,17 @@ export class TimelinePoint extends Emitter {
104
110
  ? apply()
105
111
  : revert());
106
112
  }
113
+ /**
114
+ * Creates an emitter that forwards point events whose direction differs from the previous emission
115
+ * @returns Listenable: emits non-repeating point events
116
+ */
117
+ dedupe() {
118
+ let previous = 0;
119
+ return new Emitter(handler => this.onListen(event => {
120
+ if (event.direction !== previous) {
121
+ handler(event);
122
+ previous = event.direction;
123
+ }
124
+ }));
125
+ }
107
126
  }
@@ -48,6 +48,7 @@ export declare class TimelineRange extends RangeProgression {
48
48
  * Progresses the Timeline across the range at 1000 units per second
49
49
  * @param easer Optional easing function
50
50
  * @returns Promise, resolved when the end is reached
51
+ * @deprecated Use timeline.play(range, easer?)
51
52
  */
52
53
  play(easer?: Easer | keyof typeof easers): Promise<void>;
53
54
  /**
package/internal/range.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { RangeProgression } from "./emitters";
2
2
  import { TimelinePoint } from "./point";
3
- import { clamp } from "./utils";
4
3
  export class TimelineRange extends RangeProgression {
5
4
  /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
6
5
  constructor(onListen, timeline, startPosition,
@@ -28,8 +27,8 @@ export class TimelineRange extends RangeProgression {
28
27
  */
29
28
  bisect(position = this.duration / 2) {
30
29
  return [
31
- this.timeline.range(position, this.startPosition),
32
- this.timeline.range(position + this.startPosition, this.duration - this.startPosition),
30
+ this.timeline.range(this.startPosition, position),
31
+ this.timeline.range(position + this.startPosition, this.duration - position),
33
32
  ];
34
33
  }
35
34
  /**
@@ -65,11 +64,12 @@ export class TimelineRange extends RangeProgression {
65
64
  * Progresses the Timeline across the range at 1000 units per second
66
65
  * @param easer Optional easing function
67
66
  * @returns Promise, resolved when the end is reached
67
+ * @deprecated Use timeline.play(range, easer?)
68
68
  */
69
69
  play(easer) {
70
70
  this.timeline.pause();
71
71
  this.timeline.currentTime = this.startPosition;
72
- return this.timeline.seek(this.startPosition + this.duration, this.duration, easer);
72
+ return this.timeline.seek(this.end, this.duration, easer);
73
73
  }
74
74
  /**
75
75
  * Creates a new range representing a direct expansion of this one
@@ -78,16 +78,7 @@ export class TimelineRange extends RangeProgression {
78
78
  * @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
79
79
  */
80
80
  grow(delta, anchor = 0) {
81
- const clampedAnchor = clamp(anchor, 0, 1);
82
- const leftDelta = -delta * (1 - clampedAnchor);
83
- const rightDelta = delta * clampedAnchor;
84
- const newStart = this.startPosition + leftDelta;
85
- const newEnd = this.startPosition + this.duration + rightDelta;
86
- if (newEnd < newStart) {
87
- const mid = (newStart + newEnd) / 2;
88
- return this.timeline.range(mid, 0);
89
- }
90
- return this.timeline.range(newStart, newEnd - newStart);
81
+ return this.timeline.range(this.startPosition - (delta * anchor), this.duration + delta);
91
82
  }
92
83
  /**
93
84
  * Creates a new range representing a multiplicative expansion of this one
@@ -99,16 +90,7 @@ export class TimelineRange extends RangeProgression {
99
90
  if (factor <= 0) {
100
91
  throw new RangeError('Scale factor must be > 0');
101
92
  }
102
- const clampedAnchor = clamp(anchor, 0, 1);
103
- const oldLen = this.endPosition - this.startPosition;
104
- const pivot = this.startPosition + oldLen * clampedAnchor;
105
- const newStart = pivot - (pivot - this.startPosition) * factor;
106
- const newEnd = pivot + (this.endPosition - pivot) * factor;
107
- if (newEnd < newStart) {
108
- const mid = (newStart + newEnd) / 2;
109
- return this.timeline.range(mid, 0);
110
- }
111
- return this.timeline.range(newStart, newEnd - newStart);
93
+ return this.grow((factor - 1) * this.duration, anchor);
112
94
  }
113
95
  contains(target) {
114
96
  const [targetStart, targetEnd] = target instanceof TimelinePoint
@@ -89,12 +89,12 @@ export declare class Timeline {
89
89
  * Defines a range on this Timeline
90
90
  *
91
91
  * @param start The position on this Timeline at which the range starts
92
- * @param duration Length of the resulting range - if omitted, the range will end at the Timeline's **current** final position
92
+ * @param duration Length of the resulting range
93
93
  * @returns A range on the Timeline
94
94
  *
95
95
  * Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
96
96
  */
97
- range(start: number | TimelinePoint, duration?: number): TimelineRange;
97
+ range(start: number | TimelinePoint, duration: number): TimelineRange;
98
98
  /**
99
99
  * Creates an observable range from position 0 to the Timeline's **current** final position
100
100
  */
@@ -124,6 +124,16 @@ export declare class Timeline {
124
124
  */
125
125
  play(): void;
126
126
  play(fps: number): void;
127
+ /**
128
+ * Performs a smooth-seek through a range at (1000 × this.timeScale) units per second
129
+ */
130
+ play(range: TimelineRange, easer?: Easer): Promise<void>;
131
+ /**
132
+ * Stops normal progression instigated by play()
133
+ *
134
+ * Does not affect ongoing smooth-seek operations or play(range)
135
+ *
136
+ */
127
137
  pause(): void;
128
138
  /**
129
139
  * Progresses the Timeline by 1 unit
@@ -148,9 +158,10 @@ export declare class Timeline {
148
158
  get position(): number;
149
159
  }
150
160
  export interface ChainingInterface {
151
- thenTween<T extends Tweenable>(duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer: Easer): ChainingInterface;
161
+ thenTween<T extends Tweenable>(duration: number, apply: (v: Widen<T>) => void, from: T, to: T, easer?: Easer): ChainingInterface;
152
162
  then(action: () => void): ChainingInterface;
153
163
  thenWait(duration: number): ChainingInterface;
164
+ fork(fn: (chain: ChainingInterface) => void): ChainingInterface;
154
165
  readonly end: TimelinePoint;
155
166
  }
156
167
  export {};
@@ -179,12 +179,12 @@ export class Timeline {
179
179
  ? to
180
180
  : to.position;
181
181
  if (this.seeking) {
182
- throw new Error("Can't seek while seeking");
182
+ throw new Error("Can't seek while a seek event is processed");
183
183
  }
184
184
  if (this.smoothSeeker !== null) {
185
185
  this.smoothSeeker.pause();
186
186
  // ensure any awaits are resolved for the previous seek
187
- this.smoothSeeker.end.seek();
187
+ this.smoothSeeker.seek(this.smoothSeeker.end);
188
188
  this.smoothSeeker = null;
189
189
  }
190
190
  if (duration === 0) {
@@ -193,8 +193,12 @@ export class Timeline {
193
193
  }
194
194
  const seeker = new Timeline(true);
195
195
  this.smoothSeeker = seeker;
196
- seeker.range(0, duration).ease(easer).tween(this.currentTime, toPosition).apply(v => this.seekDirect(v));
197
- return new Promise(r => seeker.end.apply(() => r()));
196
+ seeker
197
+ .range(0, duration)
198
+ .ease(easer)
199
+ .tween(this.currentTime, toPosition)
200
+ .apply(v => this.seekDirect(v));
201
+ return seeker.end.promise();
198
202
  }
199
203
  seekDirect(toPosition) {
200
204
  const fromPosition = this._currentTime;
@@ -252,7 +256,7 @@ export class Timeline {
252
256
  range.handlers.slice().forEach(h => h(progress));
253
257
  }
254
258
  });
255
- this.progressionHandlers.slice().forEach(h => h(fromTime / this._endPosition));
259
+ this.progressionHandlers.slice().forEach(h => h(toTime / this._endPosition));
256
260
  }
257
261
  sortEntries(direction) {
258
262
  this.currentSortDirection = direction;
@@ -263,9 +267,18 @@ export class Timeline {
263
267
  ? sortTweens
264
268
  : sortReverse);
265
269
  }
266
- play(fps = default_fps) {
270
+ play(arg = default_fps, easer) {
267
271
  if (this.interval !== null)
268
272
  this.pause();
273
+ if (this.smoothSeeker) {
274
+ this.smoothSeeker.pause();
275
+ this.smoothSeeker.seek(this.smoothSeeker.end);
276
+ this.smoothSeeker = null;
277
+ }
278
+ if (arg instanceof TimelineRange) {
279
+ this.seek(arg.start);
280
+ return this.seek(arg.end, arg.duration / this.timeScale, easer);
281
+ }
269
282
  let previousTime = Date.now();
270
283
  this.interval = setInterval(() => {
271
284
  const newTime = Date.now();
@@ -303,8 +316,14 @@ export class Timeline {
303
316
  return;
304
317
  }
305
318
  this.currentTime += delta;
306
- }, 1000 / fps);
319
+ }, 1000 / arg);
307
320
  }
321
+ /**
322
+ * Stops normal progression instigated by play()
323
+ *
324
+ * Does not affect ongoing smooth-seek operations or play(range)
325
+ *
326
+ */
308
327
  pause() {
309
328
  if (this.interval === null)
310
329
  return;
@@ -330,12 +349,12 @@ export class Timeline {
330
349
  reverse = action;
331
350
  if (action)
332
351
  point.apply(reverse
333
- ? (event => event.direction < 0 ? reverse() : action)
352
+ ? (event => event.direction < 0 ? reverse() : action())
334
353
  : action);
335
354
  return this.createChainingInterface(point.position);
336
355
  }
337
356
  createChainingInterface(position) {
338
- return {
357
+ const chain = {
339
358
  thenTween: (duration, apply, from, to, easer) => {
340
359
  return this.tween(position, duration, apply, from, to, easer);
341
360
  },
@@ -344,8 +363,13 @@ export class Timeline {
344
363
  this.point(position + delay);
345
364
  return this.createChainingInterface(position + delay);
346
365
  },
366
+ fork: fn => {
367
+ fn(chain);
368
+ return chain;
369
+ },
347
370
  end: this.point(position),
348
371
  };
372
+ return chain;
349
373
  }
350
374
  /**
351
375
  * @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.1",
3
+ "version": "1.1.3",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"