@xtia/timeline 1.0.10 → 1.0.12
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 +11 -0
- package/internal/emitters.d.ts +14 -0
- package/internal/emitters.js +24 -3
- package/internal/point.d.ts +6 -0
- package/internal/point.js +3 -0
- package/internal/range.js +5 -1
- package/internal/timeline.js +1 -1
- package/internal/tween.d.ts +5 -3
- package/internal/tween.js +83 -80
- package/internal/utils.d.ts +1 -1
- package/internal/utils.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -483,6 +483,13 @@ Creates a [`TimelineRange`](#timelinerange-interface) on the Timeline to which t
|
|
|
483
483
|
|
|
484
484
|
Creates a `TimelinePoint` at an offset from the this point.
|
|
485
485
|
|
|
486
|
+
##### `seek(): void`
|
|
487
|
+
|
|
488
|
+
Seeks the parent Timeline to this point.
|
|
489
|
+
|
|
490
|
+
##### `seek(duration: number, easer?: Easer): Promise<void>`
|
|
491
|
+
|
|
492
|
+
Smooth-seeks the parent Timeline to this point over a specified duration and resolves the returned Promise on completion.
|
|
486
493
|
|
|
487
494
|
|
|
488
495
|
|
|
@@ -593,6 +600,10 @@ blend(from: this, to: this, progress: number): this
|
|
|
593
600
|
|
|
594
601
|
Creates an emitter that quantises progression emitted by the parent to the nearest of `steps` discrete values.
|
|
595
602
|
|
|
603
|
+
##### `sample<T>(values: ArrayLike<T>): `[`Emitter<T>`](#emittert-interface)
|
|
604
|
+
|
|
605
|
+
Creates an emitter that emits values from an array according to progression.
|
|
606
|
+
|
|
596
607
|
##### `threshold(threshold): RangeProgression`
|
|
597
608
|
|
|
598
609
|
Creates an emitter that emits 0 when the parent emits a value below `threshold` and 1 when a parent emission is equal to or greater than `threshold`.
|
package/internal/emitters.d.ts
CHANGED
|
@@ -125,6 +125,20 @@ export declare class RangeProgression extends Emitter<number> {
|
|
|
125
125
|
*/
|
|
126
126
|
tween<T extends Tweenable>(from: T, to: T): Emitter<T>;
|
|
127
127
|
tween<T extends BlendableWith<T, R>, R>(from: T, to: R): Emitter<T>;
|
|
128
|
+
/**
|
|
129
|
+
* Creates a chainable emitter that takes a value from an array according to progression
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* range
|
|
134
|
+
* .sample(["a", "b", "c"])
|
|
135
|
+
* .listen(v => console.log(v));
|
|
136
|
+
* // logs 'b' when a seek lands halfway through range
|
|
137
|
+
* ```
|
|
138
|
+
* @param source array to sample
|
|
139
|
+
* @returns Listenable: emits the sampled values
|
|
140
|
+
*/
|
|
141
|
+
sample<T>(source: ArrayLike<T>): Emitter<T>;
|
|
128
142
|
/**
|
|
129
143
|
* Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
|
|
130
144
|
*
|
package/internal/emitters.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { easers } from "./easing";
|
|
2
|
-
import {
|
|
2
|
+
import { createTween } from "./tween";
|
|
3
3
|
import { clamp } from "./utils";
|
|
4
4
|
export class Emitter {
|
|
5
5
|
constructor(onListen) {
|
|
@@ -138,7 +138,28 @@ export class RangeProgression extends Emitter {
|
|
|
138
138
|
})) : h => this.onListen(h));
|
|
139
139
|
}
|
|
140
140
|
tween(from, to) {
|
|
141
|
-
|
|
141
|
+
const tween = createTween(from, to);
|
|
142
|
+
return new Emitter(handler => this.onListen(progress => handler(tween(progress))));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Creates a chainable emitter that takes a value from an array according to progression
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* range
|
|
150
|
+
* .sample(["a", "b", "c"])
|
|
151
|
+
* .listen(v => console.log(v));
|
|
152
|
+
* // logs 'b' when a seek lands halfway through range
|
|
153
|
+
* ```
|
|
154
|
+
* @param source array to sample
|
|
155
|
+
* @returns Listenable: emits the sampled values
|
|
156
|
+
*/
|
|
157
|
+
sample(source) {
|
|
158
|
+
return new Emitter(handler => this.onListen(progress => {
|
|
159
|
+
const clampedProgress = clamp(progress);
|
|
160
|
+
const index = Math.floor(clampedProgress * (source.length - 1));
|
|
161
|
+
handler(source[index]);
|
|
162
|
+
}));
|
|
142
163
|
}
|
|
143
164
|
/**
|
|
144
165
|
* Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
|
|
@@ -153,7 +174,7 @@ export class RangeProgression extends Emitter {
|
|
|
153
174
|
}
|
|
154
175
|
return new RangeProgression(handler => this.onListen(progress => {
|
|
155
176
|
const snapped = Math.round(progress * steps) / steps;
|
|
156
|
-
handler(clamp(snapped
|
|
177
|
+
handler(clamp(snapped));
|
|
157
178
|
}));
|
|
158
179
|
}
|
|
159
180
|
/**
|
package/internal/point.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Easer } from "./easing";
|
|
1
2
|
import { Emitter, ListenFunc } from "./emitters";
|
|
2
3
|
import { TimelineRange } from "./range";
|
|
3
4
|
import { Timeline } from "./timeline";
|
|
@@ -35,4 +36,9 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
|
|
|
35
36
|
* @returns Listenable: emits a PointEvent when the point is reached or passed by a Timeline seek
|
|
36
37
|
*/
|
|
37
38
|
delta(timeOffset: number): TimelinePoint;
|
|
39
|
+
/**
|
|
40
|
+
* Seeks the parent Timeline to this point
|
|
41
|
+
*/
|
|
42
|
+
seek(): void;
|
|
43
|
+
seek(duration: number, easer?: Easer): Promise<void>;
|
|
38
44
|
}
|
package/internal/point.js
CHANGED
package/internal/range.js
CHANGED
|
@@ -6,7 +6,11 @@ export class TimelineRange extends RangeProgression {
|
|
|
6
6
|
constructor(onListen, timeline, startPosition,
|
|
7
7
|
/** The duration of this range */
|
|
8
8
|
duration) {
|
|
9
|
-
super(
|
|
9
|
+
super(duration == 0
|
|
10
|
+
? () => {
|
|
11
|
+
throw new Error("Zero-duration ranges may not be listened");
|
|
12
|
+
}
|
|
13
|
+
: onListen);
|
|
10
14
|
this.timeline = timeline;
|
|
11
15
|
this.startPosition = startPosition;
|
|
12
16
|
this.duration = duration;
|
package/internal/timeline.js
CHANGED
package/internal/tween.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export type Tweenable = number | number[] | string | Blendable;
|
|
2
|
+
export type Tweenable = number | number[] | string | string[] | Blendable | Blendable[];
|
|
3
3
|
/** @internal */
|
|
4
4
|
export interface Blendable {
|
|
5
5
|
blend(target: this, progress: number): this;
|
|
@@ -7,5 +7,7 @@ export interface Blendable {
|
|
|
7
7
|
export interface BlendableWith<T, R> {
|
|
8
8
|
blend(target: R, progress: number): T;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
export declare function
|
|
10
|
+
type TweenFunc<T> = (progress: number) => T;
|
|
11
|
+
export declare function createTween<T extends Tweenable>(from: T, to: T): TweenFunc<T>;
|
|
12
|
+
export declare function createTween<T extends BlendableWith<T, R>, R>(from: T, to: R): TweenFunc<T>;
|
|
13
|
+
export {};
|
package/internal/tween.js
CHANGED
|
@@ -1,45 +1,82 @@
|
|
|
1
1
|
import { clamp } from "./utils";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const tokenTypes = {
|
|
3
|
+
none: 0,
|
|
4
|
+
number: 1,
|
|
5
|
+
colour: 2,
|
|
6
|
+
};
|
|
7
|
+
export function createTween(from, to) {
|
|
8
|
+
if (from === to)
|
|
9
|
+
return () => from;
|
|
4
10
|
if (Array.isArray(from)) {
|
|
5
|
-
|
|
6
|
-
if (from.length != toArr.length)
|
|
11
|
+
if (from.length != to.length) {
|
|
7
12
|
throw new Error("Array size mismatch");
|
|
8
|
-
|
|
13
|
+
}
|
|
14
|
+
const tweens = from.map((f, i) => createTween(f, to[i]));
|
|
15
|
+
return progress => tweens.map(t => t(progress));
|
|
9
16
|
}
|
|
10
|
-
|
|
11
|
-
return
|
|
17
|
+
switch (typeof from) {
|
|
18
|
+
case "number": return progress => blendNumbers(from, to, progress);
|
|
19
|
+
case "object": return progress => from.blend(to, progress);
|
|
20
|
+
case "string": return createStringTween(from, to);
|
|
21
|
+
default: throw new Error("Invalid tweening type");
|
|
12
22
|
}
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
}
|
|
24
|
+
function createStringTween(from, to) {
|
|
25
|
+
const fromChunks = tokenise(from);
|
|
26
|
+
const toChunks = tokenise(to);
|
|
27
|
+
const tokenCount = fromChunks.filter(c => c.token).length;
|
|
28
|
+
// where length mismatch, use merging
|
|
29
|
+
if (tokenCount !== toChunks.filter(c => c.token).length) {
|
|
30
|
+
return createStringMerge(from, to);
|
|
15
31
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
32
|
+
// where token prefix/type mismatch, use merging
|
|
33
|
+
if (fromChunks.some((chunk, i) => toChunks[i].prefix !== chunk.prefix ||
|
|
34
|
+
toChunks[i].type !== chunk.type)) {
|
|
35
|
+
return createStringMerge(from, to);
|
|
21
36
|
}
|
|
22
|
-
|
|
37
|
+
// convert token chunks to individual string tween funcs
|
|
38
|
+
const tweenChunks = fromChunks.map((chunk, i) => {
|
|
39
|
+
const fromToken = chunk.token;
|
|
40
|
+
const toToken = toChunks[i].token;
|
|
41
|
+
const prefix = chunk.prefix;
|
|
42
|
+
if (chunk.type === tokenTypes.none)
|
|
43
|
+
return () => prefix;
|
|
44
|
+
if (chunk.type === tokenTypes.colour) {
|
|
45
|
+
const fromColour = parseColour(fromToken);
|
|
46
|
+
const toColour = parseColour(toToken);
|
|
47
|
+
return progress => prefix + blendColours(fromColour, toColour, progress);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const fromNum = parseFloat(fromToken);
|
|
51
|
+
const toNum = parseFloat(toToken);
|
|
52
|
+
return progress => {
|
|
53
|
+
const blendedNum = blendNumbers(fromNum, toNum, progress);
|
|
54
|
+
return prefix + blendedNum.toString();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (tweenChunks.length == 1)
|
|
59
|
+
return tweenChunks[0];
|
|
60
|
+
return progress => tweenChunks.map(t => t(progress)).join("");
|
|
23
61
|
}
|
|
24
62
|
function blendNumbers(from, to, progress) {
|
|
25
63
|
return from + progress * (to - from);
|
|
26
64
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
// Fast‑path: identical strings or one is empty
|
|
65
|
+
function createStringMerge(from, to) {
|
|
66
|
+
// fast‑path: identical strings or one is empty
|
|
30
67
|
if (from === to)
|
|
31
|
-
return from;
|
|
68
|
+
return () => from;
|
|
32
69
|
if (!from)
|
|
33
|
-
return to;
|
|
70
|
+
return () => to;
|
|
34
71
|
if (!to)
|
|
35
|
-
return from;
|
|
72
|
+
return () => from;
|
|
36
73
|
const split = (s) => {
|
|
37
|
-
//
|
|
74
|
+
// prefer Intl.Segmenter if available (Node 14, modern browsers)
|
|
38
75
|
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
39
76
|
const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
40
77
|
return Array.from(seg.segment(s), (seg) => seg.segment);
|
|
41
78
|
}
|
|
42
|
-
//
|
|
79
|
+
// fallback regex (covers surrogate pairs & combining marks)
|
|
43
80
|
const graphemeRegex = /(\P{Mark}\p{Mark}*|[\uD800-\uDBFF][\uDC00-\uDFFF])/gu;
|
|
44
81
|
return s.match(graphemeRegex) ?? Array.from(s);
|
|
45
82
|
};
|
|
@@ -54,15 +91,18 @@ function mergeStrings(from, to, progress) {
|
|
|
54
91
|
};
|
|
55
92
|
const fromP = pad(a);
|
|
56
93
|
const toP = pad(b);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
result
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
return (progress) => {
|
|
95
|
+
const clampedProgress = clamp(progress);
|
|
96
|
+
const replaceCount = Math.floor(clampedProgress * maxLen);
|
|
97
|
+
const result = new Array(maxLen);
|
|
98
|
+
for (let i = 0; i < maxLen; ++i) {
|
|
99
|
+
result[i] = i < replaceCount ? toP[i] : fromP[i];
|
|
100
|
+
}
|
|
101
|
+
while (result.length && result[result.length - 1] === " ") {
|
|
102
|
+
result.pop();
|
|
103
|
+
}
|
|
104
|
+
return result.join("");
|
|
105
|
+
};
|
|
66
106
|
}
|
|
67
107
|
function parseColour(code) {
|
|
68
108
|
if (code.length < 2 || !code.startsWith("#"))
|
|
@@ -84,68 +124,31 @@ function parseColour(code) {
|
|
|
84
124
|
return [...rawHex.matchAll(/../g)].map(hex => parseInt(hex[0], 16));
|
|
85
125
|
}
|
|
86
126
|
function blendColours(from, to, bias) {
|
|
87
|
-
const
|
|
88
|
-
const toColour = parseColour(to);
|
|
89
|
-
const blended = fromColour.map((val, i) => clamp(blendNumbers(val, toColour[i], bias), 0, 255));
|
|
127
|
+
const blended = from.map((val, i) => clamp(blendNumbers(val, to[i], bias), 0, 255));
|
|
90
128
|
return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
|
|
91
129
|
}
|
|
92
130
|
const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
|
|
93
|
-
|
|
131
|
+
function tokenise(s) {
|
|
94
132
|
const chunks = [];
|
|
95
133
|
let lastIdx = 0;
|
|
96
134
|
let m;
|
|
97
135
|
while ((m = tweenableTokenRegex.exec(s))) {
|
|
98
136
|
const token = m[0];
|
|
99
137
|
const prefix = s.slice(lastIdx, m.index); // literal before token
|
|
100
|
-
|
|
138
|
+
const type = getTokenType(token);
|
|
139
|
+
chunks.push({ prefix, token, type });
|
|
101
140
|
lastIdx = m.index + token.length;
|
|
102
141
|
}
|
|
103
142
|
// trailing literal after the last token – stored as a final chunk
|
|
104
|
-
// with an empty token (so the consumer can easily append it)
|
|
105
143
|
const tail = s.slice(lastIdx);
|
|
106
144
|
if (tail.length) {
|
|
107
|
-
chunks.push({ prefix: tail, token: "" });
|
|
145
|
+
chunks.push({ prefix: tail, token: "", type: tokenTypes.none });
|
|
108
146
|
}
|
|
109
147
|
return chunks;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const tokenCount = fromChunks.filter(c => c.token).length;
|
|
117
|
-
if (tokenCount !== toChunks.filter(c => c.token).length) {
|
|
118
|
-
return mergeStrings(from, to, progress);
|
|
119
|
-
}
|
|
120
|
-
let result = "";
|
|
121
|
-
for (let i = 0, j = 0; i < fromChunks.length && j < toChunks.length;) {
|
|
122
|
-
const f = fromChunks[i];
|
|
123
|
-
const t = toChunks[j];
|
|
124
|
-
// The *prefix* (the text before the token) must be the same.
|
|
125
|
-
if (f.prefix !== t.prefix) {
|
|
126
|
-
return mergeStrings(from, to, progress);
|
|
127
|
-
}
|
|
128
|
-
// Append the unchanged prefix.
|
|
129
|
-
result += f.prefix;
|
|
130
|
-
// If we are at the *trailing* chunk (no token), just break.
|
|
131
|
-
if (!f.token && !t.token) {
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
// Blend the token according to its kind.
|
|
135
|
-
let blended;
|
|
136
|
-
if (f.token.startsWith("#")) {
|
|
137
|
-
blended = blendColours(f.token, t.token, progress);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
const fNum = parseFloat(f.token);
|
|
141
|
-
const tNum = parseFloat(t.token);
|
|
142
|
-
const blendedNum = blendNumbers(fNum, tNum, progress);
|
|
143
|
-
blended = blendedNum.toString();
|
|
144
|
-
}
|
|
145
|
-
result += blended;
|
|
146
|
-
// Advance both pointers.
|
|
147
|
-
i++;
|
|
148
|
-
j++;
|
|
149
|
-
}
|
|
150
|
-
return result;
|
|
148
|
+
}
|
|
149
|
+
;
|
|
150
|
+
function getTokenType(token) {
|
|
151
|
+
if (token.startsWith("#"))
|
|
152
|
+
return tokenTypes.colour;
|
|
153
|
+
return tokenTypes.number;
|
|
151
154
|
}
|
package/internal/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export declare const clamp: (value: number, min
|
|
2
|
+
export declare const clamp: (value: number, min?: number, max?: number) => number;
|
|
3
3
|
/** @internal */
|
|
4
4
|
export type Widen<T> = T extends number ? number : T extends string ? string : T;
|
package/internal/utils.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
2
|
+
export const clamp = (value, min = 0, max = 1) => Math.min(Math.max(value, min), max);
|