@flighthq/timeline 0.1.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/movieClip.d.ts +24 -0
- package/dist/movieClip.d.ts.map +1 -0
- package/dist/movieClip.js +123 -0
- package/dist/movieClip.js.map +1 -0
- package/dist/timeline.d.ts +23 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +211 -0
- package/dist/timeline.js.map +1 -0
- package/package.json +39 -0
- package/src/movieClip.test.ts +430 -0
- package/src/timeline.test.ts +505 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import type { DisplayObject, Timeline } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
addTimelineFrameScript,
|
|
5
|
+
createTimeline,
|
|
6
|
+
createTimelineSource,
|
|
7
|
+
disposeTimelineSignals,
|
|
8
|
+
enableTimelineSignals,
|
|
9
|
+
findTimelineLabel,
|
|
10
|
+
getTimelineCurrentLabel,
|
|
11
|
+
getTimelineFrameScript,
|
|
12
|
+
gotoAndPlayTimeline,
|
|
13
|
+
gotoAndStopTimeline,
|
|
14
|
+
nextFrameTimeline,
|
|
15
|
+
playTimeline,
|
|
16
|
+
prevFrameTimeline,
|
|
17
|
+
removeTimelineFrameScript,
|
|
18
|
+
stopTimeline,
|
|
19
|
+
updateTimeline,
|
|
20
|
+
} from './timeline';
|
|
21
|
+
|
|
22
|
+
interface MakeOptions {
|
|
23
|
+
totalFrames?: number;
|
|
24
|
+
frameRate?: number | null;
|
|
25
|
+
labels?: { name: string; frame: number }[];
|
|
26
|
+
constructFrame?: (frame: number) => void;
|
|
27
|
+
currentFrame?: number;
|
|
28
|
+
isPlaying?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Builds a timeline backed by a source (totalFrames/frameRate/labels/constructFrame) with a mock target
|
|
32
|
+
// so constructFrame fires. Test callbacks take just the frame; the source adapts to (target, frame).
|
|
33
|
+
function make(o: MakeOptions = {}): Timeline {
|
|
34
|
+
return createTimeline({
|
|
35
|
+
source: createTimelineSource({
|
|
36
|
+
totalFrames: o.totalFrames ?? 4,
|
|
37
|
+
frameRate: o.frameRate === undefined ? 10 : o.frameRate,
|
|
38
|
+
labels: o.labels,
|
|
39
|
+
constructFrame: o.constructFrame ? (_target, frame) => o.constructFrame!(frame) : undefined,
|
|
40
|
+
}),
|
|
41
|
+
target: {} as DisplayObject,
|
|
42
|
+
currentFrame: o.currentFrame,
|
|
43
|
+
isPlaying: o.isPlaying,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('addTimelineFrameScript', () => {
|
|
48
|
+
it('attaches a script that fires once on frame entry', () => {
|
|
49
|
+
const fired: number[] = [];
|
|
50
|
+
const t = make({ frameRate: null });
|
|
51
|
+
addTimelineFrameScript(t, 2, (_target, f) => fired.push(f));
|
|
52
|
+
playTimeline(t);
|
|
53
|
+
updateTimeline(t, 0); // frame 1 — no script
|
|
54
|
+
updateTimeline(t, 0); // frame 2 — script fires
|
|
55
|
+
expect(fired).toEqual([2]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not re-fire the script on repeated updates to the same stopped frame', () => {
|
|
59
|
+
const fired: number[] = [];
|
|
60
|
+
const t = make({ frameRate: null });
|
|
61
|
+
addTimelineFrameScript(t, 2, (_target, f) => fired.push(f));
|
|
62
|
+
playTimeline(t);
|
|
63
|
+
updateTimeline(t, 0); // frame 1
|
|
64
|
+
updateTimeline(t, 0); // frame 2 — fires once
|
|
65
|
+
stopTimeline(t);
|
|
66
|
+
updateTimeline(t, 0); // still frame 2, stopped — no re-fire
|
|
67
|
+
updateTimeline(t, 0); // still frame 2, stopped — no re-fire
|
|
68
|
+
expect(fired).toEqual([2]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('accepts a label string as the frame argument', () => {
|
|
72
|
+
const fired: number[] = [];
|
|
73
|
+
const t = make({ frameRate: null, labels: [{ name: 'run', frame: 3 }] });
|
|
74
|
+
addTimelineFrameScript(t, 'run', (_target, f) => fired.push(f));
|
|
75
|
+
gotoAndStopTimeline(t, 3);
|
|
76
|
+
expect(fired).toEqual([3]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('createTimeline', () => {
|
|
81
|
+
it('starts at frame 1, stopped, lastFrameUpdate -1', () => {
|
|
82
|
+
const t = make();
|
|
83
|
+
expect(t.currentFrame).toBe(1);
|
|
84
|
+
expect(t.isPlaying).toBe(false);
|
|
85
|
+
expect(t.lastFrameUpdate).toBe(-1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('defaults playMode to loop', () => {
|
|
89
|
+
const t = make();
|
|
90
|
+
expect(t.playMode).toBe('loop');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('defaults frameScripts to null', () => {
|
|
94
|
+
const t = make();
|
|
95
|
+
expect(t.frameScripts).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('defaults signals to null', () => {
|
|
99
|
+
const t = make();
|
|
100
|
+
expect(t.signals).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies overrides', () => {
|
|
104
|
+
const t = make({ currentFrame: 3, frameRate: 24 });
|
|
105
|
+
expect(t.currentFrame).toBe(3);
|
|
106
|
+
expect(t.source?.frameRate).toBe(24);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('createTimelineSource', () => {
|
|
111
|
+
it('builds a source with defaults', () => {
|
|
112
|
+
const s = createTimelineSource({});
|
|
113
|
+
expect(s.totalFrames).toBe(1);
|
|
114
|
+
expect(s.frameRate).toBeNull();
|
|
115
|
+
expect(s.labels).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('carries provided fields and invokes constructFrame with (target, frame)', () => {
|
|
119
|
+
const seen: number[] = [];
|
|
120
|
+
const s = createTimelineSource({
|
|
121
|
+
totalFrames: 3,
|
|
122
|
+
frameRate: 12,
|
|
123
|
+
labels: [{ name: 'a', frame: 2 }],
|
|
124
|
+
constructFrame: (_target, frame) => seen.push(frame),
|
|
125
|
+
});
|
|
126
|
+
expect(s.totalFrames).toBe(3);
|
|
127
|
+
expect(s.frameRate).toBe(12);
|
|
128
|
+
expect(s.labels).toEqual([{ name: 'a', frame: 2 }]);
|
|
129
|
+
s.constructFrame({} as unknown as DisplayObject, 2);
|
|
130
|
+
expect(seen).toEqual([2]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('disposeTimelineSignals', () => {
|
|
135
|
+
it('clears timeline.signals to null', () => {
|
|
136
|
+
const t = make();
|
|
137
|
+
enableTimelineSignals(t);
|
|
138
|
+
expect(t.signals).not.toBeNull();
|
|
139
|
+
disposeTimelineSignals(t);
|
|
140
|
+
expect(t.signals).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('is idempotent — no-op when signals are already null', () => {
|
|
144
|
+
const t = make();
|
|
145
|
+
expect(() => disposeTimelineSignals(t)).not.toThrow();
|
|
146
|
+
expect(t.signals).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('allows enableTimelineSignals to re-arm after dispose', () => {
|
|
150
|
+
const t = make();
|
|
151
|
+
const first = enableTimelineSignals(t);
|
|
152
|
+
disposeTimelineSignals(t);
|
|
153
|
+
const second = enableTimelineSignals(t);
|
|
154
|
+
expect(second).not.toBe(first);
|
|
155
|
+
expect(t.signals).toBe(second);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('enableTimelineSignals', () => {
|
|
160
|
+
it('returns a TimelineSignals group with all lifecycle signals defined', () => {
|
|
161
|
+
const t = make();
|
|
162
|
+
const signals = enableTimelineSignals(t);
|
|
163
|
+
expect(signals.onEnterFrame).toBeDefined();
|
|
164
|
+
expect(signals.onExitFrame).toBeDefined();
|
|
165
|
+
expect(signals.onFrameConstructed).toBeDefined();
|
|
166
|
+
expect(signals.onComplete).toBeDefined();
|
|
167
|
+
expect(signals.onLoop).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('is idempotent — returns the same group on subsequent calls', () => {
|
|
171
|
+
const t = make();
|
|
172
|
+
expect(enableTimelineSignals(t)).toBe(enableTimelineSignals(t));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('stores the signals on timeline.signals', () => {
|
|
176
|
+
const t = make();
|
|
177
|
+
const signals = enableTimelineSignals(t);
|
|
178
|
+
expect(t.signals).toBe(signals);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('emits onEnterFrame with correct frame and previousFrame when frame changes', () => {
|
|
182
|
+
const events: { frame: number; previousFrame: number }[] = [];
|
|
183
|
+
const t = make({ frameRate: null });
|
|
184
|
+
const signals = enableTimelineSignals(t);
|
|
185
|
+
signals.onEnterFrame.emit = (event) => events.push({ frame: event.frame, previousFrame: event.previousFrame });
|
|
186
|
+
playTimeline(t);
|
|
187
|
+
updateTimeline(t, 0); // frame 1 (was -1 sentinel)
|
|
188
|
+
updateTimeline(t, 0); // frame 2
|
|
189
|
+
expect(events[0].frame).toBe(1);
|
|
190
|
+
expect(events[1].frame).toBe(2);
|
|
191
|
+
expect(events[1].previousFrame).toBe(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('emits onExitFrame before frame changes', () => {
|
|
195
|
+
const order: string[] = [];
|
|
196
|
+
const t = make({ frameRate: null });
|
|
197
|
+
const signals = enableTimelineSignals(t);
|
|
198
|
+
signals.onExitFrame.emit = () => order.push('exit');
|
|
199
|
+
signals.onEnterFrame.emit = () => order.push('enter');
|
|
200
|
+
signals.onFrameConstructed.emit = () => order.push('constructed');
|
|
201
|
+
playTimeline(t);
|
|
202
|
+
updateTimeline(t, 0); // first frame entry
|
|
203
|
+
expect(order).toEqual(['exit', 'enter', 'constructed']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('emits onLoop when the timeline wraps in loop mode', () => {
|
|
207
|
+
let looped = false;
|
|
208
|
+
const t = make({ totalFrames: 2, frameRate: null });
|
|
209
|
+
const signals = enableTimelineSignals(t);
|
|
210
|
+
signals.onLoop.emit = () => {
|
|
211
|
+
looped = true;
|
|
212
|
+
};
|
|
213
|
+
playTimeline(t);
|
|
214
|
+
updateTimeline(t, 0); // frame 1
|
|
215
|
+
updateTimeline(t, 0); // frame 2
|
|
216
|
+
updateTimeline(t, 0); // wraps to frame 1 → onLoop
|
|
217
|
+
expect(looped).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('emits onComplete and stops when playMode is once and last frame is reached', () => {
|
|
221
|
+
let completed = false;
|
|
222
|
+
const t = make({ totalFrames: 2, frameRate: null });
|
|
223
|
+
t.playMode = 'once';
|
|
224
|
+
const signals = enableTimelineSignals(t);
|
|
225
|
+
signals.onComplete.emit = () => {
|
|
226
|
+
completed = true;
|
|
227
|
+
};
|
|
228
|
+
playTimeline(t);
|
|
229
|
+
updateTimeline(t, 0); // frame 1
|
|
230
|
+
updateTimeline(t, 0); // frame 2
|
|
231
|
+
updateTimeline(t, 0); // would loop — stops, fires onComplete
|
|
232
|
+
expect(completed).toBe(true);
|
|
233
|
+
expect(t.isPlaying).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('findTimelineLabel', () => {
|
|
238
|
+
it('returns the matching label', () => {
|
|
239
|
+
const t = make({
|
|
240
|
+
labels: [
|
|
241
|
+
{ name: 'idle', frame: 1 },
|
|
242
|
+
{ name: 'run', frame: 3 },
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
expect(findTimelineLabel(t, 'run')?.frame).toBe(3);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns null for an unknown name', () => {
|
|
249
|
+
const t = make();
|
|
250
|
+
expect(findTimelineLabel(t, 'missing')).toBeNull();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('getTimelineCurrentLabel', () => {
|
|
255
|
+
it('returns null when there are no labels', () => {
|
|
256
|
+
const t = make();
|
|
257
|
+
expect(getTimelineCurrentLabel(t)).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('returns null when no label precedes the current frame', () => {
|
|
261
|
+
const t = make({ labels: [{ name: 'run', frame: 3 }], currentFrame: 1 });
|
|
262
|
+
expect(getTimelineCurrentLabel(t)).toBeNull();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('returns the label exactly at the current frame', () => {
|
|
266
|
+
const t = make({ labels: [{ name: 'run', frame: 3 }], currentFrame: 3 });
|
|
267
|
+
expect(getTimelineCurrentLabel(t)?.name).toBe('run');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('returns the last label at or before the current frame', () => {
|
|
271
|
+
const t = make({
|
|
272
|
+
totalFrames: 6,
|
|
273
|
+
labels: [
|
|
274
|
+
{ name: 'idle', frame: 1 },
|
|
275
|
+
{ name: 'run', frame: 3 },
|
|
276
|
+
],
|
|
277
|
+
currentFrame: 4,
|
|
278
|
+
});
|
|
279
|
+
expect(getTimelineCurrentLabel(t)?.name).toBe('run');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('getTimelineFrameScript', () => {
|
|
284
|
+
it('returns null when no scripts are attached', () => {
|
|
285
|
+
const t = make();
|
|
286
|
+
expect(getTimelineFrameScript(t, 1)).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('returns null for a frame with no script when others exist', () => {
|
|
290
|
+
const t = make();
|
|
291
|
+
addTimelineFrameScript(t, 2, () => {});
|
|
292
|
+
expect(getTimelineFrameScript(t, 1)).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('returns the script attached to a frame', () => {
|
|
296
|
+
const t = make();
|
|
297
|
+
const fn = () => {};
|
|
298
|
+
addTimelineFrameScript(t, 3, fn);
|
|
299
|
+
expect(getTimelineFrameScript(t, 3)).toBe(fn);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('gotoAndPlayTimeline', () => {
|
|
304
|
+
it('seeks to frame and starts playing', () => {
|
|
305
|
+
const t = make();
|
|
306
|
+
gotoAndPlayTimeline(t, 3);
|
|
307
|
+
expect(t.currentFrame).toBe(3);
|
|
308
|
+
expect(t.isPlaying).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('fires constructFrame immediately for the target frame', () => {
|
|
312
|
+
const frames: number[] = [];
|
|
313
|
+
const t = make({ constructFrame: (f) => frames.push(f) });
|
|
314
|
+
gotoAndPlayTimeline(t, 3);
|
|
315
|
+
expect(frames).toEqual([3]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('resolves a label name to a frame number', () => {
|
|
319
|
+
const t = make({ labels: [{ name: 'run', frame: 2 }] });
|
|
320
|
+
gotoAndPlayTimeline(t, 'run');
|
|
321
|
+
expect(t.currentFrame).toBe(2);
|
|
322
|
+
expect(t.isPlaying).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('throws for an unknown label', () => {
|
|
326
|
+
const t = make();
|
|
327
|
+
expect(() => gotoAndPlayTimeline(t, 'missing')).toThrow();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('gotoAndStopTimeline', () => {
|
|
332
|
+
it('seeks to frame and stops', () => {
|
|
333
|
+
const t = make();
|
|
334
|
+
playTimeline(t);
|
|
335
|
+
gotoAndStopTimeline(t, 2);
|
|
336
|
+
expect(t.currentFrame).toBe(2);
|
|
337
|
+
expect(t.isPlaying).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('clamps frame to valid range', () => {
|
|
341
|
+
const t = make({ totalFrames: 4 });
|
|
342
|
+
gotoAndStopTimeline(t, 99);
|
|
343
|
+
expect(t.currentFrame).toBe(4);
|
|
344
|
+
gotoAndStopTimeline(t, 0);
|
|
345
|
+
expect(t.currentFrame).toBe(1);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('nextFrameTimeline', () => {
|
|
350
|
+
it('advances one frame and stops', () => {
|
|
351
|
+
const t = make();
|
|
352
|
+
playTimeline(t);
|
|
353
|
+
nextFrameTimeline(t);
|
|
354
|
+
expect(t.currentFrame).toBe(2);
|
|
355
|
+
expect(t.isPlaying).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('clamps at the last frame', () => {
|
|
359
|
+
const t = make({ totalFrames: 4 });
|
|
360
|
+
gotoAndStopTimeline(t, 4);
|
|
361
|
+
nextFrameTimeline(t);
|
|
362
|
+
expect(t.currentFrame).toBe(4);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('playTimeline', () => {
|
|
367
|
+
it('sets isPlaying to true', () => {
|
|
368
|
+
const t = make();
|
|
369
|
+
playTimeline(t);
|
|
370
|
+
expect(t.isPlaying).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('does nothing when totalFrames < 2', () => {
|
|
374
|
+
const t = createTimeline({ source: createTimelineSource({ totalFrames: 1 }) });
|
|
375
|
+
playTimeline(t);
|
|
376
|
+
expect(t.isPlaying).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('resets timeElapsed on play', () => {
|
|
380
|
+
const t = make();
|
|
381
|
+
t.timeElapsed = 999;
|
|
382
|
+
playTimeline(t);
|
|
383
|
+
expect(t.timeElapsed).toBe(0);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('prevFrameTimeline', () => {
|
|
388
|
+
it('retreats one frame and stops', () => {
|
|
389
|
+
const t = make();
|
|
390
|
+
gotoAndStopTimeline(t, 3);
|
|
391
|
+
prevFrameTimeline(t);
|
|
392
|
+
expect(t.currentFrame).toBe(2);
|
|
393
|
+
expect(t.isPlaying).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('clamps at frame 1', () => {
|
|
397
|
+
const t = make();
|
|
398
|
+
prevFrameTimeline(t);
|
|
399
|
+
expect(t.currentFrame).toBe(1);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('removeTimelineFrameScript', () => {
|
|
404
|
+
it('is a no-op when no scripts are attached', () => {
|
|
405
|
+
const t = make();
|
|
406
|
+
expect(() => removeTimelineFrameScript(t, 2)).not.toThrow();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('removes the script so it no longer fires', () => {
|
|
410
|
+
const fired: number[] = [];
|
|
411
|
+
const t = make({ frameRate: null });
|
|
412
|
+
addTimelineFrameScript(t, 2, (_target, f) => fired.push(f));
|
|
413
|
+
removeTimelineFrameScript(t, 2);
|
|
414
|
+
playTimeline(t);
|
|
415
|
+
updateTimeline(t, 0); // frame 1
|
|
416
|
+
updateTimeline(t, 0); // frame 2 — script should not fire
|
|
417
|
+
expect(fired).toEqual([]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('clears frameScripts to null when the last script is removed', () => {
|
|
421
|
+
const t = make();
|
|
422
|
+
addTimelineFrameScript(t, 2, () => {});
|
|
423
|
+
removeTimelineFrameScript(t, 2);
|
|
424
|
+
expect(t.frameScripts).toBeNull();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('stopTimeline', () => {
|
|
429
|
+
it('sets isPlaying to false', () => {
|
|
430
|
+
const t = make();
|
|
431
|
+
playTimeline(t);
|
|
432
|
+
stopTimeline(t);
|
|
433
|
+
expect(t.isPlaying).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe('updateTimeline', () => {
|
|
438
|
+
it('fires constructFrame for frame 1 on first update even when stopped', () => {
|
|
439
|
+
const frames: number[] = [];
|
|
440
|
+
const t = make({ constructFrame: (f) => frames.push(f) });
|
|
441
|
+
updateTimeline(t, 0);
|
|
442
|
+
expect(frames).toEqual([1]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('does not double-fire constructFrame on repeated stopped updates', () => {
|
|
446
|
+
const frames: number[] = [];
|
|
447
|
+
const t = make({ constructFrame: (f) => frames.push(f) });
|
|
448
|
+
updateTimeline(t, 0);
|
|
449
|
+
updateTimeline(t, 0);
|
|
450
|
+
expect(frames).toEqual([1]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('advances one frame per update when frameRate is null', () => {
|
|
454
|
+
const frames: number[] = [];
|
|
455
|
+
const t = make({ frameRate: null, constructFrame: (f) => frames.push(f) });
|
|
456
|
+
playTimeline(t);
|
|
457
|
+
updateTimeline(t, 0);
|
|
458
|
+
updateTimeline(t, 0);
|
|
459
|
+
updateTimeline(t, 0);
|
|
460
|
+
expect(frames).toEqual([1, 2, 3]);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('advances frame after enough time has elapsed for frameRate', () => {
|
|
464
|
+
const frames: number[] = [];
|
|
465
|
+
const t = make({ frameRate: 10, constructFrame: (f) => frames.push(f) }); // 100ms/frame
|
|
466
|
+
playTimeline(t);
|
|
467
|
+
updateTimeline(t, 50);
|
|
468
|
+
expect(frames).toEqual([1]);
|
|
469
|
+
updateTimeline(t, 50);
|
|
470
|
+
expect(frames).toEqual([1, 2]);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('wraps around to frame 1 after the last frame in loop mode', () => {
|
|
474
|
+
const frames: number[] = [];
|
|
475
|
+
const t = make({ totalFrames: 3, frameRate: null, constructFrame: (f) => frames.push(f) });
|
|
476
|
+
playTimeline(t);
|
|
477
|
+
updateTimeline(t, 0); // frame 1
|
|
478
|
+
updateTimeline(t, 0); // frame 2
|
|
479
|
+
updateTimeline(t, 0); // frame 3
|
|
480
|
+
updateTimeline(t, 0); // wraps to frame 1
|
|
481
|
+
expect(frames).toEqual([1, 2, 3, 1]);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('stops at the last frame in once mode and does not wrap', () => {
|
|
485
|
+
const frames: number[] = [];
|
|
486
|
+
const t = make({ totalFrames: 3, frameRate: null, constructFrame: (f) => frames.push(f) });
|
|
487
|
+
t.playMode = 'once';
|
|
488
|
+
playTimeline(t);
|
|
489
|
+
updateTimeline(t, 0); // frame 1
|
|
490
|
+
updateTimeline(t, 0); // frame 2
|
|
491
|
+
updateTimeline(t, 0); // frame 3
|
|
492
|
+
updateTimeline(t, 0); // stopped — no new frame
|
|
493
|
+
expect(frames).toEqual([1, 2, 3]);
|
|
494
|
+
expect(t.isPlaying).toBe(false);
|
|
495
|
+
expect(t.currentFrame).toBe(3);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('can skip multiple frames in one large deltaTime', () => {
|
|
499
|
+
const frames: number[] = [];
|
|
500
|
+
const t = make({ totalFrames: 4, frameRate: 10, constructFrame: (f) => frames.push(f) }); // 100ms/frame
|
|
501
|
+
playTimeline(t);
|
|
502
|
+
updateTimeline(t, 250); // should advance 2 frames beyond current
|
|
503
|
+
expect(t.currentFrame).toBe(3);
|
|
504
|
+
});
|
|
505
|
+
});
|