@xtia/timeline 1.1.19 → 1.2.1

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
@@ -12,7 +12,9 @@ Timeline is a type-safe, seekable, deterministic choreography system that can co
12
12
 
13
13
  ## Basic Use:
14
14
 
15
- `npm i @xtia/timeline`
15
+ Install with `npm i @xtia/timeline`
16
+
17
+ Timeline's primary building block is a *range* that emits a normalised progression (0 -> 1) as time flows through it. It can be expressively chained to ease progression, interpolate values and apply effects.
16
18
 
17
19
  ```ts
18
20
  import { Timeline } from "@xtia/timeline";
@@ -57,12 +59,12 @@ timeline.play();
57
59
  const firstFiveSeconds = timeline.range(0, 5000);
58
60
  ```
59
61
 
60
- The range object is *applyable* and emits a progression value (between 0 and 1) when the Timeline's internal position passes through or over that period.
62
+ The range object is *applyable* and emits a progression value when the Timeline seeks through or over that period.
61
63
 
62
64
  ```ts
63
65
  firstFiveSeconds
64
66
  .apply(
65
- value => console.log(`${value} is between 0 and 1`)
67
+ value => console.log(value) // logs 0 -> 1 over 5 seconds
66
68
  );
67
69
  ```
68
70
 
@@ -104,7 +106,7 @@ range
104
106
  // each step in a chain is a 'pure', independent emitter that emits a
105
107
  // transformation of its parent's emissions
106
108
  const filenameEmitter = range
107
- .tween(0, 3)
109
+ .tween(0, 30)
108
110
  .map(Math.floor)
109
111
  .dedupe()
110
112
  .map(n => `animation-frame-${n}.png`);
@@ -221,7 +221,7 @@ export declare class RangeProgression extends Emitter<number> {
221
221
  */
222
222
  tap(cb: Handler<number>): RangeProgression;
223
223
  }
