@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 +3 -1
- package/index.d.ts +2 -2
- package/internal/emitters.d.ts +102 -1
- package/internal/emitters.js +48 -43
- package/internal/point.d.ts +3 -0
- package/internal/range.d.ts +10 -80
- package/internal/timeline.js +3 -0
- package/internal/tween.js +18 -18
- package/package.json +1 -1
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
|
-
//
|
|
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 {
|
|
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";
|
package/internal/emitters.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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 {};
|
package/internal/emitters.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/internal/point.d.ts
CHANGED
|
@@ -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
|
/**
|
package/internal/range.d.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import { Easer, easers } from "./easing";
|
|
2
|
-
import {
|
|
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
|
-
}
|
package/internal/timeline.js
CHANGED
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;
|