@xtia/timeline 1.0.9 → 1.0.11
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/internal/emitters.js +3 -2
- package/internal/point.d.ts +3 -0
- package/internal/point.js +3 -0
- package/internal/range.js +6 -3
- package/internal/timeline.js +7 -5
- package/internal/tween.d.ts +3 -3
- package/internal/tween.js +67 -77
- package/package.json +1 -1
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,8 @@ 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))));
|
|
142
143
|
}
|
|
143
144
|
/**
|
|
144
145
|
* Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
|
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,6 @@ 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
|
+
seek(): void;
|
|
40
|
+
seek(duration?: number, easer?: Easer): void;
|
|
38
41
|
}
|
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;
|
|
@@ -97,7 +101,6 @@ export class TimelineRange extends RangeProgression {
|
|
|
97
101
|
const [start, end] = range instanceof TimelineRange
|
|
98
102
|
? [range.startPosition, range.endPosition]
|
|
99
103
|
: [range.position, range.position + range.duration];
|
|
100
|
-
return
|
|
101
|
-
Math.max(this.startPosition, this.endPosition) >= Math.min(start, end);
|
|
104
|
+
return this.startPosition <= end && this.endPosition >= start;
|
|
102
105
|
}
|
|
103
106
|
}
|
package/internal/timeline.js
CHANGED
|
@@ -189,7 +189,7 @@ export class Timeline {
|
|
|
189
189
|
}
|
|
190
190
|
if (duration === 0) {
|
|
191
191
|
this.seekDirect(toPosition);
|
|
192
|
-
return;
|
|
192
|
+
return Promise.resolve();
|
|
193
193
|
}
|
|
194
194
|
const seeker = new Timeline(true);
|
|
195
195
|
this.smoothSeeker = seeker;
|
|
@@ -242,15 +242,17 @@ export class Timeline {
|
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
244
|
seekRanges(to) {
|
|
245
|
-
const
|
|
246
|
-
|
|
245
|
+
const fromTime = Math.min(this._currentTime, to);
|
|
246
|
+
const toTime = Math.max(this._currentTime, to);
|
|
247
247
|
this.ranges.slice().forEach((range) => {
|
|
248
|
-
|
|
248
|
+
const rangeEnd = range.position + range.duration;
|
|
249
|
+
const overlaps = fromTime <= rangeEnd && toTime >= range.position;
|
|
250
|
+
if (overlaps) {
|
|
249
251
|
let progress = clamp((to - range.position) / range.duration, 0, 1);
|
|
250
252
|
range.handlers.slice().forEach(h => h(progress));
|
|
251
253
|
}
|
|
252
254
|
});
|
|
253
|
-
this.progressionHandlers.slice().forEach(h => h(
|
|
255
|
+
this.progressionHandlers.slice().forEach(h => h(fromTime / this._endPosition));
|
|
254
256
|
}
|
|
255
257
|
sortEntries(direction) {
|
|
256
258
|
this.currentSortDirection = direction;
|
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,5 @@ 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
|
+
export declare function createTween<T extends Tweenable>(from: T, to: T): ((progress: number) => T);
|
|
11
|
+
export declare function createTween<T extends BlendableWith<T, R>, R>(from: T, to: R): ((progress: number) => T);
|
package/internal/tween.js
CHANGED
|
@@ -1,45 +1,76 @@
|
|
|
1
1
|
import { clamp } from "./utils";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
export function createTween(from, to) {
|
|
3
|
+
if (from === to)
|
|
4
|
+
return () => from;
|
|
4
5
|
if (Array.isArray(from)) {
|
|
5
|
-
|
|
6
|
-
if (from.length != toArr.length)
|
|
6
|
+
if (from.length != to.length) {
|
|
7
7
|
throw new Error("Array size mismatch");
|
|
8
|
-
|
|
8
|
+
}
|
|
9
|
+
const tweens = from.map((f, i) => createTween(f, to[i]));
|
|
10
|
+
return progress => tweens.map(t => t(progress));
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
+
switch (typeof from) {
|
|
13
|
+
case "number": return progress => blendNumbers(from, to, progress);
|
|
14
|
+
case "object": return progress => from.blend(to, progress);
|
|
15
|
+
case "string": return createStringTween(from, to);
|
|
16
|
+
default: throw new Error("Invalid tweening type");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function createStringTween(from, to) {
|
|
20
|
+
const fromChunks = tokenise(from);
|
|
21
|
+
const toChunks = tokenise(to);
|
|
22
|
+
const tokenCount = fromChunks.filter(c => c.token).length;
|
|
23
|
+
// where length mismatch, use merging
|
|
24
|
+
if (tokenCount !== toChunks.filter(c => c.token).length) {
|
|
25
|
+
return createStringMerge(from, to);
|
|
12
26
|
}
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
// where token prefix mismatch, use merging
|
|
28
|
+
if (fromChunks.some((chunk, i) => toChunks[i].prefix !== chunk.prefix)) {
|
|
29
|
+
return createStringMerge(from, to);
|
|
15
30
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
// convert token chunks to individual string tween funcs
|
|
32
|
+
const tweenChunks = fromChunks.map((chunk, i) => {
|
|
33
|
+
const fromToken = chunk.token;
|
|
34
|
+
const toToken = toChunks[i].token;
|
|
35
|
+
const prefix = chunk.prefix;
|
|
36
|
+
if (!fromToken)
|
|
37
|
+
return () => prefix;
|
|
38
|
+
if (fromToken.startsWith("#")) {
|
|
39
|
+
const fromColour = parseColour(fromToken);
|
|
40
|
+
const toColour = parseColour(toToken);
|
|
41
|
+
return progress => prefix + blendColours(fromColour, toColour, progress);
|
|
20
42
|
}
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
else {
|
|
44
|
+
const fromNum = parseFloat(fromToken);
|
|
45
|
+
const toNum = parseFloat(toToken);
|
|
46
|
+
return progress => {
|
|
47
|
+
const blendedNum = blendNumbers(fromNum, toNum, progress);
|
|
48
|
+
return prefix + blendedNum.toString();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (tweenChunks.length == 1)
|
|
53
|
+
return tweenChunks[0];
|
|
54
|
+
return progress => tweenChunks.map(t => t(progress)).join("");
|
|
23
55
|
}
|
|
24
56
|
function blendNumbers(from, to, progress) {
|
|
25
57
|
return from + progress * (to - from);
|
|
26
58
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
// Fast‑path: identical strings or one is empty
|
|
59
|
+
function createStringMerge(from, to) {
|
|
60
|
+
// fast‑path: identical strings or one is empty
|
|
30
61
|
if (from === to)
|
|
31
|
-
return from;
|
|
62
|
+
return () => from;
|
|
32
63
|
if (!from)
|
|
33
|
-
return to;
|
|
64
|
+
return () => to;
|
|
34
65
|
if (!to)
|
|
35
|
-
return from;
|
|
66
|
+
return () => from;
|
|
36
67
|
const split = (s) => {
|
|
37
|
-
//
|
|
68
|
+
// prefer Intl.Segmenter if available (Node ≥ 14, modern browsers)
|
|
38
69
|
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
39
70
|
const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
40
71
|
return Array.from(seg.segment(s), (seg) => seg.segment);
|
|
41
72
|
}
|
|
42
|
-
//
|
|
73
|
+
// fallback regex (covers surrogate pairs & combining marks)
|
|
43
74
|
const graphemeRegex = /(\P{Mark}\p{Mark}*|[\uD800-\uDBFF][\uDC00-\uDFFF])/gu;
|
|
44
75
|
return s.match(graphemeRegex) ?? Array.from(s);
|
|
45
76
|
};
|
|
@@ -54,15 +85,18 @@ function mergeStrings(from, to, progress) {
|
|
|
54
85
|
};
|
|
55
86
|
const fromP = pad(a);
|
|
56
87
|
const toP = pad(b);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
result
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
return (progress) => {
|
|
89
|
+
const clampedProgress = clamp(progress, 0, 1);
|
|
90
|
+
const replaceCount = Math.floor(clampedProgress * maxLen);
|
|
91
|
+
const result = new Array(maxLen);
|
|
92
|
+
for (let i = 0; i < maxLen; ++i) {
|
|
93
|
+
result[i] = i < replaceCount ? toP[i] : fromP[i];
|
|
94
|
+
}
|
|
95
|
+
while (result.length && result[result.length - 1] === " ") {
|
|
96
|
+
result.pop();
|
|
97
|
+
}
|
|
98
|
+
return result.join("");
|
|
99
|
+
};
|
|
66
100
|
}
|
|
67
101
|
function parseColour(code) {
|
|
68
102
|
if (code.length < 2 || !code.startsWith("#"))
|
|
@@ -84,9 +118,7 @@ function parseColour(code) {
|
|
|
84
118
|
return [...rawHex.matchAll(/../g)].map(hex => parseInt(hex[0], 16));
|
|
85
119
|
}
|
|
86
120
|
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));
|
|
121
|
+
const blended = from.map((val, i) => clamp(blendNumbers(val, to[i], bias), 0, 255));
|
|
90
122
|
return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
|
|
91
123
|
}
|
|
92
124
|
const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
|
|
@@ -101,51 +133,9 @@ const tokenise = (s) => {
|
|
|
101
133
|
lastIdx = m.index + token.length;
|
|
102
134
|
}
|
|
103
135
|
// trailing literal after the last token – stored as a final chunk
|
|
104
|
-
// with an empty token (so the consumer can easily append it)
|
|
105
136
|
const tail = s.slice(lastIdx);
|
|
106
137
|
if (tail.length) {
|
|
107
138
|
chunks.push({ prefix: tail, token: "" });
|
|
108
139
|
}
|
|
109
140
|
return chunks;
|
|
110
141
|
};
|
|
111
|
-
function blendStrings(from, to, progress) {
|
|
112
|
-
if (from === to || progress === 0)
|
|
113
|
-
return from;
|
|
114
|
-
const fromChunks = tokenise(from);
|
|
115
|
-
const toChunks = tokenise(to);
|
|
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;
|
|
151
|
-
}
|