@flighthq/displayobject-canvas 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.
Files changed (150) hide show
  1. package/dist/canvasBackground.d.ts +3 -0
  2. package/dist/canvasBackground.d.ts.map +1 -0
  3. package/dist/canvasBackground.js +19 -0
  4. package/dist/canvasBackground.js.map +1 -0
  5. package/dist/canvasBitmap.d.ts +4 -0
  6. package/dist/canvasBitmap.d.ts.map +1 -0
  7. package/dist/canvasBitmap.js +32 -0
  8. package/dist/canvasBitmap.js.map +1 -0
  9. package/dist/canvasCSSFilterBinding.d.ts +24 -0
  10. package/dist/canvasCSSFilterBinding.d.ts.map +1 -0
  11. package/dist/canvasCSSFilterBinding.js +43 -0
  12. package/dist/canvasCSSFilterBinding.js.map +1 -0
  13. package/dist/canvasCache.d.ts +33 -0
  14. package/dist/canvasCache.d.ts.map +1 -0
  15. package/dist/canvasCache.js +141 -0
  16. package/dist/canvasCache.js.map +1 -0
  17. package/dist/canvasClip.d.ts +3 -0
  18. package/dist/canvasClip.d.ts.map +1 -0
  19. package/dist/canvasClip.js +38 -0
  20. package/dist/canvasClip.js.map +1 -0
  21. package/dist/canvasClipRectangle.d.ts +5 -0
  22. package/dist/canvasClipRectangle.d.ts.map +1 -0
  23. package/dist/canvasClipRectangle.js +31 -0
  24. package/dist/canvasClipRectangle.js.map +1 -0
  25. package/dist/canvasDisplayObject.d.ts +5 -0
  26. package/dist/canvasDisplayObject.d.ts.map +1 -0
  27. package/dist/canvasDisplayObject.js +46 -0
  28. package/dist/canvasDisplayObject.js.map +1 -0
  29. package/dist/canvasElement.d.ts +2 -0
  30. package/dist/canvasElement.d.ts.map +1 -0
  31. package/dist/canvasElement.js +9 -0
  32. package/dist/canvasElement.js.map +1 -0
  33. package/dist/canvasFillPattern.d.ts +4 -0
  34. package/dist/canvasFillPattern.d.ts.map +1 -0
  35. package/dist/canvasFillPattern.js +108 -0
  36. package/dist/canvasFillPattern.js.map +1 -0
  37. package/dist/canvasMaterialRegistry.d.ts +6 -0
  38. package/dist/canvasMaterialRegistry.d.ts.map +1 -0
  39. package/dist/canvasMaterialRegistry.js +42 -0
  40. package/dist/canvasMaterialRegistry.js.map +1 -0
  41. package/dist/canvasMaterials.d.ts +5 -0
  42. package/dist/canvasMaterials.d.ts.map +1 -0
  43. package/dist/canvasMaterials.js +33 -0
  44. package/dist/canvasMaterials.js.map +1 -0
  45. package/dist/canvasParticleEmitter.d.ts +4 -0
  46. package/dist/canvasParticleEmitter.d.ts.map +1 -0
  47. package/dist/canvasParticleEmitter.js +64 -0
  48. package/dist/canvasParticleEmitter.js.map +1 -0
  49. package/dist/canvasQuadBatch.d.ts +4 -0
  50. package/dist/canvasQuadBatch.d.ts.map +1 -0
  51. package/dist/canvasQuadBatch.js +64 -0
  52. package/dist/canvasQuadBatch.js.map +1 -0
  53. package/dist/canvasRegistration.d.ts +4 -0
  54. package/dist/canvasRegistration.d.ts.map +1 -0
  55. package/dist/canvasRegistration.js +39 -0
  56. package/dist/canvasRegistration.js.map +1 -0
  57. package/dist/canvasRenderState.d.ts +5 -0
  58. package/dist/canvasRenderState.d.ts.map +1 -0
  59. package/dist/canvasRenderState.js +44 -0
  60. package/dist/canvasRenderState.js.map +1 -0
  61. package/dist/canvasRenderTarget.d.ts +16 -0
  62. package/dist/canvasRenderTarget.d.ts.map +1 -0
  63. package/dist/canvasRenderTarget.js +66 -0
  64. package/dist/canvasRenderTarget.js.map +1 -0
  65. package/dist/canvasRichText.d.ts +7 -0
  66. package/dist/canvasRichText.d.ts.map +1 -0
  67. package/dist/canvasRichText.js +127 -0
  68. package/dist/canvasRichText.js.map +1 -0
  69. package/dist/canvasScale9Mapper.d.ts +10 -0
  70. package/dist/canvasScale9Mapper.d.ts.map +1 -0
  71. package/dist/canvasScale9Mapper.js +148 -0
  72. package/dist/canvasScale9Mapper.js.map +1 -0
  73. package/dist/canvasScale9Shape.d.ts +5 -0
  74. package/dist/canvasScale9Shape.d.ts.map +1 -0
  75. package/dist/canvasScale9Shape.js +156 -0
  76. package/dist/canvasScale9Shape.js.map +1 -0
  77. package/dist/canvasShape.d.ts +5 -0
  78. package/dist/canvasShape.d.ts.map +1 -0
  79. package/dist/canvasShape.js +79 -0
  80. package/dist/canvasShape.js.map +1 -0
  81. package/dist/canvasShapeCommands.d.ts +19 -0
  82. package/dist/canvasShapeCommands.d.ts.map +1 -0
  83. package/dist/canvasShapeCommands.js +352 -0
  84. package/dist/canvasShapeCommands.js.map +1 -0
  85. package/dist/canvasShapeRegistry.d.ts +5 -0
  86. package/dist/canvasShapeRegistry.d.ts.map +1 -0
  87. package/dist/canvasShapeRegistry.js +17 -0
  88. package/dist/canvasShapeRegistry.js.map +1 -0
  89. package/dist/canvasSprite.d.ts +5 -0
  90. package/dist/canvasSprite.d.ts.map +1 -0
  91. package/dist/canvasSprite.js +59 -0
  92. package/dist/canvasSprite.js.map +1 -0
  93. package/dist/canvasTextInput.d.ts +4 -0
  94. package/dist/canvasTextInput.d.ts.map +1 -0
  95. package/dist/canvasTextInput.js +67 -0
  96. package/dist/canvasTextInput.js.map +1 -0
  97. package/dist/canvasTextLabel.d.ts +4 -0
  98. package/dist/canvasTextLabel.d.ts.map +1 -0
  99. package/dist/canvasTextLabel.js +55 -0
  100. package/dist/canvasTextLabel.js.map +1 -0
  101. package/dist/canvasTextMeasure.d.ts +3 -0
  102. package/dist/canvasTextMeasure.d.ts.map +1 -0
  103. package/dist/canvasTextMeasure.js +15 -0
  104. package/dist/canvasTextMeasure.js.map +1 -0
  105. package/dist/canvasTilemap.d.ts +4 -0
  106. package/dist/canvasTilemap.d.ts.map +1 -0
  107. package/dist/canvasTilemap.js +50 -0
  108. package/dist/canvasTilemap.js.map +1 -0
  109. package/dist/canvasTransform.d.ts +3 -0
  110. package/dist/canvasTransform.d.ts.map +1 -0
  111. package/dist/canvasTransform.js +9 -0
  112. package/dist/canvasTransform.js.map +1 -0
  113. package/dist/canvasVideo.d.ts +4 -0
  114. package/dist/canvasVideo.d.ts.map +1 -0
  115. package/dist/canvasVideo.js +26 -0
  116. package/dist/canvasVideo.js.map +1 -0
  117. package/dist/index.d.ts +30 -0
  118. package/dist/index.d.ts.map +1 -0
  119. package/dist/index.js +30 -0
  120. package/dist/index.js.map +1 -0
  121. package/package.json +50 -0
  122. package/src/canvasBackground.test.ts +65 -0
  123. package/src/canvasBitmap.test.ts +92 -0
  124. package/src/canvasCSSFilterBinding.test.ts +86 -0
  125. package/src/canvasCache.test.ts +189 -0
  126. package/src/canvasClip.test.ts +12 -0
  127. package/src/canvasClipRectangle.test.ts +101 -0
  128. package/src/canvasDisplayObject.test.ts +117 -0
  129. package/src/canvasElement.test.ts +21 -0
  130. package/src/canvasFillPattern.test.ts +96 -0
  131. package/src/canvasMaterialRegistry.test.ts +69 -0
  132. package/src/canvasMaterials.test.ts +85 -0
  133. package/src/canvasParticleEmitter.test.ts +105 -0
  134. package/src/canvasQuadBatch.test.ts +119 -0
  135. package/src/canvasRegistration.test.ts +57 -0
  136. package/src/canvasRenderState.test.ts +186 -0
  137. package/src/canvasRenderTarget.test.ts +154 -0
  138. package/src/canvasRichText.test.ts +89 -0
  139. package/src/canvasScale9Mapper.test.ts +53 -0
  140. package/src/canvasScale9Shape.test.ts +89 -0
  141. package/src/canvasShape.test.ts +183 -0
  142. package/src/canvasShapeCommands.test.ts +448 -0
  143. package/src/canvasShapeRegistry.test.ts +42 -0
  144. package/src/canvasSprite.test.ts +90 -0
  145. package/src/canvasTextInput.test.ts +67 -0
  146. package/src/canvasTextLabel.test.ts +32 -0
  147. package/src/canvasTextMeasure.test.ts +9 -0
  148. package/src/canvasTilemap.test.ts +69 -0
  149. package/src/canvasTransform.test.ts +42 -0
  150. package/src/canvasVideo.test.ts +43 -0
