@flighthq/movieclip 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/spritesheetTimelineSource.d.ts +3 -0
- package/dist/spritesheetTimelineSource.d.ts.map +1 -0
- package/dist/spritesheetTimelineSource.js +38 -0
- package/dist/spritesheetTimelineSource.js.map +1 -0
- package/package.json +43 -0
- package/src/movieClip.test.ts +430 -0
- package/src/spritesheetTimelineSource.test.ts +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,6BAA6B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FrameScript, MovieClip, MovieClipData, MovieClipRuntime, MovieClipSignals, PartialNode, TimelineLabel, TimelineSource } from '@flighthq/types';
|
|
2
|
+
export declare function addMovieClipFrameScript(clip: MovieClip, frame: number | string, script: FrameScript): void;
|
|
3
|
+
export declare function createMovieClip(obj?: Readonly<PartialNode<MovieClip>>): MovieClip;
|
|
4
|
+
export declare function createMovieClipData(data?: Readonly<Partial<MovieClipData>>): MovieClipData;
|
|
5
|
+
export declare function createMovieClipRuntime(): MovieClipRuntime;
|
|
6
|
+
export declare function disposeMovieClipSignals(clip: MovieClip): void;
|
|
7
|
+
export declare function enableMovieClipSignals(clip: MovieClip): MovieClipSignals;
|
|
8
|
+
export declare function getMovieClipCurrentFrame(clip: MovieClip): number;
|
|
9
|
+
export declare function getMovieClipCurrentLabel(clip: MovieClip): TimelineLabel | null;
|
|
10
|
+
export declare function getMovieClipFrameScript(clip: MovieClip, frame: number | string): FrameScript | null;
|
|
11
|
+
export declare function getMovieClipRuntime(source: Readonly<MovieClip>): Readonly<MovieClipRuntime>;
|
|
12
|
+
export declare function getMovieClipSignals(clip: MovieClip): MovieClipSignals | null;
|
|
13
|
+
export declare function getMovieClipTotalFrames(clip: MovieClip): number;
|
|
14
|
+
export declare function gotoAndPlayMovieClip(clip: MovieClip, frame: number | string): void;
|
|
15
|
+
export declare function gotoAndStopMovieClip(clip: MovieClip, frame: number | string): void;
|
|
16
|
+
export declare function isMovieClipPlaying(clip: MovieClip): boolean;
|
|
17
|
+
export declare function nextFrameMovieClip(clip: MovieClip): void;
|
|
18
|
+
export declare function playMovieClip(clip: MovieClip): void;
|
|
19
|
+
export declare function prevFrameMovieClip(clip: MovieClip): void;
|
|
20
|
+
export declare function removeMovieClipFrameScript(clip: MovieClip, frame: number | string): void;
|
|
21
|
+
export declare function setMovieClipSource(clip: MovieClip, source: TimelineSource): void;
|
|
22
|
+
export declare function stopMovieClip(clip: MovieClip): void;
|
|
23
|
+
export declare function updateMovieClip(clip: MovieClip, deltaTime: number): void;
|
|
24
|
+
//# sourceMappingURL=movieClip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"movieClip.d.ts","sourceRoot":"","sources":["../src/movieClip.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,cAAc,EACf,MAAM,iBAAiB,CAAC;AAKzB,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAG1G;AAED,wBAAgB,eAAe,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,GAAG,SAAS,CAEjF;AAED,wBAAgB,mBAAmB,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,GAAG,aAAa,CAI1F;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAIzD;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAI7D;AAKD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,CAQxE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEhE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,GAAG,aAAa,GAAG,IAAI,CAG9E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,IAAI,CAGnG;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAE3F;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,GAAG,IAAI,CAG5E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAE/D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlF;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlF;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAGxD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAGxD;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGxF;AAKD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAShF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAGxE"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createDisplayObjectGeneric, createDisplayObjectRuntime, getDisplayObjectRuntime, } from '@flighthq/displayobject';
|
|
2
|
+
import { addTimelineFrameScript, createTimeline, disposeTimelineSignals, enableTimelineSignals, getTimelineCurrentLabel, getTimelineFrameScript, gotoAndPlayTimeline, gotoAndStopTimeline, nextFrameTimeline, playTimeline, prevFrameTimeline, removeTimelineFrameScript, stopTimeline, updateTimeline, } from '@flighthq/timeline';
|
|
3
|
+
import { EntityRuntimeKey, MovieClipKind } from '@flighthq/types';
|
|
4
|
+
// The MovieClip display node lives here, with its playback engine — a MovieClip is a DisplayObject whose
|
|
5
|
+
// content is driven by a timeline, so the node and the engine that constructs its frames are one feature.
|
|
6
|
+
export function addMovieClipFrameScript(clip, frame, script) {
|
|
7
|
+
if (clip.data.timeline === null)
|
|
8
|
+
return;
|
|
9
|
+
addTimelineFrameScript(clip.data.timeline, frame, script);
|
|
10
|
+
}
|
|
11
|
+
export function createMovieClip(obj) {
|
|
12
|
+
return createDisplayObjectGeneric(MovieClipKind, obj, createMovieClipData, createMovieClipRuntime);
|
|
13
|
+
}
|
|
14
|
+
export function createMovieClipData(data) {
|
|
15
|
+
return {
|
|
16
|
+
timeline: data?.timeline ?? null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createMovieClipRuntime() {
|
|
20
|
+
const out = createDisplayObjectRuntime();
|
|
21
|
+
out.movieClipSignals = null;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
export function disposeMovieClipSignals(clip) {
|
|
25
|
+
const runtime = clip[EntityRuntimeKey];
|
|
26
|
+
if (clip.data.timeline !== null)
|
|
27
|
+
disposeTimelineSignals(clip.data.timeline);
|
|
28
|
+
runtime.movieClipSignals = null;
|
|
29
|
+
}
|
|
30
|
+
// Allocates a MovieClipSignals group on the clip and arms per-frame signal emission. Idempotent —
|
|
31
|
+
// returns the same group on subsequent calls. Also enables the underlying timeline signals so the
|
|
32
|
+
// clip's signals fire when the timeline advances.
|
|
33
|
+
export function enableMovieClipSignals(clip) {
|
|
34
|
+
const runtime = clip[EntityRuntimeKey];
|
|
35
|
+
if (runtime.movieClipSignals !== null)
|
|
36
|
+
return runtime.movieClipSignals;
|
|
37
|
+
// Ensure the timeline exists so signals can be armed even before setMovieClipSource is called.
|
|
38
|
+
if (clip.data.timeline === null)
|
|
39
|
+
clip.data.timeline = createTimeline();
|
|
40
|
+
const signals = enableTimelineSignals(clip.data.timeline);
|
|
41
|
+
runtime.movieClipSignals = signals;
|
|
42
|
+
return signals;
|
|
43
|
+
}
|
|
44
|
+
export function getMovieClipCurrentFrame(clip) {
|
|
45
|
+
return clip.data.timeline?.currentFrame ?? 1;
|
|
46
|
+
}
|
|
47
|
+
export function getMovieClipCurrentLabel(clip) {
|
|
48
|
+
if (clip.data.timeline === null)
|
|
49
|
+
return null;
|
|
50
|
+
return getTimelineCurrentLabel(clip.data.timeline);
|
|
51
|
+
}
|
|
52
|
+
export function getMovieClipFrameScript(clip, frame) {
|
|
53
|
+
if (clip.data.timeline === null)
|
|
54
|
+
return null;
|
|
55
|
+
return getTimelineFrameScript(clip.data.timeline, frame);
|
|
56
|
+
}
|
|
57
|
+
export function getMovieClipRuntime(source) {
|
|
58
|
+
return getDisplayObjectRuntime(source);
|
|
59
|
+
}
|
|
60
|
+
export function getMovieClipSignals(clip) {
|
|
61
|
+
const runtime = clip[EntityRuntimeKey];
|
|
62
|
+
return runtime.movieClipSignals;
|
|
63
|
+
}
|
|
64
|
+
export function getMovieClipTotalFrames(clip) {
|
|
65
|
+
return clip.data.timeline?.source?.totalFrames ?? 1;
|
|
66
|
+
}
|
|
67
|
+
export function gotoAndPlayMovieClip(clip, frame) {
|
|
68
|
+
if (clip.data.timeline === null)
|
|
69
|
+
return;
|
|
70
|
+
gotoAndPlayTimeline(clip.data.timeline, frame);
|
|
71
|
+
}
|
|
72
|
+
export function gotoAndStopMovieClip(clip, frame) {
|
|
73
|
+
if (clip.data.timeline === null)
|
|
74
|
+
return;
|
|
75
|
+
gotoAndStopTimeline(clip.data.timeline, frame);
|
|
76
|
+
}
|
|
77
|
+
export function isMovieClipPlaying(clip) {
|
|
78
|
+
return clip.data.timeline?.isPlaying ?? false;
|
|
79
|
+
}
|
|
80
|
+
export function nextFrameMovieClip(clip) {
|
|
81
|
+
if (clip.data.timeline === null)
|
|
82
|
+
return;
|
|
83
|
+
nextFrameTimeline(clip.data.timeline);
|
|
84
|
+
}
|
|
85
|
+
export function playMovieClip(clip) {
|
|
86
|
+
if (clip.data.timeline === null)
|
|
87
|
+
return;
|
|
88
|
+
playTimeline(clip.data.timeline);
|
|
89
|
+
}
|
|
90
|
+
export function prevFrameMovieClip(clip) {
|
|
91
|
+
if (clip.data.timeline === null)
|
|
92
|
+
return;
|
|
93
|
+
prevFrameTimeline(clip.data.timeline);
|
|
94
|
+
}
|
|
95
|
+
export function removeMovieClipFrameScript(clip, frame) {
|
|
96
|
+
if (clip.data.timeline === null)
|
|
97
|
+
return;
|
|
98
|
+
removeTimelineFrameScript(clip.data.timeline, frame);
|
|
99
|
+
}
|
|
100
|
+
// Binds a TimelineSource to `clip`: gives it a timeline (reusing an existing one) pointed at the clip as
|
|
101
|
+
// its construct target, and realizes the initial frame so the clip isn't blank before play. The source
|
|
102
|
+
// comes from a format — createTimelineSource (hand-authored), createSpritesheetTimelineSource, etc.
|
|
103
|
+
export function setMovieClipSource(clip, source) {
|
|
104
|
+
// Reuse the clip's existing timeline if it has one: enableMovieClipSignals may have already created and
|
|
105
|
+
// armed it (storing the same group in runtime.movieClipSignals), so pointing a source at the same object
|
|
106
|
+
// keeps those signals live with no re-wire needed — the runtime slot and timeline.signals are one group.
|
|
107
|
+
const timeline = clip.data.timeline ?? createTimeline();
|
|
108
|
+
timeline.source = source;
|
|
109
|
+
timeline.target = clip;
|
|
110
|
+
clip.data.timeline = timeline;
|
|
111
|
+
gotoAndStopTimeline(timeline, timeline.currentFrame);
|
|
112
|
+
}
|
|
113
|
+
export function stopMovieClip(clip) {
|
|
114
|
+
if (clip.data.timeline === null)
|
|
115
|
+
return;
|
|
116
|
+
stopTimeline(clip.data.timeline);
|
|
117
|
+
}
|
|
118
|
+
export function updateMovieClip(clip, deltaTime) {
|
|
119
|
+
if (clip.data.timeline === null)
|
|
120
|
+
return;
|
|
121
|
+
updateTimeline(clip.data.timeline, deltaTime);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=movieClip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"movieClip.js","sourceRoot":"","sources":["../src/movieClip.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Spritesheet, SpritesheetAnimation, TimelineSource } from '@flighthq/types';
|
|
2
|
+
export declare function createSpritesheetTimelineSource(spritesheet: Readonly<Spritesheet>, animation: Readonly<SpritesheetAnimation>): TimelineSource;
|
|
3
|
+
//# sourceMappingURL=spritesheetTimelineSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spritesheetTimelineSource.d.ts","sourceRoot":"","sources":["../src/spritesheetTimelineSource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAyB,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAUhH,wBAAgB,+BAA+B,CAC7C,WAAW,EAAE,QAAQ,CAAC,WAAW,CAAC,EAClC,SAAS,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GACxC,cAAc,CA0BhB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createBitmap } from '@flighthq/displayobject';
|
|
2
|
+
import { addNodeChild, invalidateNodeLocalTransform } from '@flighthq/node';
|
|
3
|
+
// Exposes a spritesheet animation as a TimelineSource so a MovieClip can play it (the spritesheet side of
|
|
4
|
+
// the timeline frame-source contract — `@flighthq/timeline` consumes `TimelineSource`, this produces one,
|
|
5
|
+
// and neither depends on the other beyond the shared interface in `@flighthq/types`). Each frame swaps
|
|
6
|
+
// the displayed atlas region and offset on a bitmap the source lazily creates as a child of the target
|
|
7
|
+
// the first time it constructs onto that target; tracking the bitmap per target keeps the source
|
|
8
|
+
// shareable across many MovieClips.
|
|
9
|
+
//
|
|
10
|
+
// Bind it with `setMovieClipSource(clip, createSpritesheetTimelineSource(sheet, anim))`, then `playMovieClip`.
|
|
11
|
+
export function createSpritesheetTimelineSource(spritesheet, animation) {
|
|
12
|
+
const bitmaps = new WeakMap();
|
|
13
|
+
return {
|
|
14
|
+
totalFrames: animation.frames.length,
|
|
15
|
+
labels: [],
|
|
16
|
+
frameRate: 1000 / animation.frameDuration,
|
|
17
|
+
constructFrame(target, frame) {
|
|
18
|
+
const atlas = spritesheet.atlas;
|
|
19
|
+
if (atlas === null)
|
|
20
|
+
return;
|
|
21
|
+
let bitmap = bitmaps.get(target);
|
|
22
|
+
if (bitmap === undefined) {
|
|
23
|
+
bitmap = createBitmap();
|
|
24
|
+
bitmap.data.image = atlas.image;
|
|
25
|
+
addNodeChild(target, bitmap);
|
|
26
|
+
bitmaps.set(target, bitmap);
|
|
27
|
+
}
|
|
28
|
+
const sheetFrame = spritesheet.frames[animation.frames[frame - 1]];
|
|
29
|
+
if (sheetFrame === undefined)
|
|
30
|
+
return;
|
|
31
|
+
bitmap.data.sourceRectangle = atlas.regions[sheetFrame.id];
|
|
32
|
+
bitmap.x = sheetFrame.offsetX - animation.originX;
|
|
33
|
+
bitmap.y = sheetFrame.offsetY - animation.originY;
|
|
34
|
+
invalidateNodeLocalTransform(bitmap);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=spritesheetTimelineSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spritesheetTimelineSource.js","sourceRoot":"","sources":["../src/spritesheetTimelineSource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAG5E,0GAA0G;AAC1G,0GAA0G;AAC1G,uGAAuG;AACvG,uGAAuG;AACvG,iGAAiG;AACjG,oCAAoC;AACpC,EAAE;AACF,+GAA+G;AAC/G,MAAM,UAAU,+BAA+B,CAC7C,WAAkC,EAClC,SAAyC;IAEzC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAyB,CAAC;IACrD,OAAO;QACL,WAAW,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM;QACpC,MAAM,EAAE,EAAE;QACV,SAAS,EAAE,IAAI,GAAG,SAAS,CAAC,aAAa;QACzC,cAAc,CAAC,MAAqB,EAAE,KAAa;YACjD,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC;YAChC,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO;YAE3B,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACjC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,GAAG,YAAY,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;gBAChC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC9B,CAAC;YAED,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;YACnE,IAAI,UAAU,KAAK,SAAS;gBAAE,OAAO;YACrC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YAC3D,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC;YAClD,MAAM,CAAC,CAAC,GAAG,UAAU,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC;YAClD,4BAA4B,CAAC,MAAM,CAAC,CAAC;QACvC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/movieclip",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/displayobject": "0.1.0",
|
|
31
|
+
"@flighthq/node": "0.1.0",
|
|
32
|
+
"@flighthq/timeline": "0.1.0",
|
|
33
|
+
"@flighthq/types": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@flighthq/image": "*",
|
|
37
|
+
"@flighthq/spritesheet": "*",
|
|
38
|
+
"@flighthq/textureatlas": "*",
|
|
39
|
+
"typescript": "^5.3.0"
|
|
40
|
+
},
|
|
41
|
+
"description": "MovieClip display-object composition layer over @flighthq/timeline",
|
|
42
|
+
"sideEffects": false
|
|
43
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { createTimeline, createTimelineSource, playTimeline } from '@flighthq/timeline';
|
|
2
|
+
import type { MovieClip, Timeline } from '@flighthq/types';
|
|
3
|
+
import { MovieClipKind } from '@flighthq/types';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
addMovieClipFrameScript,
|
|
7
|
+
createMovieClip,
|
|
8
|
+
createMovieClipData,
|
|
9
|
+
createMovieClipRuntime,
|
|
10
|
+
disposeMovieClipSignals,
|
|
11
|
+
enableMovieClipSignals,
|
|
12
|
+
getMovieClipCurrentFrame,
|
|
13
|
+
getMovieClipCurrentLabel,
|
|
14
|
+
getMovieClipFrameScript,
|
|
15
|
+
getMovieClipRuntime,
|
|
16
|
+
getMovieClipSignals,
|
|
17
|
+
getMovieClipTotalFrames,
|
|
18
|
+
gotoAndPlayMovieClip,
|
|
19
|
+
gotoAndStopMovieClip,
|
|
20
|
+
isMovieClipPlaying,
|
|
21
|
+
nextFrameMovieClip,
|
|
22
|
+
playMovieClip,
|
|
23
|
+
prevFrameMovieClip,
|
|
24
|
+
removeMovieClipFrameScript,
|
|
25
|
+
setMovieClipSource,
|
|
26
|
+
stopMovieClip,
|
|
27
|
+
updateMovieClip,
|
|
28
|
+
} from './movieClip';
|
|
29
|
+
|
|
30
|
+
describe('addMovieClipFrameScript', () => {
|
|
31
|
+
it('does nothing when timeline is null', () => {
|
|
32
|
+
const clip = createMovieClip();
|
|
33
|
+
expect(() => addMovieClipFrameScript(clip, 1, () => {})).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('attaches a script that fires on frame entry', () => {
|
|
37
|
+
const fired: number[] = [];
|
|
38
|
+
const clip = createMovieClip();
|
|
39
|
+
clip.data.timeline = createTimeline({
|
|
40
|
+
source: createTimelineSource({ totalFrames: 3, frameRate: null }),
|
|
41
|
+
target: clip,
|
|
42
|
+
});
|
|
43
|
+
addMovieClipFrameScript(clip, 2, (_t, f) => fired.push(f));
|
|
44
|
+
playMovieClip(clip);
|
|
45
|
+
updateMovieClip(clip, 0); // frame 1
|
|
46
|
+
updateMovieClip(clip, 0); // frame 2 — script fires
|
|
47
|
+
expect(fired).toEqual([2]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('createMovieClip', () => {
|
|
52
|
+
let movieClip: MovieClip;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
movieClip = createMovieClip();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('initializes default values', () => {
|
|
59
|
+
expect(movieClip.data.timeline).toBeNull();
|
|
60
|
+
expect(movieClip.kind).toStrictEqual(MovieClipKind);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('allows pre-defined values', () => {
|
|
64
|
+
const base = { data: { timeline: {} as Timeline } };
|
|
65
|
+
const obj = createMovieClip(base);
|
|
66
|
+
expect(obj.data.timeline).toStrictEqual(base.data.timeline);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns a new object for better hidden-class performance', () => {
|
|
70
|
+
const base = {};
|
|
71
|
+
const obj = createMovieClip(base);
|
|
72
|
+
expect(obj).not.toStrictEqual(base);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('createMovieClipData', () => {
|
|
77
|
+
it('returns default values', () => {
|
|
78
|
+
expect(createMovieClipData().timeline).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('allows pre-defined values', () => {
|
|
82
|
+
const timeline = {} as Timeline;
|
|
83
|
+
expect(createMovieClipData({ timeline }).timeline).toBe(timeline);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('createMovieClipRuntime', () => {
|
|
88
|
+
it('returns a non-null runtime with movieClipSignals null', () => {
|
|
89
|
+
const runtime = createMovieClipRuntime();
|
|
90
|
+
expect(runtime).not.toBeNull();
|
|
91
|
+
expect(runtime.movieClipSignals).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('disposeMovieClipSignals', () => {
|
|
96
|
+
it('clears movieClipSignals to null on the runtime', () => {
|
|
97
|
+
const clip = createMovieClip();
|
|
98
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3 }));
|
|
99
|
+
enableMovieClipSignals(clip);
|
|
100
|
+
expect(getMovieClipSignals(clip)).not.toBeNull();
|
|
101
|
+
disposeMovieClipSignals(clip);
|
|
102
|
+
expect(getMovieClipSignals(clip)).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('clears the underlying timeline signals', () => {
|
|
106
|
+
const clip = createMovieClip();
|
|
107
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3 }));
|
|
108
|
+
enableMovieClipSignals(clip);
|
|
109
|
+
expect(clip.data.timeline!.signals).not.toBeNull();
|
|
110
|
+
disposeMovieClipSignals(clip);
|
|
111
|
+
expect(clip.data.timeline!.signals).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('is idempotent — no-op when signals are already null', () => {
|
|
115
|
+
const clip = createMovieClip();
|
|
116
|
+
expect(() => disposeMovieClipSignals(clip)).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('allows enableMovieClipSignals to re-arm after dispose', () => {
|
|
120
|
+
const clip = createMovieClip();
|
|
121
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3 }));
|
|
122
|
+
const first = enableMovieClipSignals(clip);
|
|
123
|
+
disposeMovieClipSignals(clip);
|
|
124
|
+
const second = enableMovieClipSignals(clip);
|
|
125
|
+
expect(second).not.toBe(first);
|
|
126
|
+
expect(getMovieClipSignals(clip)).toBe(second);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('enableMovieClipSignals', () => {
|
|
131
|
+
it('returns a signals group with all lifecycle signals defined', () => {
|
|
132
|
+
const clip = createMovieClip();
|
|
133
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3 }));
|
|
134
|
+
const signals = enableMovieClipSignals(clip);
|
|
135
|
+
expect(signals.onEnterFrame).toBeDefined();
|
|
136
|
+
expect(signals.onExitFrame).toBeDefined();
|
|
137
|
+
expect(signals.onFrameConstructed).toBeDefined();
|
|
138
|
+
expect(signals.onComplete).toBeDefined();
|
|
139
|
+
expect(signals.onLoop).toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('is idempotent — returns the same object on subsequent calls', () => {
|
|
143
|
+
const clip = createMovieClip();
|
|
144
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3 }));
|
|
145
|
+
expect(enableMovieClipSignals(clip)).toBe(enableMovieClipSignals(clip));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('works before setMovieClipSource is called', () => {
|
|
149
|
+
const clip = createMovieClip();
|
|
150
|
+
expect(() => enableMovieClipSignals(clip)).not.toThrow();
|
|
151
|
+
expect(enableMovieClipSignals(clip)).not.toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('emits onEnterFrame when the frame changes', () => {
|
|
155
|
+
const frames: number[] = [];
|
|
156
|
+
const clip = createMovieClip();
|
|
157
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3, frameRate: null }));
|
|
158
|
+
const signals = enableMovieClipSignals(clip);
|
|
159
|
+
signals.onEnterFrame.emit = (event) => frames.push(event.frame);
|
|
160
|
+
playMovieClip(clip);
|
|
161
|
+
updateMovieClip(clip, 0); // frame 1
|
|
162
|
+
updateMovieClip(clip, 0); // frame 2
|
|
163
|
+
expect(frames).toContain(2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('getMovieClipCurrentFrame', () => {
|
|
168
|
+
it('returns 1 when timeline is null', () => {
|
|
169
|
+
const clip = createMovieClip();
|
|
170
|
+
expect(getMovieClipCurrentFrame(clip)).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns the timeline currentFrame', () => {
|
|
174
|
+
const clip = createMovieClip();
|
|
175
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 5 }), currentFrame: 3 });
|
|
176
|
+
expect(getMovieClipCurrentFrame(clip)).toBe(3);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('getMovieClipCurrentLabel', () => {
|
|
181
|
+
it('returns null when timeline is null', () => {
|
|
182
|
+
const clip = createMovieClip();
|
|
183
|
+
expect(getMovieClipCurrentLabel(clip)).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('returns the label at the current frame', () => {
|
|
187
|
+
const clip = createMovieClip();
|
|
188
|
+
setMovieClipSource(
|
|
189
|
+
clip,
|
|
190
|
+
createTimelineSource({
|
|
191
|
+
totalFrames: 5,
|
|
192
|
+
labels: [
|
|
193
|
+
{ name: 'idle', frame: 1 },
|
|
194
|
+
{ name: 'run', frame: 3 },
|
|
195
|
+
],
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
gotoAndStopMovieClip(clip, 3);
|
|
199
|
+
expect(getMovieClipCurrentLabel(clip)?.name).toBe('run');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('getMovieClipFrameScript', () => {
|
|
204
|
+
it('returns null when timeline is null', () => {
|
|
205
|
+
const clip = createMovieClip();
|
|
206
|
+
expect(getMovieClipFrameScript(clip, 1)).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns null when no script is attached', () => {
|
|
210
|
+
const clip = createMovieClip();
|
|
211
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 3 }) });
|
|
212
|
+
expect(getMovieClipFrameScript(clip, 1)).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('returns the script after addMovieClipFrameScript', () => {
|
|
216
|
+
const clip = createMovieClip();
|
|
217
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 3 }) });
|
|
218
|
+
const fn = () => {};
|
|
219
|
+
addMovieClipFrameScript(clip, 2, fn);
|
|
220
|
+
expect(getMovieClipFrameScript(clip, 2)).toBe(fn);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('getMovieClipRuntime', () => {
|
|
225
|
+
it('returns the runtime for a MovieClip', () => {
|
|
226
|
+
expect(getMovieClipRuntime(createMovieClip())).not.toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('getMovieClipSignals', () => {
|
|
231
|
+
it('returns null before enableMovieClipSignals is called', () => {
|
|
232
|
+
const clip = createMovieClip();
|
|
233
|
+
expect(getMovieClipSignals(clip)).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('returns the signals group after enableMovieClipSignals', () => {
|
|
237
|
+
const clip = createMovieClip();
|
|
238
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 3 }));
|
|
239
|
+
const signals = enableMovieClipSignals(clip);
|
|
240
|
+
expect(getMovieClipSignals(clip)).toBe(signals);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('getMovieClipTotalFrames', () => {
|
|
245
|
+
it('returns 1 when timeline is null', () => {
|
|
246
|
+
const clip = createMovieClip();
|
|
247
|
+
expect(getMovieClipTotalFrames(clip)).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns the timeline totalFrames', () => {
|
|
251
|
+
const clip = createMovieClip();
|
|
252
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 10 }) });
|
|
253
|
+
expect(getMovieClipTotalFrames(clip)).toBe(10);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('gotoAndPlayMovieClip', () => {
|
|
258
|
+
it('does nothing when timeline is null', () => {
|
|
259
|
+
const clip = createMovieClip();
|
|
260
|
+
expect(() => gotoAndPlayMovieClip(clip, 2)).not.toThrow();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('seeks to the given frame and starts playing', () => {
|
|
264
|
+
const clip = createMovieClip();
|
|
265
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 5 }) });
|
|
266
|
+
gotoAndPlayMovieClip(clip, 3);
|
|
267
|
+
expect(clip.data.timeline.currentFrame).toBe(3);
|
|
268
|
+
expect(clip.data.timeline.isPlaying).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('gotoAndStopMovieClip', () => {
|
|
273
|
+
it('does nothing when timeline is null', () => {
|
|
274
|
+
const clip = createMovieClip();
|
|
275
|
+
expect(() => gotoAndStopMovieClip(clip, 2)).not.toThrow();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('seeks to the given frame and stops', () => {
|
|
279
|
+
const clip = createMovieClip();
|
|
280
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 5 }) });
|
|
281
|
+
playMovieClip(clip);
|
|
282
|
+
gotoAndStopMovieClip(clip, 2);
|
|
283
|
+
expect(clip.data.timeline.currentFrame).toBe(2);
|
|
284
|
+
expect(clip.data.timeline.isPlaying).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('isMovieClipPlaying', () => {
|
|
289
|
+
it('returns false when timeline is null', () => {
|
|
290
|
+
const clip = createMovieClip();
|
|
291
|
+
expect(isMovieClipPlaying(clip)).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('returns true when the timeline is playing', () => {
|
|
295
|
+
const clip = createMovieClip();
|
|
296
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 3 }) });
|
|
297
|
+
playTimeline(clip.data.timeline);
|
|
298
|
+
expect(isMovieClipPlaying(clip)).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('nextFrameMovieClip', () => {
|
|
303
|
+
it('does nothing when timeline is null', () => {
|
|
304
|
+
const clip = createMovieClip();
|
|
305
|
+
expect(() => nextFrameMovieClip(clip)).not.toThrow();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('advances currentFrame by one', () => {
|
|
309
|
+
const clip = createMovieClip();
|
|
310
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 5 }), currentFrame: 2 });
|
|
311
|
+
nextFrameMovieClip(clip);
|
|
312
|
+
expect(clip.data.timeline.currentFrame).toBe(3);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('playMovieClip', () => {
|
|
317
|
+
it('does nothing when timeline is null', () => {
|
|
318
|
+
const clip = createMovieClip();
|
|
319
|
+
expect(() => playMovieClip(clip)).not.toThrow();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('starts the timeline playing', () => {
|
|
323
|
+
const clip = createMovieClip();
|
|
324
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 3 }) });
|
|
325
|
+
playMovieClip(clip);
|
|
326
|
+
expect(clip.data.timeline.isPlaying).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('prevFrameMovieClip', () => {
|
|
331
|
+
it('does nothing when timeline is null', () => {
|
|
332
|
+
const clip = createMovieClip();
|
|
333
|
+
expect(() => prevFrameMovieClip(clip)).not.toThrow();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('moves currentFrame back by one', () => {
|
|
337
|
+
const clip = createMovieClip();
|
|
338
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 5 }), currentFrame: 3 });
|
|
339
|
+
prevFrameMovieClip(clip);
|
|
340
|
+
expect(clip.data.timeline.currentFrame).toBe(2);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('removeMovieClipFrameScript', () => {
|
|
345
|
+
it('does nothing when timeline is null', () => {
|
|
346
|
+
const clip = createMovieClip();
|
|
347
|
+
expect(() => removeMovieClipFrameScript(clip, 1)).not.toThrow();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('removes the script so it no longer fires', () => {
|
|
351
|
+
const fired: number[] = [];
|
|
352
|
+
const clip = createMovieClip();
|
|
353
|
+
clip.data.timeline = createTimeline({
|
|
354
|
+
source: createTimelineSource({ totalFrames: 3, frameRate: null }),
|
|
355
|
+
target: clip,
|
|
356
|
+
});
|
|
357
|
+
addMovieClipFrameScript(clip, 2, (_t, f) => fired.push(f));
|
|
358
|
+
removeMovieClipFrameScript(clip, 2);
|
|
359
|
+
playMovieClip(clip);
|
|
360
|
+
updateMovieClip(clip, 0); // frame 1
|
|
361
|
+
updateMovieClip(clip, 0); // frame 2 — script should not fire
|
|
362
|
+
expect(fired).toEqual([]);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('setMovieClipSource', () => {
|
|
367
|
+
it('binds a source, targets the clip, and realizes the initial frame', () => {
|
|
368
|
+
const frames: number[] = [];
|
|
369
|
+
const clip = createMovieClip();
|
|
370
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 4, constructFrame: (_t, f) => frames.push(f) }));
|
|
371
|
+
expect(getMovieClipTotalFrames(clip)).toBe(4);
|
|
372
|
+
expect(clip.data.timeline?.target).toBe(clip);
|
|
373
|
+
expect(frames).toEqual([1]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('reuses an existing timeline on the clip', () => {
|
|
377
|
+
const clip = createMovieClip();
|
|
378
|
+
clip.data.timeline = createTimeline({ currentFrame: 2 });
|
|
379
|
+
const existing = clip.data.timeline;
|
|
380
|
+
setMovieClipSource(clip, createTimelineSource({ totalFrames: 5 }));
|
|
381
|
+
expect(clip.data.timeline).toBe(existing);
|
|
382
|
+
expect(clip.data.timeline?.source?.totalFrames).toBe(5);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('stopMovieClip', () => {
|
|
387
|
+
it('does nothing when timeline is null', () => {
|
|
388
|
+
const clip = createMovieClip();
|
|
389
|
+
expect(() => stopMovieClip(clip)).not.toThrow();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('stops a playing timeline', () => {
|
|
393
|
+
const clip = createMovieClip();
|
|
394
|
+
clip.data.timeline = createTimeline({ source: createTimelineSource({ totalFrames: 3 }) });
|
|
395
|
+
playMovieClip(clip);
|
|
396
|
+
stopMovieClip(clip);
|
|
397
|
+
expect(clip.data.timeline.isPlaying).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('updateMovieClip', () => {
|
|
402
|
+
it('does nothing when timeline is null', () => {
|
|
403
|
+
const clip = createMovieClip();
|
|
404
|
+
expect(() => updateMovieClip(clip, 16)).not.toThrow();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('advances the timeline when playing', () => {
|
|
408
|
+
const frames: number[] = [];
|
|
409
|
+
const clip = createMovieClip();
|
|
410
|
+
clip.data.timeline = createTimeline({
|
|
411
|
+
source: createTimelineSource({ totalFrames: 3, frameRate: null, constructFrame: (_t, f) => frames.push(f) }),
|
|
412
|
+
target: clip,
|
|
413
|
+
});
|
|
414
|
+
playTimeline(clip.data.timeline);
|
|
415
|
+
updateMovieClip(clip, 16);
|
|
416
|
+
updateMovieClip(clip, 16);
|
|
417
|
+
expect(frames).toEqual([1, 2]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('fires constructFrame for frame 1 on first update even when stopped', () => {
|
|
421
|
+
const frames: number[] = [];
|
|
422
|
+
const clip = createMovieClip();
|
|
423
|
+
clip.data.timeline = createTimeline({
|
|
424
|
+
source: createTimelineSource({ totalFrames: 3, constructFrame: (_t, f) => frames.push(f) }),
|
|
425
|
+
target: clip,
|
|
426
|
+
});
|
|
427
|
+
updateMovieClip(clip, 0);
|
|
428
|
+
expect(frames).toEqual([1]);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createDisplayContainer, getDisplayObjectRuntime } from '@flighthq/displayobject';
|
|
2
|
+
import { createImageResource } from '@flighthq/image';
|
|
3
|
+
import { createSpritesheet, createSpritesheetAnimation, createSpritesheetFrame } from '@flighthq/spritesheet';
|
|
4
|
+
import { addTextureAtlasRegion, createTextureAtlas } from '@flighthq/textureatlas';
|
|
5
|
+
import type { DisplayObject } from '@flighthq/types';
|
|
6
|
+
|
|
7
|
+
import { createSpritesheetTimelineSource } from './spritesheetTimelineSource';
|
|
8
|
+
|
|
9
|
+
function makeSheet(frameCount: number) {
|
|
10
|
+
const img = document.createElement('img') as HTMLImageElement;
|
|
11
|
+
const source = createImageResource(img);
|
|
12
|
+
source.width = 128;
|
|
13
|
+
source.height = 32;
|
|
14
|
+
const atlas = createTextureAtlas({ image: source });
|
|
15
|
+
const frames = [];
|
|
16
|
+
for (let i = 0; i < frameCount; i++) {
|
|
17
|
+
addTextureAtlasRegion(atlas, i * 32, 0, 32, 32);
|
|
18
|
+
frames.push(createSpritesheetFrame({ id: i }));
|
|
19
|
+
}
|
|
20
|
+
const sheet = createSpritesheet({ atlas });
|
|
21
|
+
sheet.frames = frames;
|
|
22
|
+
return sheet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('createSpritesheetTimelineSource', () => {
|
|
26
|
+
it('reports totalFrames from the animation and frameRate from frameDuration', () => {
|
|
27
|
+
const sheet = makeSheet(3);
|
|
28
|
+
const anim = createSpritesheetAnimation({ frameDuration: 200, frames: [0, 1, 2] });
|
|
29
|
+
|
|
30
|
+
const source = createSpritesheetTimelineSource(sheet, anim);
|
|
31
|
+
|
|
32
|
+
expect(source.totalFrames).toBe(3);
|
|
33
|
+
expect(source.frameRate).toBeCloseTo(1000 / 200);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('lazily creates one bitmap child on the target and shows the frame region', () => {
|
|
37
|
+
const sheet = makeSheet(2);
|
|
38
|
+
const anim = createSpritesheetAnimation({ frameDuration: 100, frames: [0, 1] });
|
|
39
|
+
const source = createSpritesheetTimelineSource(sheet, anim);
|
|
40
|
+
const target = createDisplayContainer();
|
|
41
|
+
|
|
42
|
+
source.constructFrame(target as DisplayObject, 1);
|
|
43
|
+
source.constructFrame(target as DisplayObject, 2);
|
|
44
|
+
|
|
45
|
+
const children = getDisplayObjectRuntime(target).children;
|
|
46
|
+
expect(children).not.toBeNull();
|
|
47
|
+
expect(children!.length).toBe(1); // reused across frames, not one-per-frame
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not throw when the spritesheet has no atlas', () => {
|
|
51
|
+
const sheet = createSpritesheet({ atlas: null });
|
|
52
|
+
sheet.frames = [];
|
|
53
|
+
const anim = createSpritesheetAnimation({ frameDuration: 100, frames: [0] });
|
|
54
|
+
const source = createSpritesheetTimelineSource(sheet, anim);
|
|
55
|
+
|
|
56
|
+
expect(() => source.constructFrame(createDisplayContainer() as DisplayObject, 1)).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
});
|