224
- export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLast?: () => void): {
224
+ export declare function createListenable<T>(sourceListen?: () => UnsubscribeFunc | undefined): {
225
225
  listen: (fn: (v: T) => void) => UnsubscribeFunc;
226
226
  emit: (value: T) => void;
227
227
  };
@@ -7,14 +7,9 @@ export class Emitter {
7
7
  this.onListen = onListen;
8
8
  }
9
9
  transform(handler) {
10
- let parentUnsubscribe = null;
11
- const parentListen = this.onListen;
12
- const { emit, listen } = createListenable(() => parentUnsubscribe = parentListen(value => {
10
+ const { emit, listen } = createListenable(() => this.onListen(value => {
13
11
  handler(value, emit);
14
- }), () => {
15
- parentUnsubscribe();
16
- parentUnsubscribe = null;
17
- });
12
+ }));
18
13
  return listen;
19
14
  }
20
15
  /**
@@ -246,21 +241,14 @@ export class RangeProgression extends Emitter {
246
241
  let parentUnsubscribe = null;
247
242
  let pathUnsubscribe = null;
248
243
  const { listen, emit } = createListenable(() => {
249
- // onAddFirst - when first listener subscribes
250
244
  pathUnsubscribe = pathEvaluator.listen(emit);
251
245
  parentUnsubscribe = this.listen((timeValue) => {
252
246
  pathEvaluator.seek(timeValue);
253
247
  });
254
- }, () => {
255
- // onRemoveLast - when last listener unsubscribes
256
- if (pathUnsubscribe) {
248
+ return () => {
257
249
  pathUnsubscribe();
258
- pathUnsubscribe = null;
259
- }
260
- if (parentUnsubscribe) {
261
250
  parentUnsubscribe();
262
- parentUnsubscribe = null;
263
- }
251
+ };
264
252
  });
265
253
  return new Emitter(listen);
266
254
  }
@@ -303,13 +291,14 @@ export class RangeProgression extends Emitter {
303
291
  return new RangeProgression(listen);
304
292
  }
305
293
  }
306
- export function createListenable(onAddFirst, onRemoveLast) {
294
+ export function createListenable(sourceListen) {
307
295
  const handlers = [];
296
+ let onRemoveLast;
308
297
  const addListener = (fn) => {
309
298
  const unique = { fn };
310
299
  handlers.push(unique);
311
- if (onAddFirst && handlers.length == 1)
312
- onAddFirst();
300
+ if (sourceListen && handlers.length == 1)
301
+ onRemoveLast = sourceListen();
313
302
  return () => {
314
303
  const idx = handlers.indexOf(unique);
315
304
  if (idx === -1)
@@ -20,6 +20,14 @@ type Period = {
20
20
  */
21
21
  export declare function animate(durationMs: number): TimelineRange;
22
22
  export declare function animate(period: Period): TimelineRange;
23
+ /**
24
+ * Creates a looping Timeline and returns a range from it
25
+ *
26
+ * This timeline will play while it has active listeners
27
+ * @param duration Animation duration, in milliseconds, or a Period
28
+ * @returns Object representing a range on a single-use, autoplaying Timeline
29
+ */
30
+ export declare function animate(duration: number | Period, looping: true): TimelineRange;
23
31
  type TimelineOptions = {
24
32
  atEnd?: {
25
33
  wrapAt: number;
@@ -27,13 +35,8 @@ type TimelineOptions = {
27
35
  restartAt: number;
28
36
  } | keyof typeof EndAction;
29
37
  timeScale?: number;
30
- } & ({
31
- autoplay: true;
32
- fps?: number;
33
- } | ({
34
- autoplay?: false;
35
- fps?: never;
36
- }));
38
+ autoplay?: boolean;
39
+ };
37
40
  export declare class Timeline {
38
41
  /**
39
42
  * Multiplies the speed at which `play()` progresses through the Timeline
@@ -83,11 +86,6 @@ export declare class Timeline {
83
86
  * @param autoplay Pass `true` to begin playing at (1000 × this.timeScale) units per second immediately on creation
84
87
  */
85
88
  constructor(autoplay: boolean);
86
- /**
87
- * Creates a Timeline that begins playing immediately at (1000 × this.timeScale) units per second
88
- * @param autoplayFps Specifies frames per second
89
- */
90
- constructor(autoplayFps: number);
91
89
  /**
92
90
  * @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
93
91
  * @param endAction Specifies what should happen when the final position is passed by `play()`/`autoplay`
@@ -99,7 +97,7 @@ export declare class Timeline {
99
97
  * * `{restartAt: number}`: Like `"restart"` but seeking back to `restartAt` instead of 0
100
98
  * * `{wrapAt: number}`: Like `"wrap"` but as if restarting at `wrapAt` instead of 0
101
99
  */
102
- constructor(autoplay: boolean | number, endAction: {
100
+ constructor(autoplay: boolean, endAction: {
103
101
  wrapAt: number;
104
102
  } | {
105
103
  restartAt: number;
@@ -166,13 +164,10 @@ export declare class Timeline {
166
164
  * Starts progression of the Timeline from its current position at (1000 × this.timeScale) units per second
167
165
  */
168
166
  play(): void;
169
- play(fps: number): void;
170
167
  /**
171
168
  * Performs a smooth-seek through a range at (1000 × this.timeScale) units per second
172
169
  */
173
170
  play(range: TimelineRange, easer?: Easer): Promise<void>;
174
- private playWithInterval;
175
- private playWithRAF;
176
171
  private next;
177
172
  /**
178
173
  * Stops normal progression instigated by play()
@@ -3,38 +3,52 @@ import { TimelinePoint } from "./point.js";
3
3
  import { TimelineRange } from "./range.js";
4
4
  import { clamp } from "./utils.js";
5
5
  const default_interval_fps = 60;
6
- const requestAnimFrame = globalThis?.requestAnimationFrame;
7
- const cancelAnimFrame = globalThis?.cancelAnimationFrame;
8
- const rafController = (() => {
9
- const timelines = new Map();
6
+ const createRafDriver = (tick) => {
10
7
  let rafId = null;
11
- const start = () => {
12
- let previousTime = null;
13
- const frame = (currentTime) => {
14
- if (previousTime === null) {
15
- previousTime = currentTime;
16
- }
17
- const elapsed = currentTime - previousTime;
18
- previousTime = currentTime;
19
- timelines.forEach((step, tl) => {
20
- const delta = elapsed * tl.timeScale;
21
- step(delta);
22
- });
23
- rafId = requestAnimFrame(frame);
8
+ return () => {
9
+ const frame = (ts) => {
10
+ tick(ts);
11
+ rafId = requestAnimationFrame(frame);
24
12
  };
25
- rafId = requestAnimFrame(frame);
13
+ rafId = requestAnimationFrame(frame);
14
+ return () => cancelAnimationFrame(rafId);
15
+ };
16
+ };
17
+ const createIntervalDriver = (tick) => {
18
+ return () => {
19
+ const intervalId = setInterval(() => tick(performance.now()), 1000 / default_interval_fps);
20
+ return () => clearInterval(intervalId);
21
+ };
22
+ };
23
+ const masterDriver = (() => {
24
+ const timelines = new Map();
25
+ let previousTime = null;
26
+ let pause = null;
27
+ const step = (currentTime) => {
28
+ if (previousTime === null) {
29
+ previousTime = currentTime;
30
+ }
31
+ const delta = currentTime - previousTime;
32
+ previousTime = currentTime;
33
+ timelines.forEach((step, tl) => {
34
+ step(delta * tl.timeScale);
35
+ });
26
36
  };
37
+ const start = "requestAnimationFrame" in globalThis
38
+ ? createRafDriver(step)
39
+ : createIntervalDriver(step);
27
40
  return {
28
41
  add: (timeline, stepFn) => {
29
42
  timelines.set(timeline, stepFn);
30
- if (rafId === null)
31
- start();
43
+ if (timelines.size === 1) {
44
+ previousTime = null;
45
+ pause = start();
46
+ }
32
47
  },
33
48
  remove: (timeline) => {
34
49
  timelines.delete(timeline);
35
50
  if (timelines.size === 0) {
36
- cancelAnimFrame(rafId);
37
- rafId = null;
51
+ pause();
38
52
  }
39
53
  }
40
54
  };
@@ -45,11 +59,31 @@ const EndAction = {
45
59
  wrap: 2,
46
60
  restart: 3,
47
61
  };
48
- export function animate(durationMs) {
49
- return new Timeline(true)
50
- .range(0, typeof durationMs == "number"
51
- ? durationMs
52
- : durationMs.asMilliseconds);
62
+ export function animate(duration, looping = false) {
63
+ const tl = new Timeline(false, looping ? "wrap" : "pause");
64
+ const durationMs = typeof duration == "number"
65
+ ? duration
66
+ : duration.asMilliseconds;
67
+ const parentRange = tl.range(0, durationMs).ease();
68
+ if (looping) {
69
+ let listeners = 0;
70
+ const range = new TimelineRange(h => {
71
+ if (++listeners == 1) {
72
+ tl.play();
73
+ }
74
+ let unsub = parentRange.apply(h);
75
+ return () => {
76
+ if (--listeners == 0)
77
+ tl.pause();
78
+ unsub();
79
+ };
80
+ }, tl, tl.start, tl.point(durationMs));
81
+ return range;
82
+ }
83
+ else {
84
+ tl.play();
85
+ return parentRange;
86
+ }
53
87
  }
54
88
  export class Timeline {
55
89
  /**
@@ -125,21 +159,12 @@ export class Timeline {
125
159
  if (typeof optionsOrAutoplay == "object") {
126
160
  endAction = optionsOrAutoplay.atEnd ?? "pause";
127
161
  this.timeScale = optionsOrAutoplay.timeScale ?? 1;
128
- if ("autoplay" in optionsOrAutoplay && optionsOrAutoplay.autoplay) {
129
- if ("fps" in optionsOrAutoplay && optionsOrAutoplay.fps) {
130
- this.play(optionsOrAutoplay.fps);
131
- }
132
- else {
133
- this.play();
134
- }
135
- }
162
+ if (optionsOrAutoplay.autoplay)
163
+ this.play();
136
164
  }
137
165
  else if (optionsOrAutoplay === true) {
138
166
  this.play();
139
167
  }
140
- else if (typeof optionsOrAutoplay == "number") {
141
- this.play(optionsOrAutoplay);
142
- }
143
168
  if (typeof endAction == "object"
144
169
  && "restartAt" in endAction) {
145
170
  this.endAction = {
@@ -173,9 +198,12 @@ export class Timeline {
173
198
  this._endPosition = position;
174
199
  this._progression?.emit(this._currentTime / position);
175
200
  }
176
- const { emit, listen } = createListenable(() => this.points.push(data), () => {
177
- const idx = this.points.indexOf(data);
178
- this.points.splice(idx, 1);
201
+ const { emit, listen } = createListenable(() => {
202
+ this.points.push(data);
203
+ return () => {
204
+ const idx = this.points.indexOf(data);
205
+ this.points.splice(idx, 1);
206
+ };
179
207
  });
180
208
  const addHandler = (handler) => {
181
209
  if (this.seeking)
@@ -200,9 +228,12 @@ export class Timeline {
200
228
  const startPosition = startPoint.position;
201
229
  const duration = optionalDuration ?? this._endPosition - startPosition;
202
230
  const endPoint = this.point(startPosition + duration);
203
- const { emit, listen } = createListenable(() => this.ranges.push(rangeData), () => {
204
- const idx = this.ranges.indexOf(rangeData);
205
- this.ranges.splice(idx, 1);
231
+ const { emit, listen } = createListenable(() => {
232
+ this.ranges.push(rangeData);
233
+ return () => {
234
+ const idx = this.ranges.indexOf(rangeData);
235
+ this.ranges.splice(idx, 1);
236
+ };
206
237
  });
207
238
  const rangeData = {
208
239
  position: startPosition,
@@ -380,26 +411,8 @@ export class Timeline {
380
411
  this.seek(arg.start);
381
412
  return this.seek(arg.end, arg.duration / this.timeScale, easer);
382
413
  }
383
- if (arg === undefined && requestAnimFrame) {
384
- this.playWithRAF();
385
- return;
386
- }
387
- this.playWithInterval(arg ?? default_interval_fps);
388
- }
389
- playWithInterval(fps) {
390
- let previousTime = performance.now();
391
- const interval = setInterval(() => {
392
- const newTime = performance.now();
393
- const elapsed = newTime - previousTime;
394
- previousTime = newTime;
395
- let delta = elapsed * this.timeScale;
396
- this.next(delta);
397
- }, 1000 / fps);
398
- this._pause = () => clearInterval(interval);
399
- }
400
- playWithRAF() {
401
- rafController.add(this, n => this.next(n));
402
- this._pause = () => rafController.remove(this);
414
+ masterDriver.add(this, n => this.next(n));
415
+ this._pause = () => masterDriver.remove(this);
403
416
  }
404
417
  next(delta) {
405
418
  if (this._currentTime + delta <= this._endPosition) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.1.19",
3
+ "version": "1.2.1",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"