@xtia/timeline 0.2.5 → 1.0.0
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 +291 -0
- package/index.d.ts +189 -150
- package/index.js +543 -228
- package/package.json +19 -13
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Timeline
|
|
2
|
+
|
|
3
|
+
### A Type‑Safe Choreography Engine for Deterministic Timelines
|
|
4
|
+
|
|
5
|
+
**Timeline** is a general‑purpose, environment-agnostic choreography engine that lets you orchestrate any sequence of value changes; numbers, vectors, colour tokens, custom blendable objects, or arbitrary data structures.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Basic Use:
|
|
9
|
+
|
|
10
|
+
`npm i @xtia/timeline`
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Timeline } from "@xtia/timeline";
|
|
14
|
+
|
|
15
|
+
// create a Timeline
|
|
16
|
+
const timeline = new Timeline();
|
|
17
|
+
|
|
18
|
+
// over the first second, fade the body's background colour
|
|
19
|
+
timeline
|
|
20
|
+
.range(0, 1000)
|
|
21
|
+
.tween("#646", "#000")
|
|
22
|
+
.listen(
|
|
23
|
+
value => document.body.style.backgroundColor = value
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// add another tween to make a slow typing effect
|
|
27
|
+
const message = "Hi, planet!";
|
|
28
|
+
timeline
|
|
29
|
+
.range(500, 2000)
|
|
30
|
+
.tween(0, message.length)
|
|
31
|
+
.listen(
|
|
32
|
+
n => element.textContent = message.substring(0, n)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// use an easing function
|
|
36
|
+
typingRange
|
|
37
|
+
.end
|
|
38
|
+
.range(3000)
|
|
39
|
+
.ease("bounce")
|
|
40
|
+
.tween("50%", "0%")
|
|
41
|
+
.listen(
|
|
42
|
+
value => element.style.marginLeft = value
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// make it go
|
|
46
|
+
timeline.play();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Ranges and Emitters
|
|
50
|
+
|
|
51
|
+
`timeline.range(start, duration)` returns an object representing a period within the Timeline.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const firstFiveSeconds = timeline.range(0, 5000);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The range object is *listenable* and emits a progression value (between 0 and 1) when the Timeline's internal position passes through or over that period.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
firstFiveSeconds
|
|
61
|
+
.listen(
|
|
62
|
+
value => console.log(`${value} is between 0 and 1`)
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Range emissions can be transformed through chains:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// multiply emitted values by 100 with map()
|
|
70
|
+
const asPercent = firstFiveSeconds.map(n => n * 100);
|
|
71
|
+
|
|
72
|
+
// use the result in a log message
|
|
73
|
+
asPercent
|
|
74
|
+
.map(n => n.toFixed(2))
|
|
75
|
+
.listen(
|
|
76
|
+
n => console.log(`We are ${n}% through the first five seconds`)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// and in a css property
|
|
80
|
+
asPercent
|
|
81
|
+
.map(n => `${n}%`)
|
|
82
|
+
.listen(
|
|
83
|
+
n => progressBar.style.width = n
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// apply easing (creates a *new* emitter)
|
|
87
|
+
const eased = firstFiveSeconds.ease("easeInOut");
|
|
88
|
+
eased.listen(
|
|
89
|
+
v => console.log(`Eased value: ${v}`)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// combine them
|
|
93
|
+
const frames = eased
|
|
94
|
+
.tween(0, 30)
|
|
95
|
+
.map(Math.floor)
|
|
96
|
+
.map(n => `animation-frame-${n}.png`)
|
|
97
|
+
.listen(filename => img.src = filename);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Range objects also provide a `play()` method that instructs the Timeline to play through that particular range:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// play through the first two seconds of the Timeline
|
|
104
|
+
timeline
|
|
105
|
+
.range(0, 2000)
|
|
106
|
+
.play();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Custom easers can be passed to `ease()` as `(progress: number) => number`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
timeline
|
|
113
|
+
.range(0, 1000)
|
|
114
|
+
.ease(n => Math.sqrt(n))
|
|
115
|
+
.tween(/*...*/);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Points
|
|
119
|
+
|
|
120
|
+
Points represent specific times in the Timeline.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const twoSecondsIn = timeline.point(2000);
|
|
124
|
+
const fiveSecondsIn = firstFiveSeconds.end;
|
|
125
|
+
const sixSecondsIn = fiveSecondsdIn.delta(1000);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Points emit `PointEvent` objects when their position is reached or passed.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
twoSecondsIn.listen(event => {
|
|
132
|
+
// event.direction (-1 | 1) tells us the direction of the seek that
|
|
133
|
+
// triggered the point. This allows for reversible point events
|
|
134
|
+
document.body.classList.toggle("someClass", event.direction > 0);
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
*Note*, point events will be triggered in order, depending on the direction of the seek that passes over them.
|
|
139
|
+
|
|
140
|
+
We can also create ranges from points:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
twoSecondsIn
|
|
144
|
+
.to(fiveSecondsIn)
|
|
145
|
+
.tween(/*...*/);
|
|
146
|
+
|
|
147
|
+
timeline
|
|
148
|
+
.end
|
|
149
|
+
.range(1000)
|
|
150
|
+
.tween(/*...*/);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
*Note*, points and ranges without active listeners are not stored, so will be garbage-collected if unreferenced.
|
|
154
|
+
|
|
155
|
+
## More on tweening
|
|
156
|
+
|
|
157
|
+
Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(from: this, to: this): this`.
|
|
158
|
+
|
|
159
|
+
#### String interpolation
|
|
160
|
+
* If the strings contain tweenable tokens (numbers, colour codes) and are otherwise identical, those tokens are interpolated
|
|
161
|
+
* Otherwise the `from` string is progressively replaced, left-to-right, with the `to` string
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// note: this looks really cool
|
|
165
|
+
timeline
|
|
166
|
+
.range(0, 2000)
|
|
167
|
+
.ease("elastic")
|
|
168
|
+
.tween("0px 0px 0px #0000", "15px 15px 20px #0005")
|
|
169
|
+
.listen(s => element.style.textShadow = s);
|
|
170
|
+
|
|
171
|
+
// text progress bar
|
|
172
|
+
timeline
|
|
173
|
+
.range(0, 2000)
|
|
174
|
+
.tween("--------", "########")
|
|
175
|
+
.listen(v => document.title = v);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Autoplay and Looping Strategies
|
|
179
|
+
|
|
180
|
+
To create a Timeline that immediately starts playing, pass `true` to its constructor:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// immediately fade in an element
|
|
184
|
+
new Timeline(true)
|
|
185
|
+
.range(0, 1000)
|
|
186
|
+
.tween(v => element.style.opacity = v);
|
|
187
|
+
|
|
188
|
+
// note, an `animate(duration)` function is exported for
|
|
189
|
+
// disposable, single-use animations such as this:
|
|
190
|
+
import { animate } from "@xtia/timeline";
|
|
191
|
+
animate(1000)
|
|
192
|
+
.tween(v => element.style.opacity = v);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Normally a Timeline will simply stop playing when it reaches the end. This can be changed by passing a second argument (`endAction`) to the constructor.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// "restart" looping strategy: when its end is passed by play(),
|
|
199
|
+
// it will seek back to 0, then forward to consistently account
|
|
200
|
+
// for any overshoot
|
|
201
|
+
const repeatingTimeline = new Timeline(true, "restart");
|
|
202
|
+
|
|
203
|
+
// "wrap" looping strategy: the Timeline will continue playing
|
|
204
|
+
// beyond its end point, but points and ranges will trigger as
|
|
205
|
+
// if the Timeline was looping
|
|
206
|
+
const wrappingTimeline = new Timeline(true, "wrap");
|
|
207
|
+
|
|
208
|
+
// "continue" allows the Timeline to ignore its end point and
|
|
209
|
+
// keep playing
|
|
210
|
+
const foreverTimeline = new Timeline(true, "continue");
|
|
211
|
+
|
|
212
|
+
// "pause" is the default behaviour: stop at the end
|
|
213
|
+
const puasingTimeline = new Timeline(true, "pause");
|
|
214
|
+
|
|
215
|
+
// "restart" and "wrap" strategies can designate a position
|
|
216
|
+
// to loop back to
|
|
217
|
+
new Timeline(true, {restartAt: 1000});
|
|
218
|
+
new Timeline(true, {wrapAt: 1000});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Seeking
|
|
222
|
+
|
|
223
|
+
To seek to a position, we can either call `timeline.seek(n)` or set `timeline.currentTime`.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
timeline.seek(1500);
|
|
227
|
+
timeline.currentTime += 500;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Seeking lets us control a Timeline with anything:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// syncronise with a video, to show subtitles or related
|
|
234
|
+
// activities:
|
|
235
|
+
videoElement.addEventListener(
|
|
236
|
+
() => timeline.seek(videoElement.currentTime)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// control a Timeline using page scroll
|
|
240
|
+
window.addEventListener(
|
|
241
|
+
"scroll",
|
|
242
|
+
() => timeline.seek(window.scrollY)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// represent real time
|
|
246
|
+
setInterval(() => timeline.seek(Date.now()), 1000);
|
|
247
|
+
timeline
|
|
248
|
+
.point(new Date("2026-10-31").getTime())
|
|
249
|
+
.listen(() => console.log("Happy anniversary 🏳️⚧️💗"));
|
|
250
|
+
|
|
251
|
+
// show a progress bar for loaded resources
|
|
252
|
+
const loadingTimeline = new Timeline();
|
|
253
|
+
loadingTimeline
|
|
254
|
+
.range(0, resourceUrls.length)
|
|
255
|
+
.tween("0%", "100%");
|
|
256
|
+
.listen(v => progressBar.style.width = v);
|
|
257
|
+
|
|
258
|
+
loadingTimeline
|
|
259
|
+
.end
|
|
260
|
+
.listen(startGame);
|
|
261
|
+
|
|
262
|
+
resourceUrls.forEach(url => {
|
|
263
|
+
preload(url).then(
|
|
264
|
+
() => loadingTimeline.currentTime++
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Backward-compatibility
|
|
270
|
+
|
|
271
|
+
Despite the massive overhaul, the previous API is present and expanded and upgrading to 1.0.0 should be frictionless in the vast majority of cases.
|
|
272
|
+
|
|
273
|
+
#### Breaking changes
|
|
274
|
+
|
|
275
|
+
* `timeline.end` now provides a `TimelinePoint` instead of `number`.
|
|
276
|
+
|
|
277
|
+
#### Mitigation
|
|
278
|
+
|
|
279
|
+
* `timeline.tween()` now accepts TimelinePoint as a starting position, and provides an overload that replaces the `duration: number` parameter with `end: TimelinePoint`.
|
|
280
|
+
* Should you encounter a case where this change still causes issue, eg `tl.tween(0, tl.end / 2, ...)`, `tl.end.position` is equivalent to the old API's `tl.end`.
|
|
281
|
+
|
|
282
|
+
#### Enhancements (non-breaking)
|
|
283
|
+
|
|
284
|
+
* `timeline.tween()` also now accepts non-numeric `from` and `to` values per `ProgressEmitter.tween<T>()`.
|
|
285
|
+
* The chaining interface returned by `tween()` and `at()` now includes property `end: TimelinePoint`, to take advantage of the new functional API from existing tween chains.
|
|
286
|
+
|
|
287
|
+
#### Deprecations
|
|
288
|
+
|
|
289
|
+
* `timeline.position` will be replaced with `timeline.currentTime` to be consistent with other seekable concepts.
|
|
290
|
+
* `"loop"` endAction is now `"restart"` to disambiguate from new looping strategies.
|
|
291
|
+
* `timeline.step()` is redundant now that `currentTime` is writable; use `timeline.currentTime += delta` instead.
|