@flighthq/effects-canvas 0.3.0-next.906.07cea63 → 0.3.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 (269) hide show
  1. package/dist/canvasBevelEffect.d.ts +7 -0
  2. package/dist/canvasBevelEffect.d.ts.map +1 -0
  3. package/dist/canvasBevelEffect.js +76 -0
  4. package/dist/canvasBevelEffect.js.map +1 -0
  5. package/dist/canvasBlendEffect.d.ts +9 -0
  6. package/dist/canvasBlendEffect.d.ts.map +1 -0
  7. package/dist/canvasBlendEffect.js +89 -0
  8. package/dist/canvasBlendEffect.js.map +1 -0
  9. package/dist/canvasBloomEffect.d.ts +2 -1
  10. package/dist/canvasBloomEffect.d.ts.map +1 -1
  11. package/dist/canvasBloomEffect.js +4 -0
  12. package/dist/canvasBloomEffect.js.map +1 -1
  13. package/dist/canvasBlurEffect.d.ts +2 -1
  14. package/dist/canvasBlurEffect.d.ts.map +1 -1
  15. package/dist/canvasBlurEffect.js +4 -0
  16. package/dist/canvasBlurEffect.js.map +1 -1
  17. package/dist/canvasColorMatrixPass.d.ts +1 -0
  18. package/dist/canvasColorMatrixPass.d.ts.map +1 -1
  19. package/dist/canvasColorMatrixPass.js +22 -16
  20. package/dist/canvasColorMatrixPass.js.map +1 -1
  21. package/dist/canvasCompositeEffect.d.ts +6 -0
  22. package/dist/canvasCompositeEffect.d.ts.map +1 -0
  23. package/dist/canvasCompositeEffect.js +71 -0
  24. package/dist/canvasCompositeEffect.js.map +1 -0
  25. package/dist/canvasDropShadowEffect.d.ts +2 -1
  26. package/dist/canvasDropShadowEffect.d.ts.map +1 -1
  27. package/dist/canvasDropShadowEffect.js +4 -0
  28. package/dist/canvasDropShadowEffect.js.map +1 -1
  29. package/dist/canvasFilmGrainEffect.d.ts +2 -1
  30. package/dist/canvasFilmGrainEffect.d.ts.map +1 -1
  31. package/dist/canvasFilmGrainEffect.js +4 -0
  32. package/dist/canvasFilmGrainEffect.js.map +1 -1
  33. package/dist/canvasGradientBevelEffect.d.ts +6 -0
  34. package/dist/canvasGradientBevelEffect.d.ts.map +1 -0
  35. package/dist/canvasGradientBevelEffect.js +62 -0
  36. package/dist/canvasGradientBevelEffect.js.map +1 -0
  37. package/dist/canvasGradientGlowEffect.d.ts +6 -0
  38. package/dist/canvasGradientGlowEffect.d.ts.map +1 -0
  39. package/dist/canvasGradientGlowEffect.js +36 -0
  40. package/dist/canvasGradientGlowEffect.js.map +1 -0
  41. package/dist/canvasGradientRamp.d.ts +4 -0
  42. package/dist/canvasGradientRamp.d.ts.map +1 -0
  43. package/dist/canvasGradientRamp.js +75 -0
  44. package/dist/canvasGradientRamp.js.map +1 -0
  45. package/dist/canvasInnerGlowEffect.d.ts +6 -0
  46. package/dist/canvasInnerGlowEffect.d.ts.map +1 -0
  47. package/dist/canvasInnerGlowEffect.js +37 -0
  48. package/dist/canvasInnerGlowEffect.js.map +1 -0
  49. package/dist/canvasInnerShadowEffect.d.ts +6 -0
  50. package/dist/canvasInnerShadowEffect.d.ts.map +1 -0
  51. package/dist/canvasInnerShadowEffect.js +43 -0
  52. package/dist/canvasInnerShadowEffect.js.map +1 -0
  53. package/dist/canvasOuterGlowEffect.d.ts +2 -1
  54. package/dist/canvasOuterGlowEffect.d.ts.map +1 -1
  55. package/dist/canvasOuterGlowEffect.js +4 -0
  56. package/dist/canvasOuterGlowEffect.js.map +1 -1
  57. package/dist/canvasPixelateEffect.d.ts +2 -1
  58. package/dist/canvasPixelateEffect.d.ts.map +1 -1
  59. package/dist/canvasPixelateEffect.js +4 -0
  60. package/dist/canvasPixelateEffect.js.map +1 -1
  61. package/dist/canvasRenderEffectPipeline.d.ts.map +1 -1
  62. package/dist/canvasRenderEffectPipeline.js +9 -3
  63. package/dist/canvasRenderEffectPipeline.js.map +1 -1
  64. package/dist/canvasRenderEffectRegistry.d.ts.map +1 -1
  65. package/dist/canvasRenderEffectRegistry.js +9 -12
  66. package/dist/canvasRenderEffectRegistry.js.map +1 -1
  67. package/dist/canvasRenderTextureEffect.d.ts +3 -0
  68. package/dist/canvasRenderTextureEffect.d.ts.map +1 -0
  69. package/dist/canvasRenderTextureEffect.js +31 -0
  70. package/dist/canvasRenderTextureEffect.js.map +1 -0
  71. package/dist/canvasScanlinesEffect.d.ts +2 -1
  72. package/dist/canvasScanlinesEffect.d.ts.map +1 -1
  73. package/dist/canvasScanlinesEffect.js +4 -0
  74. package/dist/canvasScanlinesEffect.js.map +1 -1
  75. package/dist/canvasSourceModeCompositing.d.ts +1 -0
  76. package/dist/canvasSourceModeCompositing.d.ts.map +1 -1
  77. package/dist/canvasSourceModeCompositing.js +24 -0
  78. package/dist/canvasSourceModeCompositing.js.map +1 -1
  79. package/dist/canvasVignetteEffect.d.ts +2 -1
  80. package/dist/canvasVignetteEffect.d.ts.map +1 -1
  81. package/dist/canvasVignetteEffect.js +4 -0
  82. package/dist/canvasVignetteEffect.js.map +1 -1
  83. package/dist/contract.d.ts +9 -31
  84. package/dist/contract.d.ts.map +1 -1
  85. package/dist/contract.js +9 -31
  86. package/dist/contract.js.map +1 -1
  87. package/dist/index.d.ts +1 -1
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +1 -1
  90. package/dist/index.js.map +1 -1
  91. package/package.json +8 -6
  92. package/src/canvasBevelEffect.test.ts +212 -0
  93. package/src/canvasBlendEffect.test.ts +235 -0
  94. package/src/canvasBloomEffect.test.ts +9 -1
  95. package/src/canvasBlurEffect.test.ts +12 -1
  96. package/src/canvasColorMatrixPass.test.ts +61 -11
  97. package/src/canvasCompositeEffect.test.ts +181 -0
  98. package/src/canvasDropShadowEffect.test.ts +15 -8
  99. package/src/canvasEffectRegistration.test.ts +27 -0
  100. package/src/canvasFilmGrainEffect.test.ts +9 -1
  101. package/src/canvasGradientBevelEffect.test.ts +180 -0
  102. package/src/canvasGradientGlowEffect.test.ts +125 -0
  103. package/src/canvasGradientRamp.test.ts +98 -0
  104. package/src/canvasInnerGlowEffect.test.ts +152 -0
  105. package/src/canvasInnerShadowEffect.test.ts +173 -0
  106. package/src/canvasOuterGlowEffect.test.ts +15 -8
  107. package/src/canvasPixelateEffect.test.ts +9 -1
  108. package/src/canvasRealizedEffectRegistration.test.ts +30 -0
  109. package/src/canvasRenderEffectPipeline.test.ts +38 -0
  110. package/src/canvasRenderEffectRegistry.test.ts +10 -9
  111. package/src/canvasRenderTextureEffect.test.ts +73 -0
  112. package/src/canvasScanlinesEffect.test.ts +9 -1
  113. package/src/canvasSourceModeCompositing.test.ts +40 -0
  114. package/src/canvasVignetteEffect.test.ts +9 -1
  115. package/dist/canvasBokehDepthOfFieldEffect.d.ts +0 -4
  116. package/dist/canvasBokehDepthOfFieldEffect.d.ts.map +0 -1
  117. package/dist/canvasBokehDepthOfFieldEffect.js +0 -10
  118. package/dist/canvasBokehDepthOfFieldEffect.js.map +0 -1
  119. package/dist/canvasCameraMotionBlurEffect.d.ts +0 -4
  120. package/dist/canvasCameraMotionBlurEffect.d.ts.map +0 -1
  121. package/dist/canvasCameraMotionBlurEffect.js +0 -11
  122. package/dist/canvasCameraMotionBlurEffect.js.map +0 -1
  123. package/dist/canvasChromaticAberrationEffect.d.ts +0 -4
  124. package/dist/canvasChromaticAberrationEffect.d.ts.map +0 -1
  125. package/dist/canvasChromaticAberrationEffect.js +0 -11
  126. package/dist/canvasChromaticAberrationEffect.js.map +0 -1
  127. package/dist/canvasConvolutionEffect.d.ts +0 -4
  128. package/dist/canvasConvolutionEffect.d.ts.map +0 -1
  129. package/dist/canvasConvolutionEffect.js +0 -11
  130. package/dist/canvasConvolutionEffect.js.map +0 -1
  131. package/dist/canvasCrtEffect.d.ts +0 -4
  132. package/dist/canvasCrtEffect.d.ts.map +0 -1
  133. package/dist/canvasCrtEffect.js +0 -11
  134. package/dist/canvasCrtEffect.js.map +0 -1
  135. package/dist/canvasDirectionalBlurEffect.d.ts +0 -4
  136. package/dist/canvasDirectionalBlurEffect.d.ts.map +0 -1
  137. package/dist/canvasDirectionalBlurEffect.js +0 -11
  138. package/dist/canvasDirectionalBlurEffect.js.map +0 -1
  139. package/dist/canvasDisplacementEffect.d.ts +0 -4
  140. package/dist/canvasDisplacementEffect.d.ts.map +0 -1
  141. package/dist/canvasDisplacementEffect.js +0 -10
  142. package/dist/canvasDisplacementEffect.js.map +0 -1
  143. package/dist/canvasDitherEffect.d.ts +0 -4
  144. package/dist/canvasDitherEffect.d.ts.map +0 -1
  145. package/dist/canvasDitherEffect.js +0 -11
  146. package/dist/canvasDitherEffect.js.map +0 -1
  147. package/dist/canvasFxaaEffect.d.ts +0 -4
  148. package/dist/canvasFxaaEffect.d.ts.map +0 -1
  149. package/dist/canvasFxaaEffect.js +0 -11
  150. package/dist/canvasFxaaEffect.js.map +0 -1
  151. package/dist/canvasGlitchEffect.d.ts +0 -4
  152. package/dist/canvasGlitchEffect.d.ts.map +0 -1
  153. package/dist/canvasGlitchEffect.js +0 -11
  154. package/dist/canvasGlitchEffect.js.map +0 -1
  155. package/dist/canvasGodRaysEffect.d.ts +0 -4
  156. package/dist/canvasGodRaysEffect.d.ts.map +0 -1
  157. package/dist/canvasGodRaysEffect.js +0 -11
  158. package/dist/canvasGodRaysEffect.js.map +0 -1
  159. package/dist/canvasHalftoneEffect.d.ts +0 -4
  160. package/dist/canvasHalftoneEffect.d.ts.map +0 -1
  161. package/dist/canvasHalftoneEffect.js +0 -10
  162. package/dist/canvasHalftoneEffect.js.map +0 -1
  163. package/dist/canvasKuwaharaEffect.d.ts +0 -4
  164. package/dist/canvasKuwaharaEffect.d.ts.map +0 -1
  165. package/dist/canvasKuwaharaEffect.js +0 -11
  166. package/dist/canvasKuwaharaEffect.js.map +0 -1
  167. package/dist/canvasLensDirtEffect.d.ts +0 -4
  168. package/dist/canvasLensDirtEffect.d.ts.map +0 -1
  169. package/dist/canvasLensDirtEffect.js +0 -11
  170. package/dist/canvasLensDirtEffect.js.map +0 -1
  171. package/dist/canvasLensDistortionEffect.d.ts +0 -4
  172. package/dist/canvasLensDistortionEffect.d.ts.map +0 -1
  173. package/dist/canvasLensDistortionEffect.js +0 -10
  174. package/dist/canvasLensDistortionEffect.js.map +0 -1
  175. package/dist/canvasLensFlareEffect.d.ts +0 -4
  176. package/dist/canvasLensFlareEffect.d.ts.map +0 -1
  177. package/dist/canvasLensFlareEffect.js +0 -11
  178. package/dist/canvasLensFlareEffect.js.map +0 -1
  179. package/dist/canvasMedianEffect.d.ts +0 -4
  180. package/dist/canvasMedianEffect.d.ts.map +0 -1
  181. package/dist/canvasMedianEffect.js +0 -11
  182. package/dist/canvasMedianEffect.js.map +0 -1
  183. package/dist/canvasMotionBlurEffect.d.ts +0 -4
  184. package/dist/canvasMotionBlurEffect.d.ts.map +0 -1
  185. package/dist/canvasMotionBlurEffect.js +0 -11
  186. package/dist/canvasMotionBlurEffect.js.map +0 -1
  187. package/dist/canvasOutlineEffect.d.ts +0 -4
  188. package/dist/canvasOutlineEffect.d.ts.map +0 -1
  189. package/dist/canvasOutlineEffect.js +0 -11
  190. package/dist/canvasOutlineEffect.js.map +0 -1
  191. package/dist/canvasPosterizeEffect.d.ts +0 -4
  192. package/dist/canvasPosterizeEffect.d.ts.map +0 -1
  193. package/dist/canvasPosterizeEffect.js +0 -10
  194. package/dist/canvasPosterizeEffect.js.map +0 -1
  195. package/dist/canvasRadialBlurEffect.d.ts +0 -4
  196. package/dist/canvasRadialBlurEffect.d.ts.map +0 -1
  197. package/dist/canvasRadialBlurEffect.js +0 -11
  198. package/dist/canvasRadialBlurEffect.js.map +0 -1
  199. package/dist/canvasScreenSpaceFogEffect.d.ts +0 -4
  200. package/dist/canvasScreenSpaceFogEffect.d.ts.map +0 -1
  201. package/dist/canvasScreenSpaceFogEffect.js +0 -10
  202. package/dist/canvasScreenSpaceFogEffect.js.map +0 -1
  203. package/dist/canvasSharpenEffect.d.ts +0 -4
  204. package/dist/canvasSharpenEffect.d.ts.map +0 -1
  205. package/dist/canvasSharpenEffect.js +0 -11
  206. package/dist/canvasSharpenEffect.js.map +0 -1
  207. package/dist/canvasSketchEffect.d.ts +0 -4
  208. package/dist/canvasSketchEffect.d.ts.map +0 -1
  209. package/dist/canvasSketchEffect.js +0 -11
  210. package/dist/canvasSketchEffect.js.map +0 -1
  211. package/dist/canvasSmaaEffect.d.ts +0 -4
  212. package/dist/canvasSmaaEffect.d.ts.map +0 -1
  213. package/dist/canvasSmaaEffect.js +0 -11
  214. package/dist/canvasSmaaEffect.js.map +0 -1
  215. package/dist/canvasSsaoEffect.d.ts +0 -4
  216. package/dist/canvasSsaoEffect.d.ts.map +0 -1
  217. package/dist/canvasSsaoEffect.js +0 -11
  218. package/dist/canvasSsaoEffect.js.map +0 -1
  219. package/dist/canvasSsrEffect.d.ts +0 -4
  220. package/dist/canvasSsrEffect.d.ts.map +0 -1
  221. package/dist/canvasSsrEffect.js +0 -11
  222. package/dist/canvasSsrEffect.js.map +0 -1
  223. package/dist/canvasTaaEffect.d.ts +0 -4
  224. package/dist/canvasTaaEffect.d.ts.map +0 -1
  225. package/dist/canvasTaaEffect.js +0 -11
  226. package/dist/canvasTaaEffect.js.map +0 -1
  227. package/dist/canvasTiltShiftEffect.d.ts +0 -4
  228. package/dist/canvasTiltShiftEffect.d.ts.map +0 -1
  229. package/dist/canvasTiltShiftEffect.js +0 -11
  230. package/dist/canvasTiltShiftEffect.js.map +0 -1
  231. package/dist/canvasToneMapEffect.d.ts +0 -4
  232. package/dist/canvasToneMapEffect.d.ts.map +0 -1
  233. package/dist/canvasToneMapEffect.js +0 -12
  234. package/dist/canvasToneMapEffect.js.map +0 -1
  235. package/dist/canvasWhiteBalanceEffect.d.ts +0 -4
  236. package/dist/canvasWhiteBalanceEffect.d.ts.map +0 -1
  237. package/dist/canvasWhiteBalanceEffect.js +0 -11
  238. package/dist/canvasWhiteBalanceEffect.js.map +0 -1
  239. package/src/canvasBokehDepthOfFieldEffect.test.ts +0 -16
  240. package/src/canvasCameraMotionBlurEffect.test.ts +0 -16
  241. package/src/canvasChromaticAberrationEffect.test.ts +0 -16
  242. package/src/canvasConvolutionEffect.test.ts +0 -13
  243. package/src/canvasCrtEffect.test.ts +0 -13
  244. package/src/canvasDirectionalBlurEffect.test.ts +0 -16
  245. package/src/canvasDisplacementEffect.test.ts +0 -13
  246. package/src/canvasDitherEffect.test.ts +0 -13
  247. package/src/canvasFxaaEffect.test.ts +0 -13
  248. package/src/canvasGlitchEffect.test.ts +0 -13
  249. package/src/canvasGodRaysEffect.test.ts +0 -13
  250. package/src/canvasHalftoneEffect.test.ts +0 -13
  251. package/src/canvasKuwaharaEffect.test.ts +0 -13
  252. package/src/canvasLensDirtEffect.test.ts +0 -13
  253. package/src/canvasLensDistortionEffect.test.ts +0 -16
  254. package/src/canvasLensFlareEffect.test.ts +0 -13
  255. package/src/canvasMedianEffect.test.ts +0 -13
  256. package/src/canvasMotionBlurEffect.test.ts +0 -13
  257. package/src/canvasOutlineEffect.test.ts +0 -13
  258. package/src/canvasPosterizeEffect.test.ts +0 -13
  259. package/src/canvasRadialBlurEffect.test.ts +0 -13
  260. package/src/canvasScreenSpaceFogEffect.test.ts +0 -16
  261. package/src/canvasSharpenEffect.test.ts +0 -13
  262. package/src/canvasSketchEffect.test.ts +0 -13
  263. package/src/canvasSmaaEffect.test.ts +0 -13
  264. package/src/canvasSsaoEffect.test.ts +0 -13
  265. package/src/canvasSsrEffect.test.ts +0 -13
  266. package/src/canvasTaaEffect.test.ts +0 -13
  267. package/src/canvasTiltShiftEffect.test.ts +0 -13
  268. package/src/canvasToneMapEffect.test.ts +0 -13
  269. package/src/canvasWhiteBalanceEffect.test.ts +0 -13
