@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.
Files changed (4) hide show
  1. package/README.md +291 -0
  2. package/index.d.ts +189 -150
  3. package/index.js +543 -228
  4. 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.