@xtia/timeline 1.0.1 → 1.0.2
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/index.d.ts +5 -268
- package/index.js +6 -654
- package/internal/easing.d.ts +20 -0
- package/internal/easing.js +36 -0
- package/internal/emitters.d.ts +52 -0
- package/internal/emitters.js +112 -0
- package/internal/point.d.ts +26 -0
- package/internal/point.js +2 -0
- package/internal/range.d.ts +120 -0
- package/internal/range.js +2 -0
- package/internal/timeline.d.ts +147 -0
- package/internal/timeline.js +414 -0
- package/internal/tween.d.ts +8 -0
- package/internal/tween.js +154 -0
- package/internal/utils.d.ts +4 -0
- package/internal/utils.js +6 -0
- package/package.json +12 -3
package/index.js
CHANGED
|
@@ -1,656 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.easers = exports.Timeline = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
wrap: 2,
|
|
10
|
-
restart: 3,
|
|
11
|
-
};
|
|
12
|
-
/**
|
|
13
|
-
* Creates an autoplaying Timeline and returns a range from it
|
|
14
|
-
* @param duration
|
|
15
|
-
* @returns Object representing a range on a single-use, autoplaying Timeline
|
|
16
|
-
*/
|
|
17
|
-
function animate(duration) {
|
|
18
|
-
return new Timeline(true).range(0, duration);
|
|
19
|
-
}
|
|
20
|
-
class Timeline {
|
|
21
|
-
get currentTime() { return this._currentTime; }
|
|
22
|
-
set currentTime(v) {
|
|
23
|
-
this.seek(v);
|
|
24
|
-
}
|
|
25
|
-
get isPlaying() {
|
|
26
|
-
return this.interval !== null;
|
|
27
|
-
}
|
|
28
|
-
get end() {
|
|
29
|
-
return this.point(this._endPosition);
|
|
30
|
-
}
|
|
31
|
-
constructor(autoplay = false, endAction = "pause") {
|
|
32
|
-
/**
|
|
33
|
-
* Multiplies the speed at which `play()` progresses through the Timeline
|
|
34
|
-
*
|
|
35
|
-
* A value of 2 would double progression speed while .25 would slow it to a quarter
|
|
36
|
-
*/
|
|
37
|
-
this.timeScale = 1;
|
|
38
|
-
this._currentTime = 0;
|
|
39
|
-
this._endPosition = 0;
|
|
40
|
-
this.interval = null;
|
|
41
|
-
this.points = [];
|
|
42
|
-
this.ranges = [];
|
|
43
|
-
this.currentSortDirection = 0;
|
|
44
|
-
this.smoothSeeker = null;
|
|
45
|
-
this.seeking = false;
|
|
46
|
-
this.start = this.point(0);
|
|
47
|
-
this.positionHandlers = [];
|
|
48
|
-
if (endAction == "loop")
|
|
49
|
-
endAction = "restart";
|
|
50
|
-
if (autoplay !== false) {
|
|
51
|
-
this.play(typeof autoplay == "number" ? autoplay : default_fps);
|
|
52
|
-
}
|
|
53
|
-
if (typeof endAction == "object"
|
|
54
|
-
&& "restartAt" in endAction) {
|
|
55
|
-
this.endAction = {
|
|
56
|
-
type: EndAction.restart,
|
|
57
|
-
at: this.point(endAction.restartAt),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
else if (typeof endAction == "object"
|
|
61
|
-
&& "wrapAt" in endAction) {
|
|
62
|
-
this.endAction = {
|
|
63
|
-
type: EndAction.wrap,
|
|
64
|
-
at: this.point(endAction.wrapAt),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
else
|
|
68
|
-
this.endAction = {
|
|
69
|
-
type: EndAction[endAction],
|
|
70
|
-
at: this.point(0),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Defines a single point on the Timeline
|
|
75
|
-
*
|
|
76
|
-
* @param position
|
|
77
|
-
* @returns A point on the Timeline as specified
|
|
78
|
-
*
|
|
79
|
-
* Listenable: this point will emit a PointEvent whenever a `seek()` reaches or passes it
|
|
80
|
-
*/
|
|
81
|
-
point(position) {
|
|
82
|
-
if (position > this._endPosition)
|
|
83
|
-
this._endPosition = position;
|
|
84
|
-
const handlers = [];
|
|
85
|
-
const data = {
|
|
86
|
-
handlers,
|
|
87
|
-
position,
|
|
88
|
-
};
|
|
89
|
-
return createEmitter(handler => {
|
|
90
|
-
if (this.seeking)
|
|
91
|
-
throw new Error("Can't add a listener while seeking");
|
|
92
|
-
// we're adding and removing points and ranges to the internal registry according to whether any subscriptions are active, to allow obsolete points and ranges to be garbage-collected
|
|
93
|
-
if (handlers.length == 0) {
|
|
94
|
-
this.points.push(data);
|
|
95
|
-
this.currentSortDirection = 0;
|
|
96
|
-
}
|
|
97
|
-
handlers.push(handler);
|
|
98
|
-
return () => {
|
|
99
|
-
const idx = handlers.indexOf(handler);
|
|
100
|
-
if (idx === -1)
|
|
101
|
-
throw new Error("Internal error: attempting to remove a non-present handler");
|
|
102
|
-
handlers.splice(idx, 1);
|
|
103
|
-
if (handlers.length == 0) {
|
|
104
|
-
const idx = this.points.indexOf(data);
|
|
105
|
-
this.points.splice(idx, 1);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}, {
|
|
109
|
-
delta: t => this.point(position + t),
|
|
110
|
-
range: duration => this.range(position, duration),
|
|
111
|
-
to: target => {
|
|
112
|
-
const targetPosition = typeof target == "number"
|
|
113
|
-
? target
|
|
114
|
-
: target.position;
|
|
115
|
-
return this.range(position, targetPosition - position);
|
|
116
|
-
},
|
|
117
|
-
position,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
range(start = 0, optionalDuration) {
|
|
121
|
-
const startPoint = typeof start == "number"
|
|
122
|
-
? this.point(start)
|
|
123
|
-
: start;
|
|
124
|
-
const startPosition = startPoint.position;
|
|
125
|
-
const duration = optionalDuration ?? this._endPosition - startPosition;
|
|
126
|
-
if ((startPosition + duration) > this._endPosition)
|
|
127
|
-
this._endPosition = startPosition + duration;
|
|
128
|
-
const handlers = [];
|
|
129
|
-
const range = {
|
|
130
|
-
position: startPosition,
|
|
131
|
-
duration,
|
|
132
|
-
handlers,
|
|
133
|
-
};
|
|
134
|
-
const addHandler = (handler) => {
|
|
135
|
-
if (this.seeking)
|
|
136
|
-
throw new Error("Can't add a listener while seeking");
|
|
137
|
-
if (handlers.length == 0) {
|
|
138
|
-
this.ranges.push(range);
|
|
139
|
-
this.currentSortDirection = 0;
|
|
140
|
-
}
|
|
141
|
-
handlers.push(handler);
|
|
142
|
-
return () => {
|
|
143
|
-
const idx = handlers.indexOf(handler);
|
|
144
|
-
if (idx === -1)
|
|
145
|
-
throw new Error("Internal error: attempting to remove a non-present handler");
|
|
146
|
-
handlers.splice(idx, 1);
|
|
147
|
-
if (handlers.length == 0) {
|
|
148
|
-
const idx = this.ranges.indexOf(range);
|
|
149
|
-
this.ranges.splice(idx, 1);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
return createProgressEmitter(addHandler, {
|
|
154
|
-
duration,
|
|
155
|
-
start: this.point(startPosition),
|
|
156
|
-
end: this.point(startPosition + duration),
|
|
157
|
-
bisect: (position = duration / 2) => {
|
|
158
|
-
return [
|
|
159
|
-
this.range(startPosition, position),
|
|
160
|
-
this.range(startPosition + position, duration - position),
|
|
161
|
-
];
|
|
162
|
-
},
|
|
163
|
-
spread: (count) => {
|
|
164
|
-
const delta = duration / (count + 1);
|
|
165
|
-
return [
|
|
166
|
-
...Array(count).fill(0).map((_, idx) => this.point(idx * delta + startPosition + delta))
|
|
167
|
-
];
|
|
168
|
-
},
|
|
169
|
-
play: (easer) => {
|
|
170
|
-
this.pause();
|
|
171
|
-
this.currentTime = startPosition;
|
|
172
|
-
return this.seek(startPosition + duration, duration, easer);
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
getWrappedPosition(n) {
|
|
177
|
-
if (this.endAction.type !== EndAction.wrap)
|
|
178
|
-
return n;
|
|
179
|
-
const wrapAt = this.endAction.at?.position ?? 0;
|
|
180
|
-
if (wrapAt == 0)
|
|
181
|
-
return n % this._endPosition;
|
|
182
|
-
if (n <= this._endPosition)
|
|
183
|
-
return n;
|
|
184
|
-
const loopStart = wrapAt;
|
|
185
|
-
const segment = this._endPosition - loopStart;
|
|
186
|
-
if (segment <= 0)
|
|
187
|
-
return Math.min(n, this._endPosition);
|
|
188
|
-
const overflow = n - this._endPosition;
|
|
189
|
-
const remainder = overflow % segment;
|
|
190
|
-
return loopStart + remainder;
|
|
191
|
-
}
|
|
192
|
-
seek(to, duration = 0, easer) {
|
|
193
|
-
const toPosition = typeof to == "number"
|
|
194
|
-
? to
|
|
195
|
-
: to.position;
|
|
196
|
-
if (this.seeking) {
|
|
197
|
-
throw new Error("Can't seek while seeking");
|
|
198
|
-
}
|
|
199
|
-
if (this.smoothSeeker !== null) {
|
|
200
|
-
this.smoothSeeker.pause();
|
|
201
|
-
// ensure any awaits are resolved for the previous seek?
|
|
202
|
-
this.smoothSeeker.seek(this.smoothSeeker.end);
|
|
203
|
-
this.smoothSeeker = null;
|
|
204
|
-
}
|
|
205
|
-
if (duration === 0) {
|
|
206
|
-
this.seekDirect(toPosition);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const seeker = new Timeline(true);
|
|
210
|
-
this.smoothSeeker = seeker;
|
|
211
|
-
seeker.range(0, duration).ease(easer).tween(this.currentTime, toPosition).listen(v => this.seekDirect(v));
|
|
212
|
-
return new Promise(r => seeker.end.listen(() => r()));
|
|
213
|
-
}
|
|
214
|
-
seekDirect(toPosition) {
|
|
215
|
-
const fromPosition = this._currentTime;
|
|
216
|
-
if (toPosition === fromPosition)
|
|
217
|
-
return;
|
|
218
|
-
const loopingTo = this.getWrappedPosition(toPosition);
|
|
219
|
-
const loopingFrom = this.getWrappedPosition(fromPosition);
|
|
220
|
-
let virtualFrom = loopingFrom;
|
|
221
|
-
let virtualTo = loopingTo;
|
|
222
|
-
const direction = toPosition > fromPosition ? 1 : -1;
|
|
223
|
-
if (direction !== this.currentSortDirection)
|
|
224
|
-
this.sortEntries(direction);
|
|
225
|
-
if (direction === 1 && loopingTo < loopingFrom) {
|
|
226
|
-
virtualFrom = loopingFrom - this._endPosition;
|
|
227
|
-
}
|
|
228
|
-
else if (direction === -1 && loopingTo > loopingFrom) {
|
|
229
|
-
virtualFrom = loopingFrom + this._endPosition;
|
|
230
|
-
}
|
|
231
|
-
this.seeking = true;
|
|
232
|
-
this._currentTime = virtualFrom;
|
|
233
|
-
try {
|
|
234
|
-
this.seekPoints(virtualTo);
|
|
235
|
-
this.seekRanges(virtualTo);
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
this.pause();
|
|
239
|
-
throw e;
|
|
240
|
-
}
|
|
241
|
-
this._currentTime = toPosition;
|
|
242
|
-
this.positionHandlers.forEach(h => h(toPosition));
|
|
243
|
-
this.seeking = false;
|
|
244
|
-
}
|
|
245
|
-
seekPoints(to) {
|
|
246
|
-
const from = this._currentTime;
|
|
247
|
-
const direction = to > from ? 1 : -1;
|
|
248
|
-
const pointsBetween = this.points.filter(direction > 0
|
|
249
|
-
? p => p.position > from && p.position <= to
|
|
250
|
-
: p => p.position <= from && p.position > to);
|
|
251
|
-
pointsBetween.forEach(p => {
|
|
252
|
-
this.seekRanges(p.position);
|
|
253
|
-
this._currentTime = p.position;
|
|
254
|
-
const eventData = {
|
|
255
|
-
direction
|
|
256
|
-
};
|
|
257
|
-
p.handlers.forEach(h => h(eventData));
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
seekRanges(to) {
|
|
261
|
-
const fromTime = this._currentTime;
|
|
262
|
-
this.ranges.forEach((range) => {
|
|
263
|
-
const { duration, position } = range;
|
|
264
|
-
const end = position + duration;
|
|
265
|
-
// filter ranges that overlap seeked range
|
|
266
|
-
if (Math.min(position, end) <= Math.max(to, fromTime)
|
|
267
|
-
&& Math.min(to, fromTime) <= Math.max(position, end)) {
|
|
268
|
-
let progress = clamp((to - range.position) / range.duration, 0, 1);
|
|
269
|
-
range.handlers.forEach(h => h(progress));
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
sortEntries(direction) {
|
|
274
|
-
this.currentSortDirection = direction;
|
|
275
|
-
this.points.sort(direction == 1
|
|
276
|
-
? sortEvents
|
|
277
|
-
: sortReverse);
|
|
278
|
-
this.ranges.sort(direction == 1
|
|
279
|
-
? sortTweens
|
|
280
|
-
: sortReverse);
|
|
281
|
-
}
|
|
282
|
-
play(fps = default_fps) {
|
|
283
|
-
if (this.interval !== null)
|
|
284
|
-
this.pause();
|
|
285
|
-
let previousTime = Date.now();
|
|
286
|
-
this.interval = setInterval(() => {
|
|
287
|
-
const newTime = Date.now();
|
|
288
|
-
const elapsed = newTime - previousTime;
|
|
289
|
-
previousTime = newTime;
|
|
290
|
-
let delta = elapsed * this.timeScale;
|
|
291
|
-
if (this._currentTime + delta <= this._endPosition) {
|
|
292
|
-
this.currentTime += delta;
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
// overshot; perform endAction
|
|
296
|
-
if (this.endAction.type == EndAction.restart) {
|
|
297
|
-
const loopRange = this.endAction.at.to(this._endPosition);
|
|
298
|
-
const loopLen = loopRange.duration;
|
|
299
|
-
if (loopLen <= 0) {
|
|
300
|
-
const target = Math.min(this._currentTime + delta, this._endPosition);
|
|
301
|
-
this.seek(target);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
while (delta > 0) {
|
|
305
|
-
const distanceToEnd = this._endPosition - this._currentTime;
|
|
306
|
-
if (delta < distanceToEnd) {
|
|
307
|
-
this.seek(this._currentTime + delta);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
this.seek(this._endPosition);
|
|
311
|
-
delta -= distanceToEnd;
|
|
312
|
-
this.seek(this.endAction.at);
|
|
313
|
-
}
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (this.endAction.type == EndAction.pause) {
|
|
317
|
-
this.seek(this._endPosition);
|
|
318
|
-
this.pause();
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
this.currentTime += delta;
|
|
322
|
-
}, 1000 / fps);
|
|
323
|
-
}
|
|
324
|
-
pause() {
|
|
325
|
-
if (this.interval === null)
|
|
326
|
-
return;
|
|
327
|
-
clearInterval(this.interval);
|
|
328
|
-
this.interval = null;
|
|
329
|
-
}
|
|
330
|
-
step(delta = 1) {
|
|
331
|
-
this.currentTime += delta * this.timeScale;
|
|
332
|
-
}
|
|
333
|
-
tween(start, durationOrToPoint, apply, from, to, easer) {
|
|
334
|
-
const startPosition = typeof start == "number"
|
|
335
|
-
? start
|
|
336
|
-
: start.position;
|
|
337
|
-
const duration = typeof durationOrToPoint == "number"
|
|
338
|
-
? durationOrToPoint
|
|
339
|
-
: (durationOrToPoint.position - startPosition);
|
|
340
|
-
this.range(startPosition, duration).ease(easer).tween(from, to).listen(apply);
|
|
341
|
-
return this.createChainingInterface(startPosition + duration);
|
|
342
|
-
}
|
|
343
|
-
at(position, action, reverse) {
|
|
344
|
-
const point = typeof position == "number" ? this.point(position) : position;
|
|
345
|
-
if (reverse === true)
|
|
346
|
-
reverse = action;
|
|
347
|
-
if (action)
|
|
348
|
-
point.listen(reverse
|
|
349
|
-
? (event => event.direction < 0 ? reverse() : action)
|
|
350
|
-
: action);
|
|
351
|
-
return this.createChainingInterface(point.position);
|
|
352
|
-
}
|
|
353
|
-
createChainingInterface(position) {
|
|
354
|
-
return {
|
|
355
|
-
thenTween: (duration, apply, from = 0, to = 1, easer) => {
|
|
356
|
-
return this.tween(position, duration, apply, from, to, easer);
|
|
357
|
-
},
|
|
358
|
-
then: (action) => this.at(position, action),
|
|
359
|
-
thenWait: (delay) => {
|
|
360
|
-
this.point(position + delay);
|
|
361
|
-
return this.createChainingInterface(position + delay);
|
|
362
|
-
},
|
|
363
|
-
end: this.point(position),
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* @deprecated use `timeline.currentTime`
|
|
368
|
-
*/
|
|
369
|
-
get position() {
|
|
370
|
-
return this._currentTime;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
exports.Timeline = Timeline;
|
|
374
|
-
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
375
|
-
const sortEvents = (a, b) => {
|
|
376
|
-
return a.position - b.position;
|
|
377
|
-
};
|
|
378
|
-
const sortTweens = (a, b) => {
|
|
379
|
-
return (a.position + a.duration) - (b.position + b.duration);
|
|
380
|
-
};
|
|
381
|
-
const sortReverse = (a, b) => {
|
|
382
|
-
if (a.position == b.position)
|
|
383
|
-
return 1;
|
|
384
|
-
return b.position - a.position;
|
|
385
|
-
};
|
|
386
|
-
function tweenValue(from, to, progress) {
|
|
387
|
-
if (Array.isArray(from)) {
|
|
388
|
-
const toArr = to;
|
|
389
|
-
if (from.length != toArr.length)
|
|
390
|
-
throw new Error("Array size mismatch");
|
|
391
|
-
return from.map((v, i) => tweenValue(v, toArr[i], progress));
|
|
392
|
-
}
|
|
393
|
-
if (typeof from == "string") {
|
|
394
|
-
return blendStrings(from, to, progress);
|
|
395
|
-
}
|
|
396
|
-
if (typeof from == "number") {
|
|
397
|
-
return blendNumbers(from, to, progress);
|
|
398
|
-
}
|
|
399
|
-
if (from && typeof from == "object") {
|
|
400
|
-
if ("blend" in from) {
|
|
401
|
-
const blendableSource = from;
|
|
402
|
-
return blendableSource.blend(to, progress);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
throw new Error("Value not recognised as Tweenable");
|
|
406
|
-
}
|
|
407
|
-
function blendNumbers(from, to, progress) {
|
|
408
|
-
return from + progress * (to - from);
|
|
409
|
-
}
|
|
410
|
-
function mergeStrings(from, to, progress) {
|
|
411
|
-
const p = Math.min(Math.max(progress, 0), 1);
|
|
412
|
-
// Fast‑path: identical strings or one is empty
|
|
413
|
-
if (from === to)
|
|
414
|
-
return from;
|
|
415
|
-
if (!from)
|
|
416
|
-
return to;
|
|
417
|
-
if (!to)
|
|
418
|
-
return from;
|
|
419
|
-
const split = (s) => {
|
|
420
|
-
// Prefer Intl.Segmenter if available (Node ≥ 14, modern browsers)
|
|
421
|
-
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
422
|
-
const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
423
|
-
return Array.from(seg.segment(s), (seg) => seg.segment);
|
|
424
|
-
}
|
|
425
|
-
// Fallback regex (covers surrogate pairs & combining marks)
|
|
426
|
-
const graphemeRegex = /(\P{Mark}\p{Mark}*|[\uD800-\uDBFF][\uDC00-\uDFFF])/gu;
|
|
427
|
-
return s.match(graphemeRegex) ?? Array.from(s);
|
|
428
|
-
};
|
|
429
|
-
const a = split(from);
|
|
430
|
-
const b = split(to);
|
|
431
|
-
const maxLen = Math.max(a.length, b.length);
|
|
432
|
-
const pad = (arr) => {
|
|
433
|
-
const diff = maxLen - arr.length;
|
|
434
|
-
if (diff <= 0)
|
|
435
|
-
return arr;
|
|
436
|
-
return arr.concat(Array(diff).fill(" "));
|
|
437
|
-
};
|
|
438
|
-
const fromP = pad(a);
|
|
439
|
-
const toP = pad(b);
|
|
440
|
-
const replaceCount = Math.floor(p * maxLen);
|
|
441
|
-
const result = new Array(maxLen);
|
|
442
|
-
for (let i = 0; i < maxLen; ++i) {
|
|
443
|
-
result[i] = i < replaceCount ? toP[i] : fromP[i];
|
|
444
|
-
}
|
|
445
|
-
while (result.length && result[result.length - 1] === " ") {
|
|
446
|
-
result.pop();
|
|
447
|
-
}
|
|
448
|
-
return result.join("");
|
|
449
|
-
}
|
|
450
|
-
function parseColour(code) {
|
|
451
|
-
if (code.length < 2 || !code.startsWith("#"))
|
|
452
|
-
throw new Error("Invalid colour");
|
|
453
|
-
let rawHex = code.substring(1);
|
|
454
|
-
if (rawHex.length == 1)
|
|
455
|
-
rawHex = rawHex + rawHex + rawHex;
|
|
456
|
-
if (rawHex.length == 2) {
|
|
457
|
-
const white = rawHex[0];
|
|
458
|
-
const alpha = rawHex[1];
|
|
459
|
-
rawHex = white + white + white + alpha;
|
|
460
|
-
}
|
|
461
|
-
if (rawHex.length == 3)
|
|
462
|
-
rawHex += "f";
|
|
463
|
-
if (rawHex.length == 4)
|
|
464
|
-
rawHex = rawHex.replace(/./g, c => c + c);
|
|
465
|
-
if (rawHex.length == 6)
|
|
466
|
-
rawHex += "ff";
|
|
467
|
-
return [...rawHex.matchAll(/../g)].map(hex => parseInt(hex[0], 16));
|
|
468
|
-
}
|
|
469
|
-
function blendColours(from, to, bias) {
|
|
470
|
-
const fromColour = parseColour(from);
|
|
471
|
-
const toColour = parseColour(to);
|
|
472
|
-
const blended = fromColour.map((val, i) => clamp(blendNumbers(val, toColour[i], bias), 0, 255));
|
|
473
|
-
return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
|
|
474
|
-
}
|
|
475
|
-
const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
|
|
476
|
-
function blendStrings(from, to, progress) {
|
|
477
|
-
if (from === to || progress === 0)
|
|
478
|
-
return from;
|
|
479
|
-
const tokenise = (s) => {
|
|
480
|
-
const chunks = [];
|
|
481
|
-
let lastIdx = 0;
|
|
482
|
-
let m;
|
|
483
|
-
while ((m = tweenableTokenRegex.exec(s))) {
|
|
484
|
-
const token = m[0];
|
|
485
|
-
const prefix = s.slice(lastIdx, m.index); // literal before token
|
|
486
|
-
chunks.push({ prefix, token });
|
|
487
|
-
lastIdx = m.index + token.length;
|
|
488
|
-
}
|
|
489
|
-
// trailing literal after the last token – stored as a final chunk
|
|
490
|
-
// with an empty token (so the consumer can easily append it)
|
|
491
|
-
const tail = s.slice(lastIdx);
|
|
492
|
-
if (tail.length) {
|
|
493
|
-
chunks.push({ prefix: tail, token: "" });
|
|
494
|
-
}
|
|
495
|
-
return chunks;
|
|
496
|
-
};
|
|
497
|
-
const fromChunks = tokenise(from);
|
|
498
|
-
const toChunks = tokenise(to);
|
|
499
|
-
const tokenCount = fromChunks.filter(c => c.token).length;
|
|
500
|
-
if (tokenCount !== toChunks.filter(c => c.token).length) {
|
|
501
|
-
return mergeStrings(from, to, progress);
|
|
502
|
-
}
|
|
503
|
-
let result = "";
|
|
504
|
-
for (let i = 0, j = 0; i < fromChunks.length && j < toChunks.length;) {
|
|
505
|
-
const f = fromChunks[i];
|
|
506
|
-
const t = toChunks[j];
|
|
507
|
-
// The *prefix* (the text before the token) must be the same.
|
|
508
|
-
if (f.prefix !== t.prefix) {
|
|
509
|
-
return mergeStrings(from, to, progress);
|
|
510
|
-
}
|
|
511
|
-
// Append the unchanged prefix.
|
|
512
|
-
result += f.prefix;
|
|
513
|
-
// If we are at the *trailing* chunk (no token), just break.
|
|
514
|
-
if (!f.token && !t.token) {
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
// Blend the token according to its kind.
|
|
518
|
-
let blended;
|
|
519
|
-
if (f.token.startsWith("#")) {
|
|
520
|
-
blended = blendColours(f.token, t.token, progress);
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
const fNum = parseFloat(f.token);
|
|
524
|
-
const tNum = parseFloat(t.token);
|
|
525
|
-
const blendedNum = blendNumbers(fNum, tNum, progress);
|
|
526
|
-
blended = blendedNum.toString();
|
|
527
|
-
}
|
|
528
|
-
result += blended;
|
|
529
|
-
// Advance both pointers.
|
|
530
|
-
i++;
|
|
531
|
-
j++;
|
|
532
|
-
}
|
|
533
|
-
return result;
|
|
534
|
-
}
|
|
535
|
-
;
|
|
536
|
-
function createEmitter(onListen, api) {
|
|
537
|
-
const emitter = Object.create(api ?? {}, {
|
|
538
|
-
listen: {
|
|
539
|
-
value: (handler) => {
|
|
540
|
-
const uniqueHandler = (value) => {
|
|
541
|
-
handler(value);
|
|
542
|
-
};
|
|
543
|
-
return onListen(uniqueHandler);
|
|
544
|
-
},
|
|
545
|
-
},
|
|
546
|
-
map: {
|
|
547
|
-
value: (mapFunc) => {
|
|
548
|
-
return createEmitter(handler => {
|
|
549
|
-
const pipedHandler = (value) => {
|
|
550
|
-
handler(mapFunc(value));
|
|
551
|
-
};
|
|
552
|
-
return onListen(pipedHandler);
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
filter: {
|
|
557
|
-
value: (filterFunc) => {
|
|
558
|
-
return createEmitter(handler => {
|
|
559
|
-
const filteredHandler = (value) => {
|
|
560
|
-
if (filterFunc(value))
|
|
561
|
-
handler(value);
|
|
562
|
-
};
|
|
563
|
-
return onListen(filteredHandler);
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
noRepeat: {
|
|
568
|
-
value: (compare) => {
|
|
569
|
-
let previous = null;
|
|
570
|
-
return createEmitter(handler => {
|
|
571
|
-
const filteredHandler = (value) => {
|
|
572
|
-
if (!previous || (compare ? !compare(previous.value, value) : (previous.value !== value))) {
|
|
573
|
-
handler(value);
|
|
574
|
-
previous = { value };
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
return onListen(filteredHandler);
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
return emitter;
|
|
583
|
-
}
|
|
584
|
-
function createProgressEmitter(onListen, api) {
|
|
585
|
-
return createEmitter(onListen, Object.create(api, {
|
|
586
|
-
ease: {
|
|
587
|
-
value: (easer) => {
|
|
588
|
-
const easerFunc = typeof easer == "string"
|
|
589
|
-
? exports.easers[easer]
|
|
590
|
-
: easer;
|
|
591
|
-
return createProgressEmitter(easer ? handler => {
|
|
592
|
-
const pipedHandler = (value) => {
|
|
593
|
-
handler(easerFunc(value));
|
|
594
|
-
};
|
|
595
|
-
return onListen(pipedHandler);
|
|
596
|
-
} : onListen, {});
|
|
597
|
-
}
|
|
598
|
-
},
|
|
599
|
-
tween: {
|
|
600
|
-
value: (from, to) => createEmitter(handler => {
|
|
601
|
-
const tweenedHandler = (progress) => {
|
|
602
|
-
const value = tweenValue(from, to, progress);
|
|
603
|
-
handler(value);
|
|
604
|
-
};
|
|
605
|
-
return onListen(tweenedHandler);
|
|
606
|
-
})
|
|
607
|
-
}
|
|
608
|
-
}));
|
|
609
|
-
}
|
|
610
|
-
const overshoot = 1.70158;
|
|
611
|
-
exports.easers = {
|
|
612
|
-
linear: (x) => x,
|
|
613
|
-
easeIn: (x) => x * x,
|
|
614
|
-
easeIn4: (x) => Math.pow(x, 4),
|
|
615
|
-
easeOut: (x) => 1 - Math.pow((1 - x), 2),
|
|
616
|
-
easeOut4: (x) => 1 - Math.pow((1 - x), 4),
|
|
617
|
-
circleIn: (x) => 1 - Math.sqrt(1 - Math.pow(x, 2)),
|
|
618
|
-
circleIn4: (x) => 1 - Math.sqrt(1 - Math.pow(x, 4)),
|
|
619
|
-
circleOut: (x) => Math.sqrt(1 - Math.pow((1 - x), 2)),
|
|
620
|
-
circleOut4: (x) => Math.sqrt(1 - Math.pow((1 - x), 4)),
|
|
621
|
-
easeInOut: (x) => -Math.cos(x * Math.PI) / 2 + .5,
|
|
622
|
-
elastic: (x) => 1 - Math.cos(4 * Math.PI * x) * (1 - x),
|
|
623
|
-
overshootIn: (x) => --x * x * ((overshoot + 1) * x + overshoot) + 1,
|
|
624
|
-
bounce: (x) => {
|
|
625
|
-
if (x < 4 / 11.0) {
|
|
626
|
-
return (121 * x * x) / 16.0;
|
|
627
|
-
}
|
|
628
|
-
else if (x < 8 / 11.0) {
|
|
629
|
-
return (363 / 40.0 * x * x) - (99 / 10.0 * x) + 17 / 5.0;
|
|
630
|
-
}
|
|
631
|
-
else if (x < 9 / 10.0) {
|
|
632
|
-
return (4356 / 361.0 * x * x) - (35442 / 1805.0 * x) + 16061 / 1805.0;
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
return (54 / 5.0 * x * x) - (513 / 25.0 * x) + 268 / 25.0;
|
|
636
|
-
}
|
|
637
|
-
},
|
|
638
|
-
noise: (x) => x == 0 ? 0 : (x >= 1 ? 1 : Math.random()),
|
|
639
|
-
step2: (x) => {
|
|
640
|
-
if (x < 0.333)
|
|
641
|
-
return 0;
|
|
642
|
-
if (x < 0.667)
|
|
643
|
-
return 0.5;
|
|
644
|
-
return 1;
|
|
645
|
-
},
|
|
646
|
-
step3: (x) => {
|
|
647
|
-
if (x < .25)
|
|
648
|
-
return 0;
|
|
649
|
-
if (x < .5)
|
|
650
|
-
return .333;
|
|
651
|
-
if (x < .75)
|
|
652
|
-
return .667;
|
|
653
|
-
return 1;
|
|
654
|
-
},
|
|
655
|
-
pingpong: (x) => x < .5 ? x * 2 : 1 - (x - .5) * 2,
|
|
656
|
-
};
|
|
3
|
+
exports.easers = exports.animate = exports.Timeline = void 0;
|
|
4
|
+
var timeline_1 = require("./internal/timeline");
|
|
5
|
+
Object.defineProperty(exports, "Timeline", { enumerable: true, get: function () { return timeline_1.Timeline; } });
|
|
6
|
+
Object.defineProperty(exports, "animate", { enumerable: true, get: function () { return timeline_1.animate; } });
|
|
7
|
+
var easing_1 = require("./internal/easing");
|
|
8
|
+
Object.defineProperty(exports, "easers", { enumerable: true, get: function () { return easing_1.easers; } });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Easer = (n: number) => number;
|
|
2
|
+
export declare const easers: {
|
|
3
|
+
linear: (x: number) => number;
|
|
4
|
+
easeIn: (x: number) => number;
|
|
5
|
+
easeIn4: (x: number) => number;
|
|
6
|
+
easeOut: (x: number) => number;
|
|
7
|
+
easeOut4: (x: number) => number;
|
|
8
|
+
circleIn: (x: number) => number;
|
|
9
|
+
circleIn4: (x: number) => number;
|
|
10
|
+
circleOut: (x: number) => number;
|
|
11
|
+
circleOut4: (x: number) => number;
|
|
12
|
+
easeInOut: (x: number) => number;
|
|
13
|
+
elastic: (x: number) => number;
|
|
14
|
+
overshootIn: (x: number) => number;
|
|
15
|
+
sine: (x: number) => number;
|
|
16
|
+
invert: (x: number) => number;
|
|
17
|
+
bounce: (x: number) => number;
|
|
18
|
+
noise: (x: number) => number;
|
|
19
|
+
pingpong: (x: number) => number;
|
|
20
|
+
};
|