@@ -0,0 +1,235 @@
1
+ import { createCanvasRenderState, createCanvasRenderTarget } from '@flighthq/scene2d-canvas/contract';
2
+ import type { BlendEffect, CanvasRenderState, CanvasRenderTarget } from '@flighthq/types/contract';
3
+
4
+ import {
5
+ applyBlendEffectToCanvas,
6
+ defaultCanvasBlendEffectRunner,
7
+ getCanvasBlendEffectBackdrop,
8
+ getCanvasBlendEffectCompositeOperation,
9
+ registerCanvasBlendEffect,
10
+ registerCanvasBlendEffectBackdrop,
11
+ unregisterCanvasBlendEffectBackdrop,
12
+ } from './canvasBlendEffect';
13
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
14
+
15
+ // These tests assert the DRAW CONTRACT, not pixels, and that is deliberate rather than a shortcut.
16
+ // jsdom's 2D context accepts every call and every globalCompositeOperation but rasterizes nothing — a
17
+ // fillRect followed by getImageData reads back transparent black — so a pixel assertion here would
18
+ // either pass vacuously or fail for a reason that has nothing to do with the effect. What IS verifiable
19
+ // in this environment is the sequence of operations the recipe issues, which carries the substantive
20
+ // claims: that the backdrop is laid down before the layer (every non-commutative mode gives a different
21
+ // image if that order flips), that the mode maps to the right native operation, and that opacity reaches
22
+ // globalAlpha. Real pixels belong to the functional/browser suite.
23
+ function recordDraws(target: Readonly<CanvasRenderTarget>): string[] {
24
+ const drawn: string[] = [];
25
+ const context = target.context;
26
+ vi.spyOn(context, 'drawImage').mockImplementation(((image: CanvasImageSource) => {
27
+ // Captured at call time, because the recipe changes both between the two draws.
28
+ drawn.push(
29
+ `${(image as HTMLCanvasElement).id || 'canvas'}|${context.globalCompositeOperation}|${context.globalAlpha}`,
30
+ );
31
+ }) as typeof context.drawImage);
32
+ return drawn;
33
+ }
34
+
35
+ function scene(): { state: CanvasRenderState; source: CanvasRenderTarget; dest: CanvasRenderTarget } {
36
+ const state = createCanvasRenderState(document.createElement('canvas'));
37
+ const source = createCanvasRenderTarget(4, 4);
38
+ const dest = createCanvasRenderTarget(4, 4);
39
+ source.canvas.id = 'source';
40
+ dest.canvas.id = 'dest';
41
+ return { state, source, dest };
42
+ }
43
+
44
+ function backdropTarget(): CanvasRenderTarget {
45
+ const backdrop = createCanvasRenderTarget(4, 4);
46
+ backdrop.canvas.id = 'backdrop';
47
+ return backdrop;
48
+ }
49
+
50
+ function blendEffect(over: Partial<BlendEffect> = {}): BlendEffect {
51
+ return { kind: 'BlendEffect', mode: 'Overlay', ...over } as BlendEffect;
52
+ }
53
+
54
+ afterEach(() => {
55
+ vi.restoreAllMocks();
56
+ });
57
+
58
+ describe('applyBlendEffectToCanvas', () => {
59
+ it('draws the backdrop first and then the layer under the named mode', () => {
60
+ const { state, source, dest } = scene();
61
+ const backdrop = backdropTarget();
62
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdrop);
63
+ const drawn = recordDraws(dest);
64
+
65
+ applyBlendEffectToCanvas(state, source, dest, blendEffect({ mode: 'Overlay', backdropKey: 'scene' }));
66
+
67
+ // Order is the assertion. The backdrop is the destination the mode reads, so it must already be in
68
+ // dest when the layer lands; drawn the other way round every non-commutative mode blends backwards.
69
+ expect(drawn).toEqual(['backdrop|source-over|1', 'source|overlay|1']);
70
+ });
71
+
72
+ it('scales the layer with globalAlpha when the effect carries an opacity', () => {
73
+ const { state, source, dest } = scene();
74
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
75
+ const drawn = recordDraws(dest);
76
+
77
+ applyBlendEffectToCanvas(state, source, dest, blendEffect({ backdropKey: 'scene', opacity: 0.25 }));
78
+
79
+ // Only the layer is scaled — the backdrop is the destination and must go down at full strength.
80
+ expect(drawn).toEqual(['backdrop|source-over|1', 'source|overlay|0.25']);
81
+ });
82
+
83
+ it('passes the layer through unblended when the named backdrop is not registered', () => {
84
+ const { state, source, dest } = scene();
85
+ const drawn = recordDraws(dest);
86
+
87
+ applyBlendEffectToCanvas(state, source, dest, blendEffect({ backdropKey: 'absent' }));
88
+
89
+ // One draw, source-over: an unregistered key blends over an implicit transparent backdrop, which
90
+ // reduces to a passthrough. Matches the GL contract, which gates its second sampler the same way.
91
+ expect(drawn).toEqual(['source|source-over|1']);
92
+ });
93
+
94
+ it('passes the layer through unblended when the effect names no backdrop at all', () => {
95
+ const { state, source, dest } = scene();
96
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
97
+ const drawn = recordDraws(dest);
98
+
99
+ applyBlendEffectToCanvas(state, source, dest, blendEffect());
100
+
101
+ // A registered backdrop the effect does not name must not be picked up implicitly.
102
+ expect(drawn).toEqual(['source|source-over|1']);
103
+ });
104
+ });
105
+
106
+ describe('defaultCanvasBlendEffectRunner', () => {
107
+ it('applies the blend through the pipeline context', () => {
108
+ const { state, source, dest } = scene();
109
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
110
+ const drawn = recordDraws(dest);
111
+
112
+ defaultCanvasBlendEffectRunner(
113
+ { state, source, dest, pool: { free: [], inUse: [] } },
114
+ blendEffect({ mode: 'Screen', backdropKey: 'scene' }),
115
+ );
116
+
117
+ // 'Screen' is NOT an AdvancedBlendMode — it is in the fixed-function BlendMode enum — so it has no
118
+ // entry here and must fall back rather than reach a native 'screen' operation by coincidence.
119
+ expect(drawn).toEqual(['backdrop|source-over|1', 'source|source-over|1']);
120
+ });
121
+ });
122
+
123
+ describe('getCanvasBlendEffectBackdrop', () => {
124
+ it('returns the registered target for its key', () => {
125
+ const state = createCanvasRenderState(document.createElement('canvas'));
126
+ const backdrop = backdropTarget();
127
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdrop);
128
+
129
+ expect(getCanvasBlendEffectBackdrop(state, 'scene')).toBe(backdrop);
130
+ });
131
+
132
+ it('returns null for an unregistered key and for a null key', () => {
133
+ const state = createCanvasRenderState(document.createElement('canvas'));
134
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
135
+
136
+ expect(getCanvasBlendEffectBackdrop(state, 'other')).toBe(null);
137
+ expect(getCanvasBlendEffectBackdrop(state, null)).toBe(null);
138
+ });
139
+
140
+ it('returns null before anything has been registered on the state', () => {
141
+ // The registry slot is absent until first use, so the query must not assume a map exists.
142
+ const state = createCanvasRenderState(document.createElement('canvas'));
143
+
144
+ expect(getCanvasBlendEffectBackdrop(state, 'scene')).toBe(null);
145
+ });
146
+
147
+ it('keeps backdrops separate per render state', () => {
148
+ const first = createCanvasRenderState(document.createElement('canvas'));
149
+ const second = createCanvasRenderState(document.createElement('canvas'));
150
+ registerCanvasBlendEffectBackdrop(first, 'scene', backdropTarget());
151
+
152
+ expect(getCanvasBlendEffectBackdrop(second, 'scene')).toBe(null);
153
+ });
154
+ });
155
+
156
+ describe('getCanvasBlendEffectCompositeOperation', () => {
157
+ it('maps every AdvancedBlendMode to a native operation', () => {
158
+ // All eleven, including the four non-separable HSL modes, which is the reason Canvas needs no blend
159
+ // math of its own. A mode missing from the table would silently become a passthrough.
160
+ expect(
161
+ [
162
+ 'Color',
163
+ 'ColorBurn',
164
+ 'ColorDodge',
165
+ 'Difference',
166
+ 'Exclusion',
167
+ 'HardLight',
168
+ 'Hue',
169
+ 'Luminosity',
170
+ 'Overlay',
171
+ 'Saturation',
172
+ 'SoftLight',
173
+ ].map(getCanvasBlendEffectCompositeOperation),
174
+ ).toEqual([
175
+ 'color',
176
+ 'color-burn',
177
+ 'color-dodge',
178
+ 'difference',
179
+ 'exclusion',
180
+ 'hard-light',
181
+ 'hue',
182
+ 'luminosity',
183
+ 'overlay',
184
+ 'saturation',
185
+ 'soft-light',
186
+ ]);
187
+ });
188
+
189
+ it('falls back to source-over for a mode it does not know', () => {
190
+ expect(getCanvasBlendEffectCompositeOperation('acme.Sparkle')).toBe('source-over');
191
+ });
192
+ });
193
+
194
+ describe('registerCanvasBlendEffect', () => {
195
+ it('registers the default runner under the BlendEffect kind', () => {
196
+ const state = createCanvasRenderState(document.createElement('canvas'));
197
+ registerCanvasBlendEffect(state);
198
+
199
+ expect(getCanvasRenderEffectRunner(state, 'BlendEffect')).toBe(defaultCanvasBlendEffectRunner);
200
+ });
201
+ });
202
+
203
+ describe('registerCanvasBlendEffectBackdrop', () => {
204
+ it('replaces the target already held under the same key', () => {
205
+ const state = createCanvasRenderState(document.createElement('canvas'));
206
+ const replacement = backdropTarget();
207
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
208
+ registerCanvasBlendEffectBackdrop(state, 'scene', replacement);
209
+
210
+ expect(getCanvasBlendEffectBackdrop(state, 'scene')).toBe(replacement);
211
+ });
212
+ });
213
+
214
+ describe('unregisterCanvasBlendEffectBackdrop', () => {
215
+ it('drops the registered target and reports that one was present', () => {
216
+ const state = createCanvasRenderState(document.createElement('canvas'));
217
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
218
+
219
+ expect(unregisterCanvasBlendEffectBackdrop(state, 'scene')).toBe(true);
220
+ expect(getCanvasBlendEffectBackdrop(state, 'scene')).toBe(null);
221
+ });
222
+
223
+ it('reports false for a key that was never registered', () => {
224
+ const state = createCanvasRenderState(document.createElement('canvas'));
225
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
226
+
227
+ expect(unregisterCanvasBlendEffectBackdrop(state, 'other')).toBe(false);
228
+ });
229
+
230
+ it('reports false before anything has been registered on the state', () => {
231
+ const state = createCanvasRenderState(document.createElement('canvas'));
232
+
233
+ expect(unregisterCanvasBlendEffectBackdrop(state, 'scene')).toBe(false);
234
+ });
235
+ });
@@ -1,4 +1,8 @@
1
- import { applyBloomEffectToCanvas, defaultCanvasBloomEffectRunner } from './canvasBloomEffect';
1
+ import {
2
+ applyBloomEffectToCanvas,
3
+ defaultCanvasBloomEffectRunner,
4
+ registerCanvasBloomEffect,
5
+ } from './canvasBloomEffect';
2
6
 