@@ -0,0 +1,85 @@
1
+ import type { CanvasRenderState } from '@flighthq/types';
2
+ import { BlendMode } from '@flighthq/types';
3
+
4
+ import { applyCanvasBlendMode, enableCanvasBlendMode } from './canvasMaterials';
5
+ import { createCanvasRenderState, getCanvasRenderStateRuntime } from './canvasRenderState';
6
+
7
+ describe('applyCanvasBlendMode', () => {
8
+ let canvas: HTMLCanvasElement;
9
+ let state: CanvasRenderState;
10
+
11
+ beforeEach(() => {
12
+ canvas = document.createElement('canvas');
13
+ state = createCanvasRenderState(canvas);
14
+ });
15
+
16
+ it('should not change globalCompositeOperation if blend mode is the same', () => {
17
+ getCanvasRenderStateRuntime(state).currentBlendMode = BlendMode.Lighten;
18
+ state.context.globalCompositeOperation = 'lighter'; // Pre-set value
19
+ applyCanvasBlendMode(state, BlendMode.Add);
20
+ expect(state.context.globalCompositeOperation).toBe('lighter');
21
+ });
22
+
23
+ it('should set globalCompositeOperation to "lighter" for BlendMode.Add', () => {
24
+ applyCanvasBlendMode(state, BlendMode.Add);
25
+ expect(state.context.globalCompositeOperation).toBe('lighter');
26
+ });
27
+
28
+ it('should set globalCompositeOperation to "darken" for BlendMode.Darken', () => {
29
+ applyCanvasBlendMode(state, BlendMode.Darken);
30
+ expect(state.context.globalCompositeOperation).toBe('darken');
31
+ });
32
+
33
+ it('should set globalCompositeOperation to "difference" for BlendMode.Difference', () => {
34
+ applyCanvasBlendMode(state, BlendMode.Difference);
35
+ expect(state.context.globalCompositeOperation).toBe('difference');
36
+ });
37
+
38
+ it('should set globalCompositeOperation to "hard-light" for BlendMode.Hardlight', () => {
39
+ applyCanvasBlendMode(state, BlendMode.Hardlight);
40
+ expect(state.context.globalCompositeOperation).toBe('hard-light');
41
+ });
42
+
43
+ it('should set globalCompositeOperation to "lighten" for BlendMode.Lighten', () => {
44
+ applyCanvasBlendMode(state, BlendMode.Lighten);
45
+ expect(state.context.globalCompositeOperation).toBe('lighten');
46
+ });
47
+
48
+ it('should set globalCompositeOperation to "multiply" for BlendMode.Multiply', () => {
49
+ applyCanvasBlendMode(state, BlendMode.Multiply);
50
+ expect(state.context.globalCompositeOperation).toBe('multiply');
51
+ });
52
+
53
+ it('should set globalCompositeOperation to "overlay" for BlendMode.Overlay', () => {
54
+ applyCanvasBlendMode(state, BlendMode.Overlay);
55
+ expect(state.context.globalCompositeOperation).toBe('overlay');
56
+ });
57
+
58
+ it('should set globalCompositeOperation to "screen" for BlendMode.Screen', () => {
59
+ applyCanvasBlendMode(state, BlendMode.Screen);
60
+ expect(state.context.globalCompositeOperation).toBe('screen');
61
+ });
62
+
63
+ it('should set globalCompositeOperation to "source-over" for default case', () => {
64
+ applyCanvasBlendMode(state, null);
65
+ expect(state.context.globalCompositeOperation).toBe('source-over');
66
+ });
67
+ });
68
+
69
+ describe('enableCanvasBlendMode', () => {
70
+ it('wires applyBlendMode onto the state', () => {
71
+ const canvas = document.createElement('canvas');
72
+ const s = createCanvasRenderState(canvas);
73
+ expect(s.applyBlendMode).toBeNull();
74
+ enableCanvasBlendMode(s);
75
+ expect(s.applyBlendMode).not.toBeNull();
76
+ });
77
+
78
+ it('causes blend modes to be applied to the canvas context', () => {
79
+ const canvas = document.createElement('canvas');
80
+ const s = createCanvasRenderState(canvas);
81
+ enableCanvasBlendMode(s);
82
+ s.applyBlendMode!(s, BlendMode.Multiply);
83
+ expect(s.context.globalCompositeOperation).toBe('multiply');
84
+ });
85
+ });
@@ -0,0 +1,105 @@
1
+ import type { RenderProxy2D } from '@flighthq/types';
2
+
3
+ import { drawCanvasParticleEmitter } from './canvasParticleEmitter';
4
+ import { createCanvasRenderState } from './canvasRenderState';
5
+
6
+ function makeAtlas() {
7
+ const img = document.createElement('img') as HTMLImageElement;
8
+ return {
9
+ image: { source: img, width: 64, height: 64 },
10
+ regions: [{ id: 0, x: 0, y: 0, width: 32, height: 32 }],
11
+ };
12
+ }
13
+
14
+ function makeState() {
15
+ const canvas = document.createElement('canvas');
16
+ canvas.width = 800;
17
+ canvas.height = 400;
18
+ return createCanvasRenderState(canvas, {});
19
+ }
20
+
21
+ function makeRenderProxy(data: Record<string, unknown> = {}): RenderProxy2D {
22
+ return {
23
+ source: {
24
+ data: {
25
+ atlas: makeAtlas(),
26
+ particleCount: 1,
27
+ ids: new Uint16Array([0]),
28
+ transforms: new Float32Array([0, 0, 0, 1]),
29
+ alphas: new Float32Array([1]),
30
+ colors: new Float32Array([1, 1, 1]),
31
+ ...data,
32
+ },
33
+ },
34
+ blendMode: null,
35
+ alpha: 1,
36
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
37
+ } as unknown as RenderProxy2D;
38
+ }
39
+
40
+ describe('drawCanvasParticleEmitter', () => {
41
+ it('calls drawImage once for a single live particle', () => {
42
+ const state = makeState();
43
+ const spy = vi.spyOn(state.context, 'drawImage');
44
+ drawCanvasParticleEmitter(state, makeRenderProxy());
45
+ expect(spy).toHaveBeenCalledTimes(1);
46
+ });
47
+
48
+ it('calls drawImage once per live particle', () => {
49
+ const state = makeState();
50
+ const spy = vi.spyOn(state.context, 'drawImage');
51
+ drawCanvasParticleEmitter(
52
+ state,
53
+ makeRenderProxy({
54
+ particleCount: 3,
55
+ ids: new Uint16Array([0, 0, 0]),
56
+ transforms: new Float32Array([0, 0, 0, 1, 10, 10, 0, 1, 20, 20, 0, 1]),
57
+ alphas: new Float32Array([1, 0.5, 0.25]),
58
+ colors: new Float32Array([1, 1, 1, 1, 1, 1, 1, 1, 1]),
59
+ }),
60
+ );
61
+ expect(spy).toHaveBeenCalledTimes(3);
62
+ });
63
+
64
+ it('skips drawing when atlas is null', () => {
65
+ const state = makeState();
66
+ const spy = vi.spyOn(state.context, 'drawImage');
67
+ drawCanvasParticleEmitter(state, makeRenderProxy({ atlas: null }));
68
+ expect(spy).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it('skips drawing when particleCount is 0', () => {
72
+ const state = makeState();
73
+ const spy = vi.spyOn(state.context, 'drawImage');
74
+ drawCanvasParticleEmitter(state, makeRenderProxy({ particleCount: 0 }));
75
+ expect(spy).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it('does not throw on NaN particle transforms', () => {
79
+ const state = makeState();
80
+ expect(() =>
81
+ drawCanvasParticleEmitter(
82
+ state,
83
+ makeRenderProxy({
84
+ transforms: new Float32Array([Number.NaN, Number.NaN, Number.NaN, Number.NaN]),
85
+ }),
86
+ ),
87
+ ).not.toThrow();
88
+ });
89
+
90
+ it('skips particles with out-of-range region ids', () => {
91
+ const state = makeState();
92
+ const spy = vi.spyOn(state.context, 'drawImage');
93
+ drawCanvasParticleEmitter(
94
+ state,
95
+ makeRenderProxy({
96
+ particleCount: 3,
97
+ ids: new Uint16Array([0, 99, 0]),
98
+ transforms: new Float32Array([0, 0, 0, 1, 10, 10, 0, 1, 20, 20, 0, 1]),
99
+ alphas: new Float32Array([1, 1, 1]),
100
+ colors: new Float32Array([1, 1, 1, 1, 1, 1, 1, 1, 1]),
101
+ }),
102
+ );
103
+ expect(spy).toHaveBeenCalledTimes(2);
104
+ });
105
+ });
@@ -0,0 +1,119 @@
1
+ import { createImageResource } from '@flighthq/image';
2
+ import { getOrCreateRenderProxy2D, prepareDisplayObjectRender, registerRenderer } from '@flighthq/render';
3
+ import { createQuadBatch, reserveQuadBatch } from '@flighthq/sprite';
4
+ import { addTextureAtlasRegion, createTextureAtlas } from '@flighthq/textureatlas';
5
+ import { QuadBatchKind } from '@flighthq/types';
6
+
7
+ import { defaultCanvasQuadBatchRenderer, drawCanvasQuadBatch } from './canvasQuadBatch';
8
+ import { createCanvasRenderState } from './canvasRenderState';
9
+
10
+ function makeAtlas(regionCount = 1) {
11
+ const img = document.createElement('img') as HTMLImageElement;
12
+ const source = createImageResource(img);
13
+ source.width = 128;
14
+ source.height = 128;
15
+ const atlas = createTextureAtlas({ image: source });
16
+ for (let i = 0; i < regionCount; i++) {
17
+ addTextureAtlasRegion(atlas, i * 32, 0, 32, 32);
18
+ }
19
+ return atlas;
20
+ }
21
+
22
+ function makeState() {
23
+ const canvas = document.createElement('canvas');
24
+ canvas.width = 400;
25
+ canvas.height = 400;
26
+ const state = createCanvasRenderState(canvas);
27
+ registerRenderer(state, QuadBatchKind, defaultCanvasQuadBatchRenderer);
28
+ return state;
29
+ }
30
+
31
+ describe('defaultCanvasQuadBatchRenderer', () => {
32
+ it('has submit and createData', () => {
33
+ expect(typeof defaultCanvasQuadBatchRenderer.submit).toBe('function');
34
+ expect(typeof defaultCanvasQuadBatchRenderer.createData).toBe('function');
35
+ });
36
+ });
37
+
38
+ describe('drawCanvasQuadBatch', () => {
39
+ it('skips draw when atlas is null', () => {
40
+ const state = makeState();
41
+ const qb = createQuadBatch();
42
+ reserveQuadBatch(qb, 1);
43
+ qb.data.instanceCount = 1;
44
+ qb.data.atlas = null;
45
+
46
+ prepareDisplayObjectRender(state, qb);
47
+ const renderProxy = getOrCreateRenderProxy2D(state, qb);
48
+ const spy = vi.spyOn(state.context, 'drawImage');
49
+
50
+ drawCanvasQuadBatch(state, renderProxy);
51
+
52
+ expect(spy).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it('draws each instance with correct region', () => {
56
+ const state = makeState();
57
+ const atlas = makeAtlas(2);
58
+ const qb = createQuadBatch();
59
+ qb.data.atlas = atlas;
60
+ reserveQuadBatch(qb, 2);
61
+ qb.data.instanceCount = 2;
62
+ qb.data.ids[0] = 0;
63
+ qb.data.ids[1] = 1;
64
+ qb.data.transforms[0] = 10;
65
+ qb.data.transforms[1] = 20;
66
+ qb.data.transforms[2] = 30;
67
+ qb.data.transforms[3] = 40;
68
+
69
+ prepareDisplayObjectRender(state, qb);
70
+ const renderProxy = getOrCreateRenderProxy2D(state, qb);
71
+ const spy = vi.spyOn(state.context, 'drawImage');
72
+
73
+ drawCanvasQuadBatch(state, renderProxy);
74
+
75
+ expect(spy).toHaveBeenCalledTimes(2);
76
+ });
77
+
78
+ it('skips instances with out-of-range id', () => {
79
+ const state = makeState();
80
+ const atlas = makeAtlas(1);
81
+ const qb = createQuadBatch();
82
+ qb.data.atlas = atlas;
83
+ reserveQuadBatch(qb, 1);
84
+ qb.data.instanceCount = 1;
85
+ qb.data.ids[0] = 99; // out of range
86
+
87
+ prepareDisplayObjectRender(state, qb);
88
+ const renderProxy = getOrCreateRenderProxy2D(state, qb);
89
+ const spy = vi.spyOn(state.context, 'drawImage');
90
+
91
+ drawCanvasQuadBatch(state, renderProxy);
92
+
93
+ expect(spy).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it('skips instances with zero-size region', () => {
97
+ const state = makeState();
98
+ const img = document.createElement('img') as HTMLImageElement;
99
+ const source = createImageResource(img);
100
+ source.width = 128;
101
+ source.height = 128;
102
+ const atlas = createTextureAtlas({ image: source });
103
+ addTextureAtlasRegion(atlas, 0, 0, 0, 0); // zero size
104
+
105
+ const qb = createQuadBatch();
106
+ qb.data.atlas = atlas;
107
+ reserveQuadBatch(qb, 1);
108
+ qb.data.instanceCount = 1;
109
+ qb.data.ids[0] = 0;
110
+
111
+ prepareDisplayObjectRender(state, qb);
112
+ const renderProxy = getOrCreateRenderProxy2D(state, qb);
113
+ const spy = vi.spyOn(state.context, 'drawImage');
114
+
115
+ drawCanvasQuadBatch(state, renderProxy);
116
+
117
+ expect(spy).not.toHaveBeenCalled();
118
+ });
119
+ });
@@ -0,0 +1,57 @@
1
+ import {
2
+ BitmapKind,
3
+ DisplayObjectKind,
4
+ ParticleEmitterKind,
5
+ QuadBatchKind,
6
+ RichTextKind,
7
+ Scale9ShapeKind,
8
+ ShapeKind,
9
+ SpriteKind,
10
+ TextLabelKind,
11
+ TilemapKind,
12
+ VideoKind,
13
+ } from '@flighthq/types';
14
+
15
+ import { canvasDisplayObjectRendererEntries, registerCanvasDisplayObjectRenderers } from './canvasRegistration';
16
+ import { createCanvasRenderState, getCanvasRenderStateRuntime } from './canvasRenderState';
17
+
18
+ describe('canvasDisplayObjectRendererEntries', () => {
19
+ it('contains all expected display-object kinds', () => {
20
+ const kinds = canvasDisplayObjectRendererEntries.map(([kind]) => kind);
21
+ expect(kinds).toContain(BitmapKind);
22
+ expect(kinds).toContain(DisplayObjectKind);
23
+ expect(kinds).toContain(ParticleEmitterKind);
24
+ expect(kinds).toContain(QuadBatchKind);
25
+ expect(kinds).toContain(RichTextKind);
26
+ expect(kinds).toContain(Scale9ShapeKind);
27
+ expect(kinds).toContain(ShapeKind);
28
+ expect(kinds).toContain(SpriteKind);
29
+ expect(kinds).toContain(TextLabelKind);
30
+ expect(kinds).toContain(TilemapKind);
31
+ expect(kinds).toContain(VideoKind);
32
+ });
33
+
34
+ it('has a renderer for every entry', () => {
35
+ for (const [, renderer] of canvasDisplayObjectRendererEntries) {
36
+ expect(typeof renderer.submit).toBe('function');
37
+ }
38
+ });
39
+ });
40
+
41
+ describe('registerCanvasDisplayObjectRenderers', () => {
42
+ it('registers all default renderers into the state', () => {
43
+ const state = createCanvasRenderState(document.createElement('canvas'));
44
+ registerCanvasDisplayObjectRenderers(state);
45
+ const rendererMap = getCanvasRenderStateRuntime(state).rendererMap;
46
+ for (const [kind, renderer] of canvasDisplayObjectRendererEntries) {
47
+ expect(rendererMap.get(kind)).toBe(renderer);
48
+ }
49
+ });
50
+
51
+ it('registers exactly the same number of kinds as the entries array', () => {
52
+ const state = createCanvasRenderState(document.createElement('canvas'));
53
+ registerCanvasDisplayObjectRenderers(state);
54
+ const rendererMap = getCanvasRenderStateRuntime(state).rendererMap;
55
+ expect(rendererMap.size).toBe(canvasDisplayObjectRendererEntries.length);
56
+ });
57
+ });
@@ -0,0 +1,186 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+ import type { CanvasRenderOptions } from '@flighthq/types';
3
+ import { EntityRuntimeKey } from '@flighthq/types';
4
+
5
+ import {
6
+ createCanvasRenderState,
7
+ createCanvasRenderStateRuntime,
8
+ getCanvasRenderStateRuntime,
9
+ } from './canvasRenderState';
10
+
11
+ describe('createCanvasRenderState', () => {
12
+ it('creates state with a valid context and canvas', () => {
13
+ const c = document.createElement('canvas');
14
+ const state = createCanvasRenderState(c);
15
+ expect(state).not.toBeNull();
16
+ expect(state.canvas).toBe(c);
17
+ });
18
+
19
+ it('attaches a runtime under EntityRuntimeKey', () => {
20
+ const state = createCanvasRenderState(document.createElement('canvas'));
21
+ expect(state[EntityRuntimeKey]).not.toBeUndefined();
22
+ expect(getCanvasRenderStateRuntime(state)).toBe(state[EntityRuntimeKey]);
23
+ });
24
+ });
25
+
26
+ describe('createCanvasRenderStateRuntime', () => {
27
+ it('allocates an entity runtime', () => {
28
+ const runtime = createCanvasRenderStateRuntime();
29
+ expect(runtime).not.toBeNull();
30
+ expect(runtime.binding).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe('getCanvasRenderStateRuntime', () => {
35
+ it('returns the mutable runtime attached to the state', () => {
36
+ const state = createCanvasRenderState(document.createElement('canvas'));
37
+ const runtime = getCanvasRenderStateRuntime(state);
38
+ runtime.currentBlendMode = null;
39
+ expect(getCanvasRenderStateRuntime(state).currentBlendMode).toBeNull();
40
+ });
41
+ });
42
+
43
+ let canvas: HTMLCanvasElement;
44
+
45
+ beforeEach(() => {
46
+ // Mock canvas and context for testing
47
+ canvas = document.createElement('canvas');
48
+ const mockContext = {
49
+ getContextAttributes: vi.fn().mockReturnValue({
50
+ alpha: true,
51
+ desynchronized: false,
52
+ }),
53
+ imageSmoothingEnabled: true,
54
+ imageSmoothingQuality: 'high',
55
+ } as unknown as CanvasRenderingContext2D;
56
+ canvas.getContext = vi.fn().mockReturnValue(mockContext);
57
+ });
58
+
59
+ it('should be instantiated with default options', () => {
60
+ const renderer = createCanvasRenderState(canvas);
61
+
62
+ expect(renderer).not.toBeNull();
63
+ expect(renderer.canvas).toBe(canvas);
64
+ expect(renderer.context.imageSmoothingEnabled).toBe(true);
65
+ expect(renderer.context.imageSmoothingQuality).toBe('high');
66
+ expect(renderer.contextAttributes).toEqual({
67
+ alpha: true,
68
+ desynchronized: false,
69
+ });
70
+ expect(renderer.backgroundColor).toBe(0);
71
+ expect(renderer.pixelRatio).toBe(1);
72
+ expect(renderer.roundPixels).toBe(false);
73
+ expect(renderer.renderTransform2D).not.toBeNull();
74
+ });
75
+
76
+ it('should use provided options', () => {
77
+ const options: CanvasRenderOptions = {
78
+ backgroundColor: 0xffffff,
79
+ pixelRatio: 2,
80
+ roundPixels: true,
81
+ renderTransform: createMatrix(),
82
+ imageSmoothingEnabled: false,
83
+ imageSmoothingQuality: 'low',
84
+ };
85
+
86
+ const renderer = createCanvasRenderState(canvas, options);
87
+
88
+ expect(renderer.backgroundColor).toBe(0xffffff);
89
+ expect(renderer.pixelRatio).toBe(2);
90
+ expect(renderer.roundPixels).toBe(true);
91
+ expect(renderer.renderTransform2D).not.toBeNull();
92
+ expect(renderer.context.imageSmoothingEnabled).toBe(false);
93
+ expect(renderer.context.imageSmoothingQuality).toBe('low');
94
+ });
95
+
96
+ it('should throw an error if context is not available', () => {
97
+ canvas.getContext = vi.fn().mockReturnValue(null); // Simulate failure to get context
98
+
99
+ expect(() => createCanvasRenderState(canvas)).toThrowError('Failed to get context for canvas.');
100
+ });
101
+
102
+ it('should default imageSmoothingEnabled to true', () => {
103
+ const renderer = createCanvasRenderState(canvas);
104
+
105
+ expect(renderer.context.imageSmoothingEnabled).toBe(true);
106
+ });
107
+
108
+ it('should default imageSmoothingQuality to "high"', () => {
109
+ const renderer = createCanvasRenderState(canvas);
110
+
111
+ expect(renderer.context.imageSmoothingQuality).toBe('high');
112
+ });
113
+
114
+ it('should correctly handle backgroundColor option', () => {
115
+ const options: CanvasRenderOptions = {
116
+ backgroundColor: 0xff0000, // Red
117
+ };
118
+
119
+ const renderer = createCanvasRenderState(canvas, options);
120
+ expect(renderer.backgroundColor).toBe(0xff0000);
121
+ });
122
+
123
+ it('should use default pixelRatio if not provided', () => {
124
+ const renderer = createCanvasRenderState(canvas);
125
+ expect(renderer.pixelRatio).toBe(1);
126
+ });
127
+
128
+ it('should handle custom pixelRatio correctly', () => {
129
+ const options: CanvasRenderOptions = {
130
+ pixelRatio: 2,
131
+ };
132
+
133
+ const renderer = createCanvasRenderState(canvas, options);
134
+ expect(renderer.pixelRatio).toBe(2);
135
+ });
136
+
137
+ it('should default roundPixels to false', () => {
138
+ const renderer = createCanvasRenderState(canvas);
139
+ expect(renderer.roundPixels).toBe(false);
140
+ });
141
+
142
+ it('should correctly handle roundPixels option', () => {
143
+ const options: CanvasRenderOptions = {
144
+ roundPixels: true,
145
+ };
146
+
147
+ const renderer = createCanvasRenderState(canvas, options);
148
+ expect(renderer.roundPixels).toBe(true);
149
+ });
150
+
151
+ it('should handle worldTransform option correctly', () => {
152
+ const customTransform = createMatrix();
153
+ const options: CanvasRenderOptions = {
154
+ renderTransform: customTransform,
155
+ };
156
+
157
+ const renderer = createCanvasRenderState(canvas, options);
158
+ expect(renderer.renderTransform2D).toBe(customTransform);
159
+ });
160
+
161
+ it('should fall back to default Matrix if worldTransform is not provided', () => {
162
+ const renderer = createCanvasRenderState(canvas);
163
+ expect(renderer.renderTransform2D).not.toBeNull();
164
+ });
165
+
166
+ // Check if contextAttributes are passed and correctly retrieved
167
+ it('should retrieve contextAttributes from the context', () => {
168
+ const renderer = createCanvasRenderState(canvas);
169
+
170
+ expect(renderer.contextAttributes).toEqual({
171
+ alpha: true,
172
+ desynchronized: false,
173
+ });
174
+ });
175
+
176
+ // Ensure options with missing properties are handled gracefully
177
+ it('should handle missing imageSmoothingQuality and imageSmoothingEnabled in options', () => {
178
+ const options: CanvasRenderOptions = {
179
+ imageSmoothingEnabled: undefined,
180
+ imageSmoothingQuality: undefined,
181
+ };
182
+
183
+ const renderer = createCanvasRenderState(canvas, options);
184
+ expect(renderer.context.imageSmoothingEnabled).toBe(true);
185
+ expect(renderer.context.imageSmoothingQuality).toBe('high');
186
+ });