@xtia/timeline 1.0.2 → 1.0.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
@@ -94,6 +94,7 @@ const frames = eased
94
94
  .tween(0, 30)
95
95
  .map(Math.floor)
96
96
  .noRepeat()
97
+ .tap(n => console.log("Showing frame #", n))
97
98
  .map(n => `animation-frame-${n}.png`)
98
99
  .listen(filename => img.src = filename);
99
100
  ```
@@ -231,9 +232,10 @@ timeline.currentTime += 500;
231
232
  Seeking lets us control a Timeline with anything:
232
233
 
233
234
  ```ts
234
- // syncronise with a video, to show subtitles or related
235
+ // synchronise with a video, to show subtitles or related
235
236
  // activities:
236
237
  videoElement.addEventListener(
238
+ "timeupdate",
237
239
  () => timeline.seek(videoElement.currentTime)
238
240
  );
239
241
 
package/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Timeline, animate, ChainingInterface } from "./internal/timeline";
2
2
  export { TimelinePoint, PointEvent } from "./internal/point";
3
- export { RangeProgression, TimelineRange } from "./internal/range";
4
- export { Emitter, UnsubscribeFunc } from "./internal/emitters";
3
+ export { TimelineRange } from "./internal/range";
4
+ export { Emitter, RangeProgression, UnsubscribeFunc } from "./internal/emitters";
5
5
  export { easers } from "./internal/easing";
@@ -1,4 +1,5 @@
1
- import { RangeProgression } from "./range";
1
+ import { Easer, easers } from "./easing";
2
+ import { Blendable } from "./tween";
2
3
  /** @internal */
3
4
  export declare function createEmitter<T>(onListen: (handler: Handler<T>) => UnsubscribeFunc): Emitter<T>;
4
5
  /** @internal */
@@ -49,4 +50,104 @@ export interface Emitter<T> {
49
50
  */
50
51
  tap(cb: Handler<T>): Emitter<T>;
51
52
  }
53
+ export interface RangeProgression extends Emitter<number> {
54
+ /**
55
+ * Creates a chainable progress emitter that applies an easing function to its parent's emitted values
56
+ *
57
+ * @param easer An easing function of the form `(progression: number) => number`
58
+ * @returns Listenable: emits eased progression values
59
+ */
60
+ ease(easer?: Easer | keyof typeof easers): RangeProgression;
61
+ /**
62
+ * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
63
+ *
64
+ * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
65
+ *
66
+ * @param from Value to interpolate from
67
+ * @param to Value to interpolate to
68
+ * @returns Listenable: emits interpolated values
69
+ */
70
+ tween(from: number, to: number): Emitter<number>;
71
+ /**
72
+ * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
73
+ *
74
+ * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
75
+ *
76
+ * #### String interpolation
77
+ * * If the strings contain tweenable tokens (numbers, colour codes) and are otherwise identical, those tokens are interpolated
78
+ * * Otherwise the `from` string is progressively replaced, left-to-right, with the `to` string
79
+ *
80
+ * eg
81
+ * ```ts
82
+ * range
83
+ * .tween("0px 0px 0px #0000", "4px 4px 8px #0005")
84
+ * .listen(s => element.style.textShadow = s);
85
+ * ```
86
+ *
87
+ * @param from Value to interpolate from
88
+ * @param to Value to interpolate to
89
+ * @returns Listenable: emits interpolated values
90
+ */
91
+ tween(from: string, to: string): Emitter<string>;
92
+ /**
93
+ * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
94
+ *
95
+ * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
96
+ *
97
+ * @param from Value to interpolate from
98
+ * @param to Value to interpolate to
99
+ * @returns Listenable: emits interpolated values
100
+ */
101
+ tween<T extends Blendable | number[]>(from: T, to: T): Emitter<T>;
102
+ /**
103
+ * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
104
+ *
105
+ * @param steps – positive integer (e.g. 10 → 0, .1, .2 … 1)
106
+ * @throws RangeError if steps is not a positive integer
107
+ * @returns Listenable: emits quantised progression values
108
+ */
109
+ snap(steps: number): RangeProgression;
110
+ /**
111
+ * Creates a chainable progress emitter that emits `1` when the incoming progress value is greater‑than‑or‑equal to the supplied `threshold`, otherwise emits `0`
112
+ *
113
+ * @param threshold the cut‑off value
114
+ * @returns Listenable: emits 0 or 1 after comparing progress with a threshold
115
+ */
116
+ threshold(threshold: number): RangeProgression;
117
+ /**
118
+ * Creates a chainable progress emitter that clamps incoming values
119
+ * @param min default 0
120
+ * @param max default 1
121
+ * @returns Listenable: emits clamped progression values
122
+ */
123
+ clamp(min?: number, max?: number): RangeProgression;
124
+ /**
125
+ * Creates a chainable progress emitter that maps incoming values to a repeating linear scale
126
+ * @param count Number of repetitions
127
+ */
128
+ repeat(count: number): RangeProgression;
129
+ /**
130
+ * Creates a chainable progress emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
131
+ *
132
+ * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
133
+ * All listeners attached to the returned emitter receive the same values as the parent emitter.
134
+ *
135
+ * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
136
+ *
137
+ * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
138
+ * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
139
+ */
140
+ tap(cb: (value: number) => void): RangeProgression;
141
+ /**
142
+ * Creates a chainable progress emitter that selectively forwards emissions along the chain
143
+ * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
144
+ * @returns Listenable: emits values that pass the filter
145
+ */
146
+ filter(check: (value: number) => boolean): RangeProgression;
147
+ /**
148
+ * Creates a chainable progress emitter that discards emitted values that are the same as the last value emitted by the new emitter
149
+ * @returns Listenable: emits non-repeating values
150
+ */
151
+ noRepeat(): RangeProgression;
152
+ }
52
153
  export {};
