@flighthq/scene2d 0.2.1-next.840.857425c

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 (45) hide show
  1. package/dist/bitmap.d.ts +8 -0
  2. package/dist/bitmap.d.ts.map +1 -0
  3. package/dist/bitmap.js +41 -0
  4. package/dist/bitmap.js.map +1 -0
  5. package/dist/displayContainer.d.ts +5 -0
  6. package/dist/displayContainer.d.ts.map +1 -0
  7. package/dist/displayContainer.js +12 -0
  8. package/dist/displayContainer.js.map +1 -0
  9. package/dist/displayObject.d.ts +12 -0
  10. package/dist/displayObject.d.ts.map +1 -0
  11. package/dist/displayObject.js +87 -0
  12. package/dist/displayObject.js.map +1 -0
  13. package/dist/displayObjectAnimation.d.ts +3 -0
  14. package/dist/displayObjectAnimation.d.ts.map +1 -0
  15. package/dist/displayObjectAnimation.js +82 -0
  16. package/dist/displayObjectAnimation.js.map +1 -0
  17. package/dist/htmlView.d.ts +8 -0
  18. package/dist/htmlView.d.ts.map +1 -0
  19. package/dist/htmlView.js +35 -0
  20. package/dist/htmlView.js.map +1 -0
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +9 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/renderView.d.ts +8 -0
  26. package/dist/renderView.d.ts.map +1 -0
  27. package/dist/renderView.js +35 -0
  28. package/dist/renderView.js.map +1 -0
  29. package/dist/scene2d.d.ts +10 -0
  30. package/dist/scene2d.d.ts.map +1 -0
  31. package/dist/scene2d.js +73 -0
  32. package/dist/scene2d.js.map +1 -0
  33. package/dist/video.d.ts +9 -0
  34. package/dist/video.d.ts.map +1 -0
  35. package/dist/video.js +41 -0
  36. package/dist/video.js.map +1 -0
  37. package/package.json +49 -0
  38. package/src/bitmap.test.ts +131 -0
  39. package/src/displayContainer.test.ts +37 -0
  40. package/src/displayObject.test.ts +241 -0
  41. package/src/displayObjectAnimation.test.ts +69 -0
  42. package/src/htmlView.test.ts +111 -0
  43. package/src/renderView.test.ts +104 -0
  44. package/src/scene2d.test.ts +165 -0
  45. package/src/video.test.ts +113 -0
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@flighthq/scene2d",
3
+ "version": "0.2.1-next.840.857425c",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/scene2d"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src/**/*.test.ts",
21
+ "!dist/**/*.test.js",
22
+ "!dist/**/*.test.d.ts",
23
+ "!dist/**/*.test.js.map",
24
+ "!dist/**/*.test.d.ts.map"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -b",
28
+ "clean": "tsc -b --clean",
29
+ "test": "vitest run --config vitest.config.ts",
30
+ "test:watch": "vitest --watch --config vitest.config.ts",
31
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
32
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
+ },
34
+ "dependencies": {
35
+ "@flighthq/adjustments": "0.2.1-next.840.857425c",
36
+ "@flighthq/animation": "0.2.1-next.840.857425c",
37
+ "@flighthq/entity": "0.2.1-next.840.857425c",
38
+ "@flighthq/geometry": "0.2.1-next.840.857425c",
39
+ "@flighthq/materials": "0.2.1-next.840.857425c",
40
+ "@flighthq/node": "0.2.1-next.840.857425c",
41
+ "@flighthq/signals": "0.2.1-next.840.857425c",
42
+ "@flighthq/types": "0.2.1-next.840.857425c"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "^5.3.0"
46
+ },
47
+ "description": "Display object tree for composited 2D rendering: bitmaps, shapes, text, masks, blend modes",
48
+ "sideEffects": false
49
+ }
@@ -0,0 +1,131 @@
1
+ import { getEntityRuntime } from '@flighthq/entity';
2
+ import { createRectangle } from '@flighthq/geometry';
3
+ import { getNodeLocalBoundsRevision, getNodeLocalContentRevision, getNodeLocalTransformRevision } from '@flighthq/node';
4
+ import type { Bitmap, BitmapRuntime, ImageResource, Node } from '@flighthq/types';
5
+ import { BitmapKind } from '@flighthq/types';
6
+
7
+ import {
8
+ computeBitmapLocalBoundsRectangle,
9
+ createBitmap,
10
+ createBitmapData,
11
+ createBitmapRuntime,
12
+ getBitmapRuntime,
13
+ setBitmapImage,
14
+ } from './bitmap';
15
+
16
+ describe('computeBitmapLocalBoundsRectangle', () => {
17
+ it('sets out dimensions from image when image is present', () => {
18
+ const bitmap = createBitmap({ data: { image: { width: 100, height: 200 } as ImageResource } });
19
+ const out = createRectangle();
20
+ computeBitmapLocalBoundsRectangle(out, bitmap as unknown as Node);
21
+ expect(out.width).toBe(100);
22
+ expect(out.height).toBe(200);
23
+ });
24
+
25
+ it('does not modify out when image is null', () => {
26
+ const bitmap = createBitmap();
27
+ const out = createRectangle(0, 0, 50, 60);
28
+ computeBitmapLocalBoundsRectangle(out, bitmap as unknown as Node);
29
+ expect(out.width).toBe(50);
30
+ expect(out.height).toBe(60);
31
+ });
32
+ });
33
+
34
+ describe('createBitmap', () => {
35
+ let bitmap: Bitmap;
36
+
37
+ beforeEach(() => {
38
+ bitmap = createBitmap();
39
+ });
40
+
41
+ it('initializes default values', () => {
42
+ expect(bitmap.data.image).toBeNull();
43
+ expect(bitmap.data.smoothing).toBe(true);
44
+ expect(bitmap.kind).toBe(BitmapKind);
45
+ });
46
+
47
+ it('allows pre-defined values', () => {
48
+ const image = {} as ImageResource;
49
+ const base = {
50
+ data: {
51
+ image: image,
52
+ smoothing: false,
53
+ },
54
+ };
55
+ const obj = createBitmap(base);
56
+ expect(obj.data.image).toBe(image);
57
+ expect(obj.data.smoothing).toBe(false);
58
+ });
59
+
60
+ it('returns a new object for better hidden-class performance', () => {
61
+ const base = {};
62
+ const obj = createBitmap(base);
63
+ expect(obj).not.toStrictEqual(base);
64
+ });
65
+ });
66
+
67
+ describe('createBitmapData', () => {
68
+ it('returns default values', () => {
69
+ const data = createBitmapData();
70
+ expect(data.image).toBeNull();
71
+ expect(data.smoothing).toBe(true);
72
+ });
73
+
74
+ it('allows pre-defined values', () => {
75
+ const image = { width: 10, height: 10 } as ImageResource;
76
+ const data = createBitmapData({ image, smoothing: false });
77
+ expect(data.image).toBe(image);
78
+ expect(data.smoothing).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('createBitmapRuntime', () => {
83
+ it('returns a non-null runtime', () => {
84
+ const runtime = createBitmapRuntime();
85
+ expect(runtime).not.toBeNull();
86
+ });
87
+
88
+ it('uses computeBitmapLocalBoundsRectangle', () => {
89
+ const runtime = createBitmapRuntime();
90
+ expect(runtime.computeLocalBoundsRectangle).toStrictEqual(computeBitmapLocalBoundsRectangle);
91
+ });
92
+ });
93
+
94
+ describe('getBitmapRuntime', () => {
95
+ it('returns the runtime of the given Bitmap', () => {
96
+ const bitmap = createBitmap();
97
+ const runtime = getBitmapRuntime(bitmap);
98
+ expect(runtime).not.toBeNull();
99
+ });
100
+ });
101
+
102
+ describe('setBitmapImage', () => {
103
+ it('sets the image', () => {
104
+ const bitmap = createBitmap();
105
+ const image = { width: 64, height: 64 } as ImageResource;
106
+ setBitmapImage(bitmap, image);
107
+ expect(bitmap.data.image).toBe(image);
108
+ });
109
+
110
+ it('accepts null', () => {
111
+ const bitmap = createBitmap({ data: { image: { width: 1, height: 1 } as ImageResource } });
112
+ setBitmapImage(bitmap, null);
113
+ expect(bitmap.data.image).toBeNull();
114
+ });
115
+
116
+ it('invalidates local bounds', () => {
117
+ const bitmap = createBitmap();
118
+ const runtime = getEntityRuntime(bitmap) as BitmapRuntime;
119
+ const idBefore = runtime.localBoundsId;
120
+ setBitmapImage(bitmap, { width: 64, height: 64 } as ImageResource);
121
+ expect(runtime.localBoundsId).not.toBe(idBefore);
122
+ });
123
+
124
+ it('invalidates local content', () => {
125
+ const bitmap = createBitmap();
126
+ const runtime = getEntityRuntime(bitmap) as BitmapRuntime;
127
+ const idBefore = runtime.localContentId;
128
+ setBitmapImage(bitmap, { width: 64, height: 64 } as ImageResource);
129
+ expect(runtime.localContentId).not.toBe(idBefore);
130
+ });
131
+ });
@@ -0,0 +1,37 @@
1
+ import type { DisplayObject } from '@flighthq/types';
2
+ import { DisplayObjectKind } from '@flighthq/types';
3
+
4
+ import { createDisplayObject, createDisplayObjectRuntime, getDisplayObjectRuntime } from './displayContainer';
5
+
6
+ describe('createDisplayObject', () => {
7
+ let displayContainer: DisplayObject;
8
+
9
+ beforeEach(() => {
10
+ displayContainer = createDisplayObject();
11
+ });
12
+
13
+ it('initializes default values', () => {
14
+ expect(displayContainer.kind).toStrictEqual(DisplayObjectKind);
15
+ });
16
+
17
+ it('returns a new object for better hidden-class performance', () => {
18
+ const base = {};
19
+ const obj = createDisplayObject(base);
20
+ expect(obj).not.toStrictEqual(base);
21
+ });
22
+ });
23
+
24
+ describe('createDisplayObjectRuntime', () => {
25
+ it('returns a non-null runtime', () => {
26
+ const runtime = createDisplayObjectRuntime();
27
+ expect(runtime).not.toBeNull();
28
+ });
29
+ });
30
+
31
+ describe('getDisplayObjectRuntime', () => {
32
+ it('returns the runtime for a DisplayObject', () => {
33
+ const container = createDisplayObject();
34
+ const runtime = getDisplayObjectRuntime(container);
35
+ expect(runtime).not.toBeNull();
36
+ });
37
+ });
@@ -0,0 +1,241 @@
1
+ import { getEntityRuntime } from '@flighthq/entity';
2
+ import { createColorTransform } from '@flighthq/materials';
3
+ import type { BoundsNode, Node2D, Node2DData, Node2DRuntime, PartialNode, Rectangle } from '@flighthq/types';
4
+ import { BlendMode, DisplayObjectKind, Node2DTraitsKey } from '@flighthq/types';
5
+
6
+ import {
7
+ addNode2DColorAdjustment,
8
+ createDisplayObject,
9
+ createNode2D,
10
+ createNode2DRuntime,
11
+ getNode2DColorAdjustments,
12
+ getNode2DRuntime,
13
+ isNode2D,
14
+ setNode2DClip,
15
+ setNode2DColorAdjustments,
16
+ setNode2DColorTransform,
17
+ } from './displayObject';
18
+
19
+ function getRuntime_(obj: Node2D): Node2DRuntime {
20
+ return getEntityRuntime(obj) as Node2DRuntime;
21
+ }
22
+
23
+ describe('addNode2DColorAdjustment', () => {
24
+ let obj: Node2D;
25
+ beforeEach(() => {
26
+ obj = createDisplayObject();
27
+ });
28
+
29
+ it('appends to a fresh (null) stack and invalidates appearance', () => {
30
+ const idBefore = getRuntime_(obj).appearanceId;
31
+ const adjustment = { kind: 'ColorTransformAdjustment' } as never;
32
+ addNode2DColorAdjustment(obj, adjustment);
33
+ expect(getNode2DColorAdjustments(obj)).toEqual([adjustment]);
34
+ expect(getRuntime_(obj).appearanceId).not.toBe(idBefore);
35
+ });
36
+
37
+ it('appends to an existing stack without mutating the previous array', () => {
38
+ const a = { kind: 'A' } as never;
39
+ const b = { kind: 'B' } as never;
40
+ addNode2DColorAdjustment(obj, a);
41
+ const first = getNode2DColorAdjustments(obj);
42
+ addNode2DColorAdjustment(obj, b);
43
+ expect(getNode2DColorAdjustments(obj)).toEqual([a, b]);
44
+ expect(first).toEqual([a]); // the earlier array was not mutated
45
+ });
46
+ });
47
+
48
+ describe('createDisplayObject', () => {
49
+ let displayObject: Node2D;
50
+
51
+ beforeEach(() => {
52
+ displayObject = createDisplayObject();
53
+ });
54
+
55
+ it('initializes default values', () => {
56
+ expect(displayObject.alpha).toBe(1);
57
+ expect(displayObject.blendMode).toBeNull();
58
+ expect(displayObject.name).toBeNull();
59
+ expect(displayObject.visible).toBe(true);
60
+ expect(displayObject.kind).toBe(DisplayObjectKind);
61
+ });
62
+
63
+ it('allows pre-defined values', () => {
64
+ const base = {
65
+ alpha: 2,
66
+ blendMode: BlendMode.Darken,
67
+ name: 'foo',
68
+ rotation: 45,
69
+ scaleX: 2,
70
+ scaleY: 3,
71
+ visible: false,
72
+ x: 100,
73
+ y: 200,
74
+ };
75
+ const obj = createDisplayObject(base);
76
+ expect(obj.alpha).toStrictEqual(base.alpha);
77
+ expect(obj.blendMode).toStrictEqual(base.blendMode);
78
+ expect(obj.name).toStrictEqual(base.name);
79
+ expect(obj.rotation).toStrictEqual(base.rotation);
80
+ expect(obj.scaleX).toStrictEqual(base.scaleX);
81
+ expect(obj.scaleY).toStrictEqual(base.scaleY);
82
+ expect(obj.visible).toStrictEqual(base.visible);
83
+ expect(obj.x).toStrictEqual(base.x);
84
+ expect(obj.y).toStrictEqual(base.y);
85
+ });
86
+
87
+ it('returns a new object for better hidden-class performance', () => {
88
+ const base = {};
89
+ const obj = createDisplayObject(base);
90
+ expect(obj).not.toStrictEqual(base);
91
+ });
92
+ });
93
+
94
+ describe('createNode2D', () => {
95
+ it('allows creation of a type without a data field', () => {
96
+ const displayObject = createNode2D(DisplayObjectKind);
97
+ expect(displayObject).not.toBeNull();
98
+ });
99
+
100
+ it('allows a custom type', () => {
101
+ const data: PartialNode<Node2DTest> = {
102
+ x: 100,
103
+ };
104
+ const displayObject = createNode2D(DisplayObjectKind, data);
105
+ expect(displayObject.x).toBe(data.x);
106
+ });
107
+
108
+ it('returns a new object', () => {
109
+ const data: PartialNode<Node2DTest> = {};
110
+ const displayObject = createNode2D(DisplayObjectKind, data);
111
+ expect(displayObject).not.toStrictEqual(data);
112
+ });
113
+
114
+ it('allows use of a data initializer', () => {
115
+ const data: PartialNode<Node2DTest> = {};
116
+ const displayObject = createNode2D(DisplayObjectKind, data, createNode2DTestData);
117
+ expect((displayObject.data as Node2DTestData).foo).toBe('bar');
118
+ });
119
+ });
120
+
121
+ describe('createNode2DRuntime', () => {
122
+ it('returns a graph runtime object', () => {
123
+ const runtime = createNode2DRuntime();
124
+ expect(runtime).not.toBeNull();
125
+ });
126
+
127
+ it('sets traits to Node2DTraitsKey', () => {
128
+ const runtime = createNode2DRuntime();
129
+ expect(runtime.traits).toBe(Node2DTraitsKey);
130
+ });
131
+
132
+ it('allows a custom bounds calculation', () => {
133
+ const func = (_out: Rectangle, _source: Readonly<BoundsNode<any>>) => {};
134
+ const runtime = createNode2DRuntime({ computeLocalBoundsRectangle: func });
135
+ expect(runtime.computeLocalBoundsRectangle).toStrictEqual(func);
136
+ });
137
+ });
138
+
139
+ describe('getNode2DColorAdjustments', () => {
140
+ it('defaults to null on a fresh node', () => {
141
+ expect(getNode2DColorAdjustments(createDisplayObject())).toBeNull();
142
+ });
143
+ });
144
+
145
+ describe('getNode2DRuntime', () => {
146
+ it('returns the runtime for a Node2D', () => {
147
+ const obj = createDisplayObject();
148
+ const runtime = getNode2DRuntime(obj);
149
+ expect(runtime).not.toBeNull();
150
+ });
151
+ });
152
+
153
+ describe('isNode2D', () => {
154
+ it('returns true for display objects', () => {
155
+ expect(isNode2D(createDisplayObject())).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe('setNode2DClip', () => {
160
+ let obj: Node2D;
161
+ beforeEach(() => {
162
+ obj = createDisplayObject();
163
+ });
164
+
165
+ it('sets clip', () => {
166
+ const clip = {
167
+ contours: null,
168
+ rect: { x: 0, y: 0, width: 100, height: 50 } as Rectangle,
169
+ winding: 'nonZero' as const,
170
+ version: 0,
171
+ };
172
+ setNode2DClip(obj, clip);
173
+ expect(obj.clip).toBe(clip);
174
+ });
175
+
176
+ it('accepts null', () => {
177
+ setNode2DClip(obj, null);
178
+ expect(obj.clip).toBeNull();
179
+ });
180
+
181
+ it('invalidates appearance', () => {
182
+ const idBefore = getRuntime_(obj).appearanceId;
183
+ setNode2DClip(obj, {
184
+ contours: null,
185
+ rect: { x: 0, y: 0, width: 10, height: 10 } as Rectangle,
186
+ winding: 'nonZero',
187
+ version: 0,
188
+ });
189
+ expect(getRuntime_(obj).appearanceId).not.toBe(idBefore);
190
+ });
191
+ });
192
+
193
+ describe('setNode2DColorAdjustments', () => {
194
+ let obj: Node2D;
195
+ beforeEach(() => {
196
+ obj = createDisplayObject();
197
+ });
198
+
199
+ it('sets the stack and invalidates appearance', () => {
200
+ const idBefore = getRuntime_(obj).appearanceId;
201
+ const stack = [{ kind: 'ColorTransformAdjustment' }] as never;
202
+ setNode2DColorAdjustments(obj, stack);
203
+ expect(getNode2DColorAdjustments(obj)).toBe(stack);
204
+ expect(getRuntime_(obj).appearanceId).not.toBe(idBefore);
205
+ });
206
+
207
+ it('accepts null', () => {
208
+ setNode2DColorAdjustments(obj, [{ kind: 'ColorTransformAdjustment' }] as never);
209
+ setNode2DColorAdjustments(obj, null);
210
+ expect(getNode2DColorAdjustments(obj)).toBeNull();
211
+ });
212
+ });
213
+
214
+ describe('setNode2DColorTransform', () => {
215
+ it('wraps a color transform as one ColorTransformAdjustment on the stack', () => {
216
+ const obj = createDisplayObject();
217
+ setNode2DColorTransform(obj, createColorTransform({ redMultiplier: 0.5 }));
218
+ const stack = getNode2DColorAdjustments(obj);
219
+ expect(stack?.length).toBe(1);
220
+ expect(stack?.[0].kind).toBe('ColorTransformAdjustment');
221
+ });
222
+
223
+ it('clears with null', () => {
224
+ const obj = createDisplayObject();
225
+ setNode2DColorTransform(obj, createColorTransform({ redMultiplier: 0.5 }));
226
+ setNode2DColorTransform(obj, null);
227
+ expect(getNode2DColorAdjustments(obj)).toBeNull();
228
+ });
229
+ });
230
+
231
+ interface Node2DTest extends Node2D {}
232
+
233
+ interface Node2DTestData extends Node2DData {
234
+ foo: string;
235
+ }
236
+
237
+ function createNode2DTestData(data?: Partial<Node2DTestData>): Node2DTestData {
238
+ return {
239
+ foo: data?.foo ?? 'bar',
240
+ };
241
+ }
@@ -0,0 +1,69 @@
1
+ import { createAnimationChannel, createAnimationClip, createAnimationTrack } from '@flighthq/animation';
2
+ import { getNodeLocalTransformRevision } from '@flighthq/node';
3
+ import type { Node2DAnimationPath, Node2DAnimationTarget } from '@flighthq/types';
4
+
5
+ import { createDisplayObject } from './displayObject';
6
+ import { applyAnimationClipToNode2D } from './displayObjectAnimation';
7
+
8
+ describe('applyAnimationClipToNode2D', () => {
9
+ it.each([
10
+ { expected: { x: 5, y: 10 }, path: 'Position', values: [0, 0, 10, 20] },
11
+ { expected: { pivotX: 5, pivotY: 10 }, path: 'Pivot', values: [0, 0, 10, 20] },
12
+ { expected: { scaleX: 2, scaleY: 3 }, path: 'Scale', values: [1, 1, 3, 5] },
13
+ { expected: { skewX: 0.25, skewY: 0.5 }, path: 'Skew', values: [0, 0, 0.5, 1] },
14
+ ] as const)('applies the $path vector path and invalidates transform', ({ expected, path, values }) => {
15
+ const node = createDisplayObject();
16
+ const before = getNodeLocalTransformRevision(node);
17
+ const clip = createBoundClip(node, path, 2, values);
18
+
19
+ applyAnimationClipToNode2D(clip, 0.5);
20
+
21
+ expect(node).toMatchObject(expected);
22
+ expect(getNodeLocalTransformRevision(node)).toBeGreaterThan(before);
23
+ });
24
+
25
+ it('applies scalar rotation, alpha, and visibility channels', () => {
26
+ const node = createDisplayObject();
27
+ const clip = createAnimationClip([
28
+ createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 2] }), {
29
+ node,
30
+ path: 'Rotation',
31
+ } satisfies Node2DAnimationTarget),
32
+ createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [1, 0.5] }), {
33
+ node,
34
+ path: 'Alpha',
35
+ } satisfies Node2DAnimationTarget),
36
+ createAnimationChannel(createAnimationTrack({ interpolation: 'Step', times: [0, 1], values: [0, 1] }), {
37
+ node,
38
+ path: 'Visible',
39
+ } satisfies Node2DAnimationTarget),
40
+ ]);
41
+
42
+ applyAnimationClipToNode2D(clip, 0.5);
43
+
44
+ expect(node.rotation).toBe(1);
45
+ expect(node.alpha).toBe(0.75);
46
+ expect(node.visible).toBe(false);
47
+ });
48
+
49
+ it('ignores foreign target refs', () => {
50
+ const clip = createAnimationClip([
51
+ createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 1] }), {}),
52
+ ]);
53
+ expect(() => applyAnimationClipToNode2D(clip, 0.5)).not.toThrow();
54
+ });
55
+ });
56
+
57
+ function createBoundClip(
58
+ node: ReturnType<typeof createDisplayObject>,
59
+ path: Node2DAnimationPath,
60
+ components: number,
61
+ values: ReadonlyArray<number>,
62
+ ) {
63
+ return createAnimationClip([
64
+ createAnimationChannel(createAnimationTrack({ components, times: [0, 1], values }), {
65
+ node,
66
+ path,
67
+ } satisfies Node2DAnimationTarget),
68
+ ]);
69
+ }
@@ -0,0 +1,111 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+ import type { HtmlView, Node } from '@flighthq/types';
3
+ import { HtmlViewKind } from '@flighthq/types';
4
+
5
+ import {
6
+ computeHtmlViewLocalBoundsRectangle,
7
+ createHtmlView,
8
+ createHtmlViewData,
9
+ createHtmlViewRuntime,
10
+ getHtmlViewRuntime,
11
+ setHtmlViewSize,
12
+ } from './htmlView';
13
+
14
+ describe('computeHtmlViewLocalBoundsRectangle', () => {
15
+ it('sets out dimensions from data width and height', () => {
16
+ const htmlView = createHtmlView({ data: { width: 320, height: 240 } });
17
+ const out = createRectangle();
18
+ computeHtmlViewLocalBoundsRectangle(out, htmlView as unknown as Node);
19
+ expect(out.width).toBe(320);
20
+ expect(out.height).toBe(240);
21
+ });
22
+ });
23
+
24
+ describe('createHtmlView', () => {
25
+ let htmlView: HtmlView;
26
+
27
+ beforeEach(() => {
28
+ htmlView = createHtmlView();
29
+ });
30
+
31
+ it('initializes default values', () => {
32
+ expect(htmlView.data.element).toBeNull();
33
+ expect(htmlView.data.width).toBe(100);
34
+ expect(htmlView.data.height).toBe(100);
35
+ expect(htmlView.kind).toStrictEqual(HtmlViewKind);
36
+ });
37
+
38
+ it('allows pre-defined values', () => {
39
+ const element = {} as HTMLImageElement;
40
+ const base = {
41
+ data: {
42
+ element: element,
43
+ width: 640,
44
+ height: 480,
45
+ },
46
+ };
47
+ const obj = createHtmlView(base);
48
+ expect(obj.data.element).toStrictEqual(element);
49
+ expect(obj.data.width).toBe(640);
50
+ expect(obj.data.height).toBe(480);
51
+ });
52
+
53
+ it('returns a new object for better hidden-class performance', () => {
54
+ const base = {};
55
+ const obj = createHtmlView(base);
56
+ expect(obj).not.toStrictEqual(base);
57
+ });
58
+ });
59
+
60
+ describe('createHtmlViewData', () => {
61
+ it('returns default values', () => {
62
+ const data = createHtmlViewData();
63
+ expect(data.element).toBeNull();
64
+ expect(data.width).toBe(100);
65
+ expect(data.height).toBe(100);
66
+ });
67
+
68
+ it('allows pre-defined values', () => {
69
+ const element = {} as HTMLImageElement;
70
+ const data = createHtmlViewData({ element, width: 200, height: 150 });
71
+ expect(data.element).toBe(element);
72
+ expect(data.width).toBe(200);
73
+ expect(data.height).toBe(150);
74
+ });
75
+ });
76
+
77
+ describe('createHtmlViewRuntime', () => {
78
+ it('returns a non-null runtime', () => {
79
+ const runtime = createHtmlViewRuntime();
80
+ expect(runtime).not.toBeNull();
81
+ });
82
+
83
+ it('uses computeHtmlViewLocalBoundsRectangle', () => {
84
+ const runtime = createHtmlViewRuntime();
85
+ expect(runtime.computeLocalBoundsRectangle).toStrictEqual(computeHtmlViewLocalBoundsRectangle);
86
+ });
87
+ });
88
+
89
+ describe('getHtmlViewRuntime', () => {
90
+ it('returns the runtime for an HtmlView', () => {
91
+ const htmlView = createHtmlView();
92
+ const runtime = getHtmlViewRuntime(htmlView);
93
+ expect(runtime).not.toBeNull();
94
+ });
95
+ });
96
+
97
+ describe('setHtmlViewSize', () => {
98
+ it('updates width and height', () => {
99
+ const htmlView = createHtmlView();
100
+ setHtmlViewSize(htmlView, 320, 240);
101
+ expect(htmlView.data.width).toBe(320);
102
+ expect(htmlView.data.height).toBe(240);
103
+ });
104
+
105
+ it('is a no-op when dimensions are unchanged', () => {
106
+ const htmlView = createHtmlView({ data: { width: 200, height: 150 } });
107
+ setHtmlViewSize(htmlView, 200, 150);
108
+ expect(htmlView.data.width).toBe(200);
109
+ expect(htmlView.data.height).toBe(150);
110
+ });
111
+ });