@xtia/timeline 1.1.4 → 1.1.6
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 +20 -0
- package/internal/emitters.d.ts +6 -3
- package/internal/emitters.js +31 -22
- package/internal/range.d.ts +9 -7
- package/internal/range.js +15 -14
- package/internal/timeline.d.ts +10 -8
- package/internal/timeline.js +121 -108
- package/internal/tween.js +8 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -464,6 +464,26 @@ timeline
|
|
|
464
464
|
|
|
465
465
|
Returns a [`ChainingInterface`](#chaininginterface-interface) representing the point at which the tween ends.
|
|
466
466
|
|
|
467
|
+
##### `apply(handler)`
|
|
468
|
+
|
|
469
|
+
Registers a handler to be invoked on every seek, after points and ranges are applied.
|
|
470
|
+
|
|
471
|
+
This is useful for systems that use Timeline's point and range emissions to manipulate state that is to be applied *at once* to another system.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
// don't wastefully render the scene for every entity update
|
|
475
|
+
timeline
|
|
476
|
+
.range(0, 1000)
|
|
477
|
+
.tween(10, 30)
|
|
478
|
+
.apply(v => scene.hero.x = v);
|
|
479
|
+
timeline
|
|
480
|
+
.range(500, 1000)
|
|
481
|
+
.tween(15, 50)
|
|
482
|
+
.apply(v => scene.monster.x = v);
|
|
483
|
+
// render when all updates for a frame are done:
|
|
484
|
+
timeline.apply(() => renderScene(scene));
|
|
485
|
+
```
|
|
486
|
+
|
|
467
487
|
##### `tween<T>(start, end, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
|
|
468
488
|
|
|
469
489
|
As above, but if the second argument is a [`TimelinePoint`](#timelinepoint-class), it will specify when on the Timeline the tween will *end*.
|
package/internal/emitters.d.ts
CHANGED
|
@@ -68,11 +68,10 @@ export declare class Emitter<T> {
|
|
|
68
68
|
* ```ts
|
|
69
69
|
* range
|
|
70
70
|
* .tween("0%", "100%")
|
|
71
|
-
* .fork(branch =>
|
|
72
|
-
* branch
|
|
71
|
+
* .fork(branch => branch
|
|
73
72
|
* .map(s => `Loading: ${s}`)
|
|
74
73
|
* .apply(s => document.title = s)
|
|
75
|
-
*
|
|
74
|
+
* )
|
|
76
75
|
* .apply(v => progressBar.style.width = v);
|
|
77
76
|
* ```
|
|
78
77
|
* @param cb
|
|
@@ -216,4 +215,8 @@ export declare class RangeProgression extends Emitter<number> {
|
|
|
216
215
|
*/
|
|
217
216
|
offset(delta: number): RangeProgression;
|
|
218
217
|
}
|
|
218
|
+
export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLast?: () => void): {
|
|
219
|
+
listen: (fn: (v: T) => void) => UnsubscribeFunc;
|
|
220
|
+
emit: (value: T) => void;
|
|
221
|
+
};
|
|
219
222
|
export {};
|
package/internal/emitters.js
CHANGED
|
@@ -90,26 +90,15 @@ export class Emitter {
|
|
|
90
90
|
* @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
|
|
91
91
|
*/
|
|
92
92
|
tap(cb) {
|
|
93
|
-
const listeners = [];
|
|
94
93
|
let parentUnsubscribe = null;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return () => {
|
|
104
|
-
const idx = listeners.indexOf(handler);
|
|
105
|
-
listeners.splice(idx, 1);
|
|
106
|
-
if (listeners.length === 0 && parentUnsubscribe) {
|
|
107
|
-
parentUnsubscribe();
|
|
108
|
-
parentUnsubscribe = null;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
return this.redirect(tappedListen);
|
|
94
|
+
const { emit, listen } = createListenable(() => parentUnsubscribe = this.onListen(value => {
|
|
95
|
+
cb(value);
|
|
96
|
+
emit(value);
|
|
97
|
+
}), () => {
|
|
98
|
+
parentUnsubscribe();
|
|
99
|
+
parentUnsubscribe = null;
|
|
100
|
+
});
|
|
101
|
+
return this.redirect(listen);
|
|
113
102
|
}
|
|
114
103
|
/**
|
|
115
104
|
* Immediately passes this emitter to a callback and returns this emitter
|
|
@@ -120,11 +109,10 @@ export class Emitter {
|
|
|
120
109
|
* ```ts
|
|
121
110
|
* range
|
|
122
111
|
* .tween("0%", "100%")
|
|
123
|
-
* .fork(branch =>
|
|
124
|
-
* branch
|
|
112
|
+
* .fork(branch => branch
|
|
125
113
|
* .map(s => `Loading: ${s}`)
|
|
126
114
|
* .apply(s => document.title = s)
|
|
127
|
-
*
|
|
115
|
+
* )
|
|
128
116
|
* .apply(v => progressBar.style.width = v);
|
|
129
117
|
* ```
|
|
130
118
|
* @param cb
|
|
@@ -280,3 +268,24 @@ export class RangeProgression extends Emitter {
|
|
|
280
268
|
return new RangeProgression(handler => this.onListen(value => handler((value + delta) % 1)));
|
|
281
269
|
}
|
|
282
270
|
}
|
|
271
|
+
export function createListenable(onAddFirst, onRemoveLast) {
|
|
272
|
+
const handlers = [];
|
|
273
|
+
const addListener = (fn) => {
|
|
274
|
+
const unique = (v) => fn(v);
|
|
275
|
+
handlers.push(unique);
|
|
276
|
+
if (onAddFirst && handlers.length == 1)
|
|
277
|
+
onAddFirst();
|
|
278
|
+
return () => {
|
|
279
|
+
const idx = handlers.indexOf(unique);
|
|
280
|
+
if (idx === -1)
|
|
281
|
+
throw new Error("Handler already unsubscribed");
|
|
282
|
+
handlers.splice(idx, 1);
|
|
283
|
+
if (onRemoveLast && handlers.length == 0)
|
|
284
|
+
onRemoveLast();
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
listen: addListener,
|
|
289
|
+
emit: (value) => handlers.forEach(h => h(value)),
|
|
290
|
+
};
|
|
291
|
+
}
|
package/internal/range.d.ts
CHANGED
|
@@ -4,18 +4,20 @@ import { TimelinePoint } from "./point";
|
|
|
4
4
|
import { Timeline } from "./timeline";
|
|
5
5
|
export declare class TimelineRange extends RangeProgression {
|
|
6
6
|
private timeline;
|
|
7
|
-
private startPosition;
|
|
8
|
-
/** The duration of this range */
|
|
9
|
-
readonly duration: number;
|
|
10
|
-
private endPosition;
|
|
11
7
|
/** The point on the Timeline at which this range begins */
|
|
12
8
|
readonly start: TimelinePoint;
|
|
13
9
|
/** The point on the Timeline at which this range ends */
|
|
14
10
|
readonly end: TimelinePoint;
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
private startPosition;
|
|
12
|
+
private endPosition;
|
|
17
13
|
/** The duration of this range */
|
|
18
|
-
duration: number
|
|
14
|
+
readonly duration: number;
|
|
15
|
+
/** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
|
|
16
|
+
constructor(onListen: ListenFunc<number>, timeline: Timeline,
|
|
17
|
+
/** The point on the Timeline at which this range begins */
|
|
18
|
+
start: TimelinePoint,
|
|
19
|
+
/** The point on the Timeline at which this range ends */
|
|
20
|
+
end: TimelinePoint);
|
|
19
21
|
protected redirect(listen: ListenFunc<number>): TimelineRange;
|
|
20
22
|
/**
|
|
21
23
|
* Creates two ranges by seperating one at a given point
|
package/internal/range.js
CHANGED
|
@@ -2,23 +2,21 @@ import { RangeProgression } from "./emitters";
|
|
|
2
2
|
import { TimelinePoint } from "./point";
|
|
3
3
|
export class TimelineRange extends RangeProgression {
|
|
4
4
|
/** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
|
|
5
|
-
constructor(onListen, timeline,
|
|
6
|
-
/** The
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
: onListen);
|
|
5
|
+
constructor(onListen, timeline,
|
|
6
|
+
/** The point on the Timeline at which this range begins */
|
|
7
|
+
start,
|
|
8
|
+
/** The point on the Timeline at which this range ends */
|
|
9
|
+
end) {
|
|
10
|
+
super(onListen);
|
|
13
11
|
this.timeline = timeline;
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
12
|
+
this.start = start;
|
|
13
|
+
this.end = end;
|
|
14
|
+
this.startPosition = start.position;
|
|
15
|
+
this.endPosition = end.position;
|
|
16
|
+
this.duration = this.endPosition - this.startPosition;
|
|
19
17
|
}
|
|
20
18
|
redirect(listen) {
|
|
21
|
-
return new TimelineRange(listen, this.timeline, this.
|
|
19
|
+
return new TimelineRange(listen, this.timeline, this.start, this.end);
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
22
|
* Creates two ranges by seperating one at a given point
|
|
@@ -28,6 +26,9 @@ export class TimelineRange extends RangeProgression {
|
|
|
28
26
|
* @returns Tuple of two ranges
|
|
29
27
|
*/
|
|
30
28
|
bisect(position = this.duration / 2) {
|
|
29
|
+
if (position >= this.endPosition) {
|
|
30
|
+
throw new RangeError("Bisection position is beyond end of range");
|
|
31
|
+
}
|
|
31
32
|
return [
|
|
32
33
|
this.timeline.range(this.startPosition, position),
|
|
33
34
|
this.timeline.range(position + this.startPosition, this.duration - position),
|
package/internal/timeline.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Easer, easers } from "./easing";
|
|
2
|
-
import { RangeProgression } from "./emitters";
|
|
2
|
+
import { RangeProgression, UnsubscribeFunc } from "./emitters";
|
|
3
3
|
import { TimelinePoint } from "./point";
|
|
4
4
|
import { TimelineRange } from "./range";
|
|
5
5
|
import { Tweenable } from "./tween";
|
|
@@ -41,13 +41,15 @@ export declare class Timeline {
|
|
|
41
41
|
private smoothSeeker;
|
|
42
42
|
private seeking;
|
|
43
43
|
readonly start: TimelinePoint;
|
|
44
|
-
private
|
|
44
|
+
private _frameEvents;
|
|
45
|
+
/**
|
|
46
|
+
* Registers a handler to be invoked on every seek, after points and ranges are applied
|
|
47
|
+
*/
|
|
48
|
+
apply(handler: () => void): UnsubscribeFunc;
|
|
45
49
|
private _progression;
|
|
46
50
|
/**
|
|
47
|
-
* Listenable: emits a progression value (0..1)
|
|
48
|
-
* position changes, and when the Timeline's total duration is extended
|
|
49
|
-
*
|
|
50
|
-
* **Experimental**
|
|
51
|
+
* Listenable: emits a progression value (0..1), representing progression through the entire Timeline,
|
|
52
|
+
* when the Timeline's internal position changes, and when the Timeline's total duration is extended
|
|
51
53
|
*/
|
|
52
54
|
get progression(): RangeProgression;
|
|
53
55
|
constructor();
|
|
@@ -100,10 +102,9 @@ export declare class Timeline {
|
|
|
100
102
|
*/
|
|
101
103
|
range(start: number | TimelinePoint, duration: number): TimelineRange;
|
|
102
104
|
/**
|
|
103
|
-
*
|
|
105
|
+
* Defines a range from position 0 to the Timeline's **current** final position
|
|
104
106
|
*/
|
|
105
107
|
range(): TimelineRange;
|
|
106
|
-
private getWrappedPosition;
|
|
107
108
|
/**
|
|
108
109
|
* Seeks the Timeline to a specified position, triggering in order any point and range subscriptions between its current and new positions
|
|
109
110
|
* @param toPosition
|
|
@@ -121,6 +122,7 @@ export declare class Timeline {
|
|
|
121
122
|
seek(toPosition: number | TimelinePoint, durationMs: number, easer?: Easer | keyof typeof easers): Promise<void>;
|
|
122
123
|
seek(toPosition: number | TimelinePoint, duration: Period, easer?: Easer | keyof typeof easers): Promise<void>;
|
|
123
124
|
private seekDirect;
|
|
125
|
+
private seekWrapped;
|
|
124
126
|
private seekPoints;
|
|
125
127
|
private seekRanges;
|
|
126
128
|
private sortEntries;
|
package/internal/timeline.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RangeProgression } from "./emitters";
|
|
1
|
+
import { createListenable, RangeProgression } from "./emitters";
|
|
2
2
|
import { TimelinePoint } from "./point";
|
|
3
3
|
import { TimelineRange } from "./range";
|
|
4
4
|
import { clamp } from "./utils";
|
|
@@ -27,15 +27,31 @@ export class Timeline {
|
|
|
27
27
|
return this.point(this._endPosition);
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
* Registers a handler to be invoked on every seek, after points and ranges are applied
|
|
31
|
+
*/
|
|
32
|
+
apply(handler) {
|
|
33
|
+
if (this._frameEvents === null) {
|
|
34
|
+
const { emit, listen } = createListenable();
|
|
35
|
+
this._frameEvents = {
|
|
36
|
+
listen,
|
|
37
|
+
emit,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return this._frameEvents.listen(handler);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Listenable: emits a progression value (0..1), representing progression through the entire Timeline,
|
|
44
|
+
* when the Timeline's internal position changes, and when the Timeline's total duration is extended
|
|
34
45
|
*/
|
|
35
46
|
get progression() {
|
|
36
|
-
if (this._progression === null)
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
if (this._progression === null) {
|
|
48
|
+
const { emit, listen } = createListenable();
|
|
49
|
+
this._progression = {
|
|
50
|
+
emitter: new TimelineProgressionEmitter(listen),
|
|
51
|
+
emit,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return this._progression.emitter;
|
|
39
55
|
}
|
|
40
56
|
constructor(autoplay = false, endAction = "pause") {
|
|
41
57
|
/**
|
|
@@ -53,7 +69,7 @@ export class Timeline {
|
|
|
53
69
|
this.smoothSeeker = null;
|
|
54
70
|
this.seeking = false;
|
|
55
71
|
this.start = this.point(0);
|
|
56
|
-
this.
|
|
72
|
+
this._frameEvents = null;
|
|
57
73
|
this._progression = null;
|
|
58
74
|
if (endAction == "loop")
|
|
59
75
|
endAction = "restart";
|
|
@@ -91,32 +107,25 @@ export class Timeline {
|
|
|
91
107
|
point(position) {
|
|
92
108
|
if (position > this._endPosition) {
|
|
93
109
|
this._endPosition = position;
|
|
94
|
-
this.
|
|
110
|
+
this._progression?.emit(this._currentTime / position);
|
|
95
111
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
112
|
+
const { emit, listen } = createListenable(() => this.points.push(data), () => {
|
|
113
|
+
const idx = this.points.indexOf(data);
|
|
114
|
+
this.points.splice(idx, 1);
|
|
115
|
+
});
|
|
101
116
|
const addHandler = (handler) => {
|
|
102
117
|
if (this.seeking)
|
|
103
118
|
throw new Error("Can't add a listener while seeking");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
if (position == this._currentTime) {
|
|
120
|
+
emit({
|
|
121
|
+
direction: 1
|
|
122
|
+
});
|
|
108
123
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
handlers.splice(idx, 1);
|
|
115
|
-
if (handlers.length == 0) {
|
|
116
|
-
const idx = this.points.indexOf(data);
|
|
117
|
-
this.points.splice(idx, 1);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
124
|
+
return listen(handler);
|
|
125
|
+
};
|
|
126
|
+
const data = {
|
|
127
|
+
emit,
|
|
128
|
+
position,
|
|
120
129
|
};
|
|
121
130
|
return new TimelinePoint(addHandler, this, position);
|
|
122
131
|
}
|
|
@@ -126,62 +135,36 @@ export class Timeline {
|
|
|
126
135
|
: start;
|
|
127
136
|
const startPosition = startPoint.position;
|
|
128
137
|
const duration = optionalDuration ?? this._endPosition - startPosition;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
const endPoint = this.point(startPosition + duration);
|
|
139
|
+
const { emit, listen } = createListenable(() => this.ranges.push(rangeData), () => {
|
|
140
|
+
const idx = this.ranges.indexOf(rangeData);
|
|
141
|
+
this.ranges.splice(idx, 1);
|
|
142
|
+
});
|
|
133
143
|
const rangeData = {
|
|
134
144
|
position: startPosition,
|
|
135
145
|
duration,
|
|
136
|
-
|
|
146
|
+
emit,
|
|
137
147
|
};
|
|
138
|
-
const addHandler =
|
|
139
|
-
|
|
140
|
-
throw new Error("
|
|
141
|
-
if (handlers.length == 0) {
|
|
142
|
-
this.ranges.push(rangeData);
|
|
143
|
-
this.currentSortDirection = 0;
|
|
144
|
-
}
|
|
145
|
-
handlers.push(handler);
|
|
146
|
-
// if currentTime is in this range, apply immediately
|
|
147
|
-
if (range.contains(this._currentTime)) {
|
|
148
|
-
let progress = clamp((this._currentTime - startPosition) / duration, 0, 1);
|
|
149
|
-
handler(progress);
|
|
148
|
+
const addHandler = duration == 0
|
|
149
|
+
? () => {
|
|
150
|
+
throw new Error("Zero-duration ranges may not be listened");
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const idx = this.ranges.indexOf(rangeData);
|
|
158
|
-
this.ranges.splice(idx, 1);
|
|
152
|
+
: (handler) => {
|
|
153
|
+
if (this.seeking)
|
|
154
|
+
throw new Error("Can't add a listener while seeking");
|
|
155
|
+
if (range.contains(this._currentTime)) {
|
|
156
|
+
let progress = clamp((this._currentTime - startPosition) / duration, 0, 1);
|
|
157
|
+
handler(progress);
|
|
159
158
|
}
|
|
159
|
+
return listen(handler);
|
|
160
160
|
};
|
|
161
|
-
|
|
162
|
-
const range = new TimelineRange(addHandler, this, startPosition, duration);
|
|
161
|
+
const range = new TimelineRange(addHandler, this, startPoint, endPoint);
|
|
163
162
|
return range;
|
|
164
163
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (wrapAt == 0)
|
|
170
|
-
return n % this._endPosition;
|
|
171
|
-
if (n <= this._endPosition)
|
|
172
|
-
return n;
|
|
173
|
-
const loopStart = wrapAt;
|
|
174
|
-
const segment = this._endPosition - loopStart;
|
|
175
|
-
if (segment <= 0)
|
|
176
|
-
return Math.min(n, this._endPosition);
|
|
177
|
-
const overflow = n - this._endPosition;
|
|
178
|
-
const remainder = overflow % segment;
|
|
179
|
-
return loopStart + remainder;
|
|
180
|
-
}
|
|
181
|
-
seek(to, duration = 0, easer) {
|
|
182
|
-
const durationMs = typeof duration == "number"
|
|
183
|
-
? duration
|
|
184
|
-
: duration.asMilliseconds;
|
|
164
|
+
seek(to, duration, easer) {
|
|
165
|
+
const durationMs = typeof duration == "object"
|
|
166
|
+
? duration.asMilliseconds
|
|
167
|
+
: duration;
|
|
185
168
|
const toPosition = typeof to == "number"
|
|
186
169
|
? to
|
|
187
170
|
: to.position;
|
|
@@ -192,21 +175,24 @@ export class Timeline {
|
|
|
192
175
|
this.smoothSeeker.pause();
|
|
193
176
|
// ensure any awaits are resolved for the interrupted seek
|
|
194
177
|
const interruptPosition = this._currentTime;
|
|
195
|
-
this.smoothSeeker.
|
|
178
|
+
this.smoothSeeker.seekDirect(this.smoothSeeker.end.position);
|
|
196
179
|
this.smoothSeeker = null;
|
|
197
180
|
// and jump back to where we were interrupted
|
|
198
|
-
this.
|
|
181
|
+
this.seekDirect(interruptPosition);
|
|
199
182
|
}
|
|
200
|
-
if (durationMs
|
|
183
|
+
if (!durationMs) {
|
|
184
|
+
const fromTime = this._currentTime;
|
|
201
185
|
this.seekDirect(toPosition);
|
|
202
|
-
|
|
186
|
+
this._frameEvents?.emit();
|
|
187
|
+
// only add Promise overhead if duration is explicitly 0
|
|
188
|
+
return durationMs === 0 ? Promise.resolve() : undefined;
|
|
203
189
|
}
|
|
204
190
|
const seeker = new Timeline(true);
|
|
205
191
|
this.smoothSeeker = seeker;
|
|
206
192
|
seeker
|
|
207
193
|
.range(0, durationMs)
|
|
208
194
|
.ease(easer)
|
|
209
|
-
.tween(this.
|
|
195
|
+
.tween(this._currentTime, toPosition)
|
|
210
196
|
.apply(v => this.seekDirect(v));
|
|
211
197
|
return seeker.end.promise();
|
|
212
198
|
}
|
|
@@ -214,31 +200,65 @@ export class Timeline {
|
|
|
214
200
|
const fromPosition = this._currentTime;
|
|
215
201
|
if (toPosition === fromPosition)
|
|
216
202
|
return;
|
|
217
|
-
const loopingTo = this.getWrappedPosition(toPosition);
|
|
218
|
-
const loopingFrom = this.getWrappedPosition(fromPosition);
|
|
219
|
-
let virtualFrom = loopingFrom;
|
|
220
|
-
let virtualTo = loopingTo;
|
|
221
203
|
const direction = toPosition > fromPosition ? 1 : -1;
|
|
222
204
|
if (direction !== this.currentSortDirection)
|
|
223
205
|
this.sortEntries(direction);
|
|
224
|
-
if (direction === 1 && loopingTo < loopingFrom) {
|
|
225
|
-
virtualFrom = loopingFrom - this._endPosition;
|
|
226
|
-
}
|
|
227
|
-
else if (direction === -1 && loopingTo > loopingFrom) {
|
|
228
|
-
virtualFrom = loopingFrom + this._endPosition;
|
|
229
|
-
}
|
|
230
206
|
this.seeking = true;
|
|
231
|
-
this._currentTime = virtualFrom;
|
|
232
207
|
try {
|
|
233
|
-
|
|
234
|
-
this.
|
|
208
|
+
// use wrapping logic?
|
|
209
|
+
if (this.endAction.type === EndAction.wrap && (fromPosition > this._endPosition || toPosition > this._endPosition)) {
|
|
210
|
+
this.seekWrapped(toPosition);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.seekPoints(toPosition);
|
|
214
|
+
this.seekRanges(toPosition);
|
|
215
|
+
}
|
|
235
216
|
}
|
|
236
217
|
catch (e) {
|
|
237
218
|
this.pause();
|
|
238
219
|
throw e;
|
|
239
220
|
}
|
|
221
|
+
finally {
|
|
222
|
+
this.seeking = false;
|
|
223
|
+
}
|
|
224
|
+
this._currentTime = toPosition;
|
|
225
|
+
}
|
|
226
|
+
seekWrapped(toPosition) {
|
|
227
|
+
const fromPosition = this._currentTime;
|
|
228
|
+
const timelineEnd = this._endPosition;
|
|
229
|
+
const wrapAt = "at" in this.endAction ? this.endAction.at.position : 0;
|
|
230
|
+
const loopLen = timelineEnd - wrapAt;
|
|
231
|
+
const getWrappedPosition = (pos) => ((pos - wrapAt) % loopLen + loopLen) % loopLen + wrapAt;
|
|
232
|
+
const realDelta = toPosition - fromPosition;
|
|
233
|
+
const direction = realDelta >= 0 ? 1 : -1;
|
|
234
|
+
let remaining = Math.abs(realDelta);
|
|
235
|
+
let virtualFrom = getWrappedPosition(fromPosition);
|
|
236
|
+
while (remaining > 0) {
|
|
237
|
+
let virtualTo;
|
|
238
|
+
if (direction > 0) {
|
|
239
|
+
const wrapSize = timelineEnd - virtualFrom;
|
|
240
|
+
virtualTo = remaining <= wrapSize
|
|
241
|
+
? virtualFrom + remaining
|
|
242
|
+
: timelineEnd;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const wrapSize = virtualFrom - wrapAt;
|
|
246
|
+
virtualTo = remaining <= wrapSize
|
|
247
|
+
? virtualFrom - remaining
|
|
248
|
+
: wrapAt;
|
|
249
|
+
}
|
|
250
|
+
this._currentTime = virtualFrom;
|
|
251
|
+
this.seekPoints(virtualTo);
|
|
252
|
+
remaining -= Math.abs(virtualTo - virtualFrom);
|
|
253
|
+
if (remaining > 0) {
|
|
254
|
+
virtualFrom = direction > 0 ? wrapAt : timelineEnd;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
virtualFrom = virtualTo;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.seekRanges(getWrappedPosition(toPosition));
|
|
240
261
|
this._currentTime = toPosition;
|
|
241
|
-
this.seeking = false;
|
|
242
262
|
}
|
|
243
263
|
seekPoints(to) {
|
|
244
264
|
const from = this._currentTime;
|
|
@@ -252,7 +272,7 @@ export class Timeline {
|
|
|
252
272
|
pointsBetween.slice().forEach(p => {
|
|
253
273
|
this.seekRanges(p.position);
|
|
254
274
|
this._currentTime = p.position;
|
|
255
|
-
p.
|
|
275
|
+
p.emit(eventData);
|
|
256
276
|
});
|
|
257
277
|
}
|
|
258
278
|
seekRanges(to) {
|
|
@@ -265,10 +285,10 @@ export class Timeline {
|
|
|
265
285
|
const overlaps = fromTime <= rangeEnd && toTime >= range.position;
|
|
266
286
|
if (overlaps) {
|
|
267
287
|
let progress = clamp((to - range.position) / range.duration, 0, 1);
|
|
268
|
-
range.
|
|
288
|
+
range.emit(progress);
|
|
269
289
|
}
|
|
270
290
|
});
|
|
271
|
-
this.
|
|
291
|
+
this._progression?.emit(toTime / this._endPosition);
|
|
272
292
|
}
|
|
273
293
|
sortEntries(direction) {
|
|
274
294
|
this.currentSortDirection = direction;
|
|
@@ -391,15 +411,8 @@ export class Timeline {
|
|
|
391
411
|
}
|
|
392
412
|
}
|
|
393
413
|
class TimelineProgressionEmitter extends RangeProgression {
|
|
394
|
-
constructor(
|
|
395
|
-
super(
|
|
396
|
-
const unique = (n) => handler(n);
|
|
397
|
-
handlers.push(unique);
|
|
398
|
-
return () => {
|
|
399
|
-
const idx = handlers.indexOf(unique);
|
|
400
|
-
handlers.splice(idx, 1);
|
|
401
|
-
};
|
|
402
|
-
});
|
|
414
|
+
constructor(listen) {
|
|
415
|
+
super(listen);
|
|
403
416
|
}
|
|
404
417
|
}
|
|
405
418
|
const sortEvents = (a, b) => {
|
package/internal/tween.js
CHANGED
|
@@ -19,8 +19,11 @@ export function createTween(from, to) {
|
|
|
19
19
|
switch (typeof from) {
|
|
20
20
|
case "number": return progress => blendNumbers(from, to, progress);
|
|
21
21
|
case "object": {
|
|
22
|
-
if (from instanceof Date)
|
|
23
|
-
|
|
22
|
+
if (from instanceof Date) {
|
|
23
|
+
const fromStamp = from.getTime();
|
|
24
|
+
const toStamp = to.getTime();
|
|
25
|
+
return progress => new Date(blendNumbers(fromStamp, toStamp, progress));
|
|
26
|
+
}
|
|
24
27
|
return progress => from.blend(to, progress);
|
|
25
28
|
}
|
|
26
29
|
case "string": return createStringTween(from, to);
|
|
@@ -30,9 +33,9 @@ export function createTween(from, to) {
|
|
|
30
33
|
function createStringTween(from, to) {
|
|
31
34
|
const fromChunks = tokenise(from);
|
|
32
35
|
const toChunks = tokenise(to);
|
|
33
|
-
const tokenCount = fromChunks.
|
|
34
|
-
// where
|
|
35
|
-
if (tokenCount !== toChunks.
|
|
36
|
+
const tokenCount = fromChunks.length;
|
|
37
|
+
// where token count mismatch, use merging
|
|
38
|
+
if (tokenCount !== toChunks.length) {
|
|
36
39
|
return createStringMerge(from, to);
|
|
37
40
|
}
|
|
38
41
|
// where token prefix/type mismatch, use merging
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtia/timeline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "https://github.com/tiadrop/timeline",
|
|
6
6
|
"type": "github"
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"animation",
|
|
25
|
-
"timeline"
|
|
25
|
+
"timeline",
|
|
26
|
+
"choreography"
|
|
26
27
|
],
|
|
27
28
|
"author": "Aleta Lovelace",
|
|
28
29
|
"license": "MIT"
|