@@ -8,29 +8,16 @@ const utils_1 = require("./utils");
8
8
  /** @internal */
9
9
  function createEmitter(onListen, api) {
10
10
  const propertyDescriptor = Object.fromEntries(Object.entries({
11
- listen: (handler) => {
12
- const uniqueHandler = (value) => {
11
+ listen: (handler) => onListen((value) => {
12
+ handler(value);
13
+ }),
14
+ map: (mapFunc) => createEmitter(handler => onListen((value) => {
15
+ handler(mapFunc(value));
16
+ })),
17
+ filter: (filterFunc) => createEmitter(handler => onListen((value) => {
18
+ if (filterFunc(value))
13
19
  handler(value);
14
- };
15
- return onListen(uniqueHandler);
16
- },
17
- map: (mapFunc) => {
18
- return createEmitter(handler => {
19
- const pipedHandler = (value) => {
20
- handler(mapFunc(value));
21
- };
22
- return onListen(pipedHandler);
23
- });
24
- },
25
- filter: (filterFunc) => {
26
- return createEmitter(handler => {
27
- const filteredHandler = (value) => {
28
- if (filterFunc(value))
29
- handler(value);
30
- };
31
- return onListen(filteredHandler);
32
- });
33
- },
20
+ })),
34
21
  noRepeat: (compare) => {
35
22
  let previous = null;
36
23
  return createEmitter(handler => {
@@ -45,27 +32,7 @@ function createEmitter(onListen, api) {
45
32
  return onListen(filteredHandler);
46
33
  });
47
34
  },
48
- tap: (cb) => {
49
- let listeners = [];
50
- let parentUnsubscribe = null;
51
- const tapOnListen = (handler) => {
52
- listeners.push(handler);
53
- if (listeners.length === 1) {
54
- parentUnsubscribe = onListen(value => {
55
- cb(value);
56
- listeners.slice().forEach(fn => fn(value));
57
- });
58
- }
59
- return () => {
60
- listeners = listeners.filter(l => l !== handler);
61
- if (listeners.length === 0 && parentUnsubscribe) {
62
- parentUnsubscribe();
63
- parentUnsubscribe = null;
64
- }
65
- };
66
- };
67
- return createEmitter(tapOnListen);
68
- },
35
+ tap: (cb) => createEmitter(createTapListener(cb, onListen)),
69
36
  }).map(([key, value]) => [
70
37
  key,
71
38
  { value }
@@ -104,9 +71,47 @@ function createProgressEmitter(onListen, api = {}) {
104
71
  handler(out);
105
72
  }));
106
73
  },
74
+ tap: (cb) => createProgressEmitter(createTapListener(cb, onListen)),
75
+ filter: (filterFunc) => createProgressEmitter(handler => onListen((value) => {
76
+ if (filterFunc(value))
77
+ handler(value);
78
+ })),
79
+ noRepeat: () => {
80
+ let previous = null;
81
+ return createProgressEmitter(handler => {
82
+ return onListen((value) => {
83
+ if (!previous || (previous.value !== value)) {
84
+ handler(value);
85
+ previous = { value };
86
+ }
87
+ });
88
+ });
89
+ },
107
90
  }).map(([key, value]) => [
108
91
  key,
109
92
  { value }
110
93
  ]));
111
94
  return createEmitter(onListen, Object.create(api, propertyDescriptor));