3
7
  describe('applyBloomEffectToCanvas', () => {
4
8
  it('is a function', () => {
@@ -11,3 +15,7 @@ describe('defaultCanvasBloomEffectRunner', () => {
11
15
  expect(typeof defaultCanvasBloomEffectRunner).toBe('function');
12
16
  });
13
17
  });
18
+
19
+ describe('registerCanvasBloomEffect', () => {
20
+ it('is a function', () => expect(registerCanvasBloomEffect).toBeTypeOf('function'));
21
+ });
@@ -1,4 +1,7 @@
1
- import { applyBlurEffectToCanvas, defaultCanvasBlurEffectRunner } from './canvasBlurEffect';
1
+ import { createCanvasRenderState } from '@flighthq/scene2d-canvas/contract';
2
+
3
+ import { applyBlurEffectToCanvas, defaultCanvasBlurEffectRunner, registerCanvasBlurEffect } from './canvasBlurEffect';
4
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
2
5
 
3
6
  describe('applyBlurEffectToCanvas', () => {
4
7
  it('is a function', () => {
@@ -11,3 +14,11 @@ describe('defaultCanvasBlurEffectRunner', () => {
11
14
  expect(typeof defaultCanvasBlurEffectRunner).toBe('function');
12
15
  });
13
16
  });
17
+
18
+ describe('registerCanvasBlurEffect', () => {
19
+ it('registers the default runner under the BlurEffect kind', () => {
20
+ const state = createCanvasRenderState(document.createElement('canvas'));
21
+ registerCanvasBlurEffect(state);
22
+ expect(getCanvasRenderEffectRunner(state, 'BlurEffect')).toBe(defaultCanvasBlurEffectRunner);
23
+ });
24
+ });
@@ -1,22 +1,72 @@
1
- import { applyColorMatrixPassToCanvas } from './canvasColorMatrixPass';
1
+ import type { CanvasRenderTarget } from '@flighthq/types/contract';
2
2
 
3
- const { drawCanvasImageDataPass } = vi.hoisted(() => ({ drawCanvasImageDataPass: vi.fn() }));
3
+ import { applyColorMatrixPassToCanvas, applyColorMatrixToImageDataBytes } from './canvasColorMatrixPass';
4
4
 
5
- vi.mock('./canvasEffectCompositing', () => ({ drawCanvasImageDataPass }));
5
+ // Minimal stand-ins for the two canvas contexts drawCanvasImageDataPass touches. Using plain objects
6
+ // rather than substituting the compositing module keeps the wiring assertion independent of module
7
+ // load order, which matters because the suite runs non-isolated (one module registry per worker).
8
+ function createStubTargets(pixels: ReadonlyArray<number>): {
9
+ dest: CanvasRenderTarget;
10
+ written: { data: Uint8ClampedArray | null };
11
+ source: CanvasRenderTarget;
12
+ } {
13
+ const imageData = { data: new Uint8ClampedArray(pixels) };
14
+ const written: { data: Uint8ClampedArray | null } = { data: null };
15
+ const source = {
16
+ context: { getImageData: () => imageData },
17
+ height: 1,
18
+ width: pixels.length / 4,
19
+ } as unknown as CanvasRenderTarget;
20
+ const dest = {
21
+ context: {
22
+ clearRect: () => {},
23
+ filter: 'none',
24
+ globalAlpha: 1,
25
+ globalCompositeOperation: 'source-over',
26
+ putImageData: (written_: { data: Uint8ClampedArray }) => {
27
+ written.data = written_.data;
28
+ },
29
+ restore: () => {},
30
+ save: () => {},
31
+ setTransform: () => {},
32
+ },
33
+ height: 1,
34
+ width: pixels.length / 4,
35
+ } as unknown as CanvasRenderTarget;
36
+ return { dest, source, written };
37
+ }
38
+
39
+ const BIAS_MATRIX = [1, 0, 0, 0, 0.5, 0, 1, 0, 0, 0.25, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
6
40
 
7
41
  describe('applyColorMatrixPassToCanvas', () => {
8
42
  it('is a function', () => {
9
43
  expect(typeof applyColorMatrixPassToCanvas).toBe('function');
10
44
  });
11
45
 
46
+ it('reads the source pixels, applies the matrix, and writes the result to the destination', () => {
47
+ const { dest, source, written } = createStubTargets([0, 0, 0, 255]);
48
+ applyColorMatrixPassToCanvas(source, dest, BIAS_MATRIX);
49
+ expect(written.data).not.toBeNull();
50
+ expect(Array.from(written.data!)).toEqual([128, 64, 0, 255]);
51
+ });
52
+ });
53
+
54
+ describe('applyColorMatrixToImageDataBytes', () => {
12
55
  it('converts normalized-linear matrix bias to the ImageData byte domain', () => {
13
- drawCanvasImageDataPass.mockImplementationOnce((_dest, _source, transform) => {
14
- const data = new Uint8ClampedArray([0, 0, 0, 255]);
15
- transform(data, 1);
16
- expect(Array.from(data)).toEqual([128, 64, 0, 255]);
17
- });
18
- const matrix = [1, 0, 0, 0, 0.5, 0, 1, 0, 0, 0.25, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
19
- applyColorMatrixPassToCanvas({} as never, {} as never, matrix);
20
- expect(drawCanvasImageDataPass).toHaveBeenCalledOnce();
56
+ const data = new Uint8ClampedArray([0, 0, 0, 255]);
57
+ applyColorMatrixToImageDataBytes(data, 1, BIAS_MATRIX);
58
+ expect(Array.from(data)).toEqual([128, 64, 0, 255]);
59
+ });
60
+
61
+ it('applies the matrix to every pixel in the run', () => {
62
+ const data = new Uint8ClampedArray([0, 0, 0, 255, 0, 0, 0, 255]);
63
+ applyColorMatrixToImageDataBytes(data, 2, BIAS_MATRIX);
64
+ expect(Array.from(data)).toEqual([128, 64, 0, 255, 128, 64, 0, 255]);
65
+ });
66
+
67
+ it('treats absent matrix entries as zero', () => {
68
+ const data = new Uint8ClampedArray([200, 200, 200, 255]);
69
+ applyColorMatrixToImageDataBytes(data, 1, []);
70
+ expect(Array.from(data)).toEqual([0, 0, 0, 0]);
21
71
  });
22
72
  });
@@ -0,0 +1,181 @@
1
+ import { createCanvasRenderState, createCanvasRenderTarget } from '@flighthq/scene2d-canvas/contract';
2
+ import type { CanvasRenderState, CanvasRenderTarget, CompositeEffect } from '@flighthq/types/contract';
3
+
4
+ import { registerCanvasBlendEffectBackdrop } from './canvasBlendEffect';
5
+ import {
6
+ applyCompositeEffectToCanvas,
7
+ defaultCanvasCompositeEffectRunner,
8
+ getCanvasCompositeEffectOperation,
9
+ registerCanvasCompositeEffect,
10
+ } from './canvasCompositeEffect';
11
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
12
+
13
+ // Draw-contract assertions rather than pixels, for the reason spelled out in canvasBlendEffect.test.ts:
14
+ // jsdom's 2D context accepts every call and rasterizes nothing, so a pixel assertion would pass
15
+ // vacuously. What is verifiable — and what carries the correctness claims — is which images are drawn,
16
+ // in what order, and under which globalCompositeOperation.
17
+ function recordDraws(target: Readonly<CanvasRenderTarget>): string[] {
18
+ const drawn: string[] = [];
19
+ const context = target.context;
20
+ vi.spyOn(context, 'drawImage').mockImplementation(((image: CanvasImageSource) => {
21
+ drawn.push(`${(image as HTMLCanvasElement).id || 'canvas'}|${context.globalCompositeOperation}`);
22
+ }) as typeof context.drawImage);
23
+ return drawn;
24
+ }
25
+
26
+ function scene(): { state: CanvasRenderState; source: CanvasRenderTarget; dest: CanvasRenderTarget } {
27
+ const state = createCanvasRenderState(document.createElement('canvas'));
28
+ const source = createCanvasRenderTarget(4, 4);
29
+ const dest = createCanvasRenderTarget(4, 4);
30
+ source.canvas.id = 'source';
31
+ dest.canvas.id = 'dest';
32
+ return { state, source, dest };
33
+ }
34
+
35
+ function backdropTarget(): CanvasRenderTarget {
36
+ const backdrop = createCanvasRenderTarget(4, 4);
37
+ backdrop.canvas.id = 'backdrop';
38
+ return backdrop;
39
+ }
40
+
41
+ function compositeEffect(over: Partial<CompositeEffect> = {}): CompositeEffect {
42
+ return { kind: 'CompositeEffect', operator: 'SourceOver', ...over } as CompositeEffect;
43
+ }
44
+
45
+ afterEach(() => {
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ describe('applyCompositeEffectToCanvas', () => {
50
+ it('draws the backdrop first and then the layer under the named operator', () => {
51
+ const { state, source, dest } = scene();
52
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
53
+ const drawn = recordDraws(dest);
54
+
55
+ applyCompositeEffectToCanvas(
56
+ state,
57
+ source,
58
+ dest,
59
+ compositeEffect({ operator: 'DestinationOut', backdropKey: 'scene' }),
60
+ );
61
+
62
+ // The backdrop is the destination every Porter-Duff operator is defined against, so it must be in
63
+ // the target before the layer lands. DestinationOut is the classic erase: reversing the order would
64
+ // punch the hole in the wrong image.
65
+ expect(drawn).toEqual(['backdrop|source-over', 'source|destination-out']);
66
+ });
67
+
68
+ it('composites against a transparent destination when the named backdrop is not registered', () => {
69
+ const { state, source, dest } = scene();
70
+ const drawn = recordDraws(dest);
71
+
72
+ applyCompositeEffectToCanvas(state, source, dest, compositeEffect({ operator: 'SourceIn', backdropKey: 'absent' }));
73
+
74
+ // Still draws, and still under SourceIn — an absent backdrop is a transparent DESTINATION, not a
75
+ // reason to skip the composite. SourceIn against zero coverage correctly yields nothing, which is
76
+ // the operator doing its job rather than a fallback.
77
+ expect(drawn).toEqual(['source|source-in']);
78
+ });
79
+
80
+ it('reads no backdrop the effect has not named', () => {
81
+ const { state, source, dest } = scene();
82
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
83
+ const drawn = recordDraws(dest);
84
+
85
+ applyCompositeEffectToCanvas(state, source, dest, compositeEffect());
86
+
87
+ expect(drawn).toEqual(['source|source-over']);
88
+ });
89
+
90
+ it('shares the backdrop registry with BlendEffect rather than keeping its own', () => {
91
+ // Registered through the Blend entry point and read by the Composite pass, which is the GL contract:
92
+ // both effects resolve their backdrop through the same per-state registry.
93
+ const { state, source, dest } = scene();
94
+ registerCanvasBlendEffectBackdrop(state, 'shared', backdropTarget());
95
+ const drawn = recordDraws(dest);
96
+
97
+ applyCompositeEffectToCanvas(state, source, dest, compositeEffect({ operator: 'Xor', backdropKey: 'shared' }));
98
+
99
+ expect(drawn).toEqual(['backdrop|source-over', 'source|xor']);
100
+ });
101
+
102
+ it('draws neither input for Clear, even with a backdrop registered', () => {
103
+ // Clear means both coverage factors are zero, so the cleared target IS the result. Drawing the
104
+ // backdrop first and only skipping the layer would leave the backdrop showing through.
105
+ const { state, source, dest } = scene();
106
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
107
+ const drawn = recordDraws(dest);
108
+ const cleared = vi.spyOn(dest.context, 'clearRect');
109
+
110
+ applyCompositeEffectToCanvas(state, source, dest, compositeEffect({ operator: 'Clear', backdropKey: 'scene' }));
111
+
112
+ expect(drawn).toEqual([]);
113
+ expect(cleared).toHaveBeenCalledWith(0, 0, 4, 4);
114
+ });
115
+ });
116
+
117
+ describe('defaultCanvasCompositeEffectRunner', () => {
118
+ it('applies the composite through the pipeline context', () => {
119
+ const { state, source, dest } = scene();
120
+ registerCanvasBlendEffectBackdrop(state, 'scene', backdropTarget());
121
+ const drawn = recordDraws(dest);
122
+
123
+ defaultCanvasCompositeEffectRunner(
124
+ { state, source, dest, pool: { free: [], inUse: [] } },
125
+ compositeEffect({ operator: 'DestinationOver', backdropKey: 'scene' }),
126
+ );
127
+
128
+ expect(drawn).toEqual(['backdrop|source-over', 'source|destination-over']);
129
+ });
130
+ });
131
+
132
+ describe('getCanvasCompositeEffectOperation', () => {
133
+ it('maps every operator Canvas realizes natively', () => {
134
+ // Ten of the eleven. Canvas 2D owns this vocabulary, so each is a direct rename of the same
135
+ // Porter-Duff term; a missing entry would silently become SourceOver and composite the wrong way.
136
+ expect(
137
+ [
138
+ 'Copy',
139
+ 'DestinationAtop',
140
+ 'DestinationIn',
141
+ 'DestinationOut',
142
+ 'DestinationOver',
143
+ 'SourceAtop',
144
+ 'SourceIn',
145
+ 'SourceOut',
146
+ 'SourceOver',
147
+ 'Xor',
148
+ ].map(getCanvasCompositeEffectOperation),
149
+ ).toEqual([
150
+ 'copy',
151
+ 'destination-atop',
152
+ 'destination-in',
153
+ 'destination-out',
154
+ 'destination-over',
155
+ 'source-atop',
156
+ 'source-in',
157
+ 'source-out',
158
+ 'source-over',
159
+ 'xor',
160
+ ]);
161
+ });
162
+
163
+ it('falls back to source-over for an operator it does not know', () => {
164
+ expect(getCanvasCompositeEffectOperation('acme.Dissolve')).toBe('source-over');
165
+ });
166
+
167
+ it('has no entry for Clear, which the pass handles by clearing instead', () => {
168
+ // Pinned so that "adding the missing Clear entry" cannot look like a fix: Canvas has no 'clear'
169
+ // operation, and an entry here would route Clear into a draw the operator is defined not to make.
170
+ expect(getCanvasCompositeEffectOperation('Clear')).toBe('source-over');
171
+ });
172
+ });
173
+
174
+ describe('registerCanvasCompositeEffect', () => {
175
+ it('registers the default runner under the CompositeEffect kind', () => {
176
+ const state = createCanvasRenderState(document.createElement('canvas'));
177
+ registerCanvasCompositeEffect(state);
178
+
179
+ expect(getCanvasRenderEffectRunner(state, 'CompositeEffect')).toBe(defaultCanvasCompositeEffectRunner);
180
+ });
181
+ });
@@ -28,7 +28,11 @@ vi.mock('./canvasSourceModeCompositing', () => ({
28
28
  drawCanvasTintedAlphaMask: vi.fn(),
29
29
  }));
30
30
 
31
- import { applyDropShadowEffectToCanvas, defaultCanvasDropShadowEffectRunner } from './canvasDropShadowEffect';
31
+ import {
32
+ applyDropShadowEffectToCanvas,
33
+ defaultCanvasDropShadowEffectRunner,
34
+ registerCanvasDropShadowEffect,
35
+ } from './canvasDropShadowEffect';
32
36
  import { drawCanvasEffectPass } from './canvasEffectCompositing';
33
37
  import { compositeCanvasSourceMode } from './canvasSourceModeCompositing';
34
38
 
@@ -72,17 +76,20 @@ describe('defaultCanvasDropShadowEffectRunner', () => {
72
76
  });
73
77
  });
74
78
 
75
- beforeEach(() => {
76
- vi.clearAllMocks();
79
+ describe('registerCanvasDropShadowEffect', () => {
80
+ it('is a function', () => expect(registerCanvasDropShadowEffect).toBeTypeOf('function'));
77
81
  });
78
82
 
79
- afterAll(() => {
80
- vi.doUnmock('./canvasEffectCompositing');
81
- vi.doUnmock('./canvasRenderEffectPipeline');
82
- vi.doUnmock('./canvasSourceModeCompositing');
83
- vi.resetModules();
83
+ beforeEach(() => {
84
+ vi.clearAllMocks();
84
85
  });
85
86
 
86
87
  function createTarget(id: string, width = 32, height = 16): never {
87
88
  return { id, canvas: {}, context: {}, width, height } as never;
88
89
  }
90
+
91
+ describe('registerCanvasDropShadowEffect', () => {
92
+ it('is a function', () => {
93
+ expect(typeof registerCanvasDropShadowEffect).toBe('function');
94
+ });
95
+ });
@@ -0,0 +1,27 @@
1
+ import { createCanvasRenderState } from '@flighthq/scene2d-canvas/contract';
2
+
3
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
4
+ import * as contractEffects from './contract';
5
+ import * as publicEffects from './index';
6
+
7
+ describe('Canvas effect registration', () => {
8
+ it.each([
9
+ ['BlendEffect', 'registerCanvasBlendEffect', 'defaultCanvasBlendEffectRunner'],
10
+ ['BloomEffect', 'registerCanvasBloomEffect', 'defaultCanvasBloomEffectRunner'],
11
+ ['BlurEffect', 'registerCanvasBlurEffect', 'defaultCanvasBlurEffectRunner'],
12
+ ['DropShadowEffect', 'registerCanvasDropShadowEffect', 'defaultCanvasDropShadowEffectRunner'],
13
+ ['FilmGrainEffect', 'registerCanvasFilmGrainEffect', 'defaultCanvasFilmGrainEffectRunner'],
14
+ ['OuterGlowEffect', 'registerCanvasOuterGlowEffect', 'defaultCanvasOuterGlowEffectRunner'],
15
+ ['PixelateEffect', 'registerCanvasPixelateEffect', 'defaultCanvasPixelateEffectRunner'],
16
+ ['ScanlinesEffect', 'registerCanvasScanlinesEffect', 'defaultCanvasScanlinesEffectRunner'],
17
+ ['VignetteEffect', 'registerCanvasVignetteEffect', 'defaultCanvasVignetteEffectRunner'],
18
+ ] as const)('registers the public %s runner on only the supplied state', (kind, registerName, runnerName) => {
19
+ const state = createCanvasRenderState(document.createElement('canvas'));
20
+ const other = createCanvasRenderState(document.createElement('canvas'));
21
+
22
+ publicEffects[registerName](state);
23
+
24
+ expect(getCanvasRenderEffectRunner(state, kind)).toBe(contractEffects[runnerName]);
25
+ expect(getCanvasRenderEffectRunner(other, kind)).toBeNull();
26
+ });
27
+ });
@@ -1,4 +1,8 @@
1
- import { applyFilmGrainEffectToCanvas, defaultCanvasFilmGrainEffectRunner } from './canvasFilmGrainEffect';
1
+ import {
2
+ applyFilmGrainEffectToCanvas,
3
+ defaultCanvasFilmGrainEffectRunner,
4
+ registerCanvasFilmGrainEffect,
5
+ } from './canvasFilmGrainEffect';
2
6
 
3
7
  describe('applyFilmGrainEffectToCanvas', () => {
4
8
  it('is a function', () => {
@@ -11,3 +15,7 @@ describe('defaultCanvasFilmGrainEffectRunner', () => {
11
15
  expect(typeof defaultCanvasFilmGrainEffectRunner).toBe('function');
12
16
  });
13
17
  });
18
+
19
+ describe('registerCanvasFilmGrainEffect', () => {
20
+ it('is a function', () => expect(registerCanvasFilmGrainEffect).toBeTypeOf('function'));
21
+ });