112
95
  }
96
+ function createTapListener(callback, parentOnListen) {
97
+ const listeners = [];
98
+ let parentUnsubscribe = null;
99
+ const tapOnListen = (handler) => {
100
+ listeners.push(handler);
101
+ if (listeners.length === 1) {
102
+ parentUnsubscribe = parentOnListen(value => {
103
+ callback(value);
104
+ listeners.slice().forEach(fn => fn(value));
105
+ });
106
+ }
107
+ return () => {
108
+ const idx = listeners.indexOf(handler);
109
+ listeners.splice(idx, 1);
110
+ if (listeners.length === 0 && parentUnsubscribe) {
111
+ parentUnsubscribe();
112
+ parentUnsubscribe = null;
113
+ }
114
+ };
115
+ };
116
+ return tapOnListen;
117
+ }
@@ -7,16 +7,19 @@ export interface TimelinePoint extends Emitter<PointEvent> {
7
7
  /**
8
8
  * Creates a range on the Timeline, with a given duration, starting at this point
9
9
  * @param duration
10
+ * @returns Listenable: emits normalised (0..1) range progression
10
11
  */
11
12
  range(duration: number): TimelineRange;
12
13
  /**
13
14
  * Creates a range on the Timeline, with a given end point, starting at this point
14
15
  * @param endPoint
16
+ * @returns Listenable: emits normalised (0..1) range progression
15
17
  */
16
18
  to(endPoint: number | TimelinePoint): TimelineRange;
17
19
  /**
18
20
  * Creates a point on the Timeline at an offset position from this one
19
21
  * @param timeOffset
22
+ * @returns Listenable: emits a PointEvent when the point is reached or passed by a Timeline seek
20
23
  */
21
24
  delta(timeOffset: number): TimelinePoint;
22
25
  /**
@@ -1,25 +1,26 @@
1
1
  import { Easer, easers } from "./easing";
2
- import { Emitter } from "./emitters";
2
+ import { RangeProgression } from "./emitters";
3
3
  import { TimelinePoint } from "./point";
4
- import { Blendable } from "./tween";
5
4
  export interface TimelineRange extends RangeProgression {
6
5
  /**
7
6
  * Creates two ranges by seperating one at a given point
8
7
  * @param position Point of separation, relative to the range's start - if omitted, the range will be separated halfway
9
8
  *
10
9
  * Must be greater than 0 and less than the range's duration
10
+ * @returns Tuple of two ranges
11
11
  */
12
12
  bisect(position?: number): [TimelineRange, TimelineRange];
13
13
  /**
14
14
  * Creates a series of evenly-spread points across the range, excluding the range's start and end
15
15
  * @param count Number of Points to return
16
+ * @returns Array(count) of points
16
17
  */
17
18
  spread(count: number): TimelinePoint[];
18
19
  /**
19
20
  * Progresses the Timeline across the range
20
21
  * @param easer
21
22
  */
22
- play(easer?: Easer): Promise<void>;
23
+ play(easer?: Easer | keyof typeof easers): Promise<void>;
23
24
  /**
24
25
  * Creates a new range representing a direct expansion of this one
25
26
  * @param delta Amount to grow by (in time units)
@@ -34,6 +35,12 @@ export interface TimelineRange extends RangeProgression {
34
35
  * @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
35
36
  */
36
37
  scale(factor: number, anchor?: number): TimelineRange;
38
+ /**
39
+ * Checks if a point is within this range
40
+ * @param point The point to check
41
+ * @returns true if the provided point is within the range
42
+ */
43
+ contains(point: TimelinePoint): boolean;
37
44
  /** The point on the Timeline at which this range begins */
38
45
  readonly start: TimelinePoint;
39
46
  /** The point on the Timeline at which this range ends */
@@ -41,80 +48,3 @@ export interface TimelineRange extends RangeProgression {
41
48
  /** The duration of this range */
42
49
  readonly duration: number;
43
50
  }
44
- export interface RangeProgression extends Emitter<number> {
45
- /**
46
- * Creates a chainable progress emitter that applies an easing function to its parent's emitted values
47
- *
48
- * @param easer An easing function of the form `(progression: number) => number`
49
- * @returns Listenable: emits eased progression values
50
- */
51
- ease(easer?: Easer | keyof typeof easers): RangeProgression;
52
- /**
53
- * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
54
- *
55
- * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
56
- *
57
- * @param from Value to interpolate from
58
- * @param to Value to interpolate to
59
- * @returns Listenable: emits interpolated values
60
- */
61
- tween(from: number, to: number): Emitter<number>;
62
- /**
63
- * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
64
- *
65
- * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
66
- *
67
- * #### String interpolation
68
- * * If the strings contain tweenable tokens (numbers, colour codes) and are otherwise identical, those tokens are interpolated
69
- * * Otherwise the `from` string is progressively replaced, left-to-right, with the `to` string
70
- *
71
- * eg
72
- * ```ts
73
- * range
74
- * .tween("0px 0px 0px #0000", "4px 4px 8px #0005")
75
- * .listen(s => element.style.textShadow = s);
76
- * ```
77
- *
78
- * @param from Value to interpolate from
79
- * @param to Value to interpolate to
80
- * @returns Listenable: emits interpolated values
81
- */
82
- tween(from: string, to: string): Emitter<string>;
83
- /**
84
- * Creates a chainable emitter that interpolates two given values by progression emitted by its parent
85
- *
86
- * Can interpolate types `number`, `number[]`, string and objects with a `blend(from: this, to: this): this` method
87
- *
88
- * @param from Value to interpolate from
89
- * @param to Value to interpolate to
90
- * @returns Listenable: emits interpolated values
91
- */
92
- tween<T extends Blendable | number[]>(from: T, to: T): Emitter<T>;
93
- /**
94
- * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
95
- *
96
- * @param steps – positive integer (e.g. 10 → 0, .1, .2 … 1)
97
- * @throws RangeError if steps is not a positive integer
98
- * @returns Listenable: emits quantised progression values
99
- */
100
- snap(steps: number): RangeProgression;
101
- /**
102
- * Creates a chainable progress emitter that emits `1` when the incoming progress value is greater‑than‑or‑equal to the supplied `threshold`, otherwise emits `0`
103
- *
104
- * @param threshold the cut‑off value
105
- * @returns Listenable: emits 0 or 1 after comparing progress with a threshold
106
- */
107
- threshold(threshold: number): RangeProgression;
108
- /**
109
- * Creates a chainable progress emitter that clamps incoming values
110
- * @param min default 0
111
- * @param max default 1
112
- * @returns Listenable: emits clamped progression values
113
- */
114
- clamp(min?: number, max?: number): RangeProgression;
115
- /**
116
- * Creates a chainable progress emitter that maps incoming values to a repeating linear scale
117
- * @param count Number of repetitions
118
- */
119
- repeat(count: number): RangeProgression;
120
- }
@@ -201,6 +201,9 @@ class Timeline {
201
201
  }
202
202
  return this.range(newStart, newEnd - newStart);
203
203
  },
204
+ contains: point => {
205
+ return point.position >= startPosition && point.position < endPosition;
206
+ }
204
207
  });
205
208
  }
206
209
  getWrappedPosition(n) {
package/internal/tween.js CHANGED
@@ -93,27 +93,27 @@ function blendColours(from, to, bias) {
93
93
  return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
94
94
  }
95
95
  const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
96
+ const tokenise = (s) => {
97
+ const chunks = [];
98
+ let lastIdx = 0;
99
+ let m;
100
+ while ((m = tweenableTokenRegex.exec(s))) {
101
+ const token = m[0];
102
+ const prefix = s.slice(lastIdx, m.index); // literal before token
103
+ chunks.push({ prefix, token });
104
+ lastIdx = m.index + token.length;
105
+ }
106
+ // trailing literal after the last token – stored as a final chunk
107
+ // with an empty token (so the consumer can easily append it)
108
+ const tail = s.slice(lastIdx);
109
+ if (tail.length) {
110
+ chunks.push({ prefix: tail, token: "" });
111
+ }
112
+ return chunks;
113
+ };
96
114
  function blendStrings(from, to, progress) {
97
115
  if (from === to || progress === 0)
98
116
  return from;
99
- const tokenise = (s) => {
100
- const chunks = [];
101
- let lastIdx = 0;
102
- let m;
103
- while ((m = tweenableTokenRegex.exec(s))) {
104
- const token = m[0];
105
- const prefix = s.slice(lastIdx, m.index); // literal before token
106
- chunks.push({ prefix, token });
107
- lastIdx = m.index + token.length;
108
- }
109
- // trailing literal after the last token – stored as a final chunk
110
- // with an empty token (so the consumer can easily append it)
111
- const tail = s.slice(lastIdx);
112
- if (tail.length) {
113
- chunks.push({ prefix: tail, token: "" });
114
- }
115
- return chunks;
116
- };
117
117
  const fromChunks = tokenise(from);
118
118
  const toChunks = tokenise(to);
119
119
  const tokenCount = fromChunks.filter(c => c.token).length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"