@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,180 @@
1
+ import { createCanvasRenderState, createCanvasRenderTarget } from '@flighthq/scene2d-canvas/contract';
2
+ import type { CanvasRenderTarget, CanvasRenderTargetPool, GradientBevelEffect } from '@flighthq/types/contract';
3
+
4
+ import {
5
+ applyGradientBevelEffectToCanvas,
6
+ defaultCanvasGradientBevelEffectRunner,
7
+ registerCanvasGradientBevelEffect,
8
+ } from './canvasGradientBevelEffect';
9
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
10
+
11
+ // Recipe assertions rather than pixels — see canvasBlendEffect.test.ts. Scratch targets are pre-seeded so
12
+ // each pass is identifiable; the pool pops from the end, so the seed order reverses the acquire order.
13
+ function seededPool(ids: readonly string[]): { pool: CanvasRenderTargetPool; targets: CanvasRenderTarget[] } {
14
+ const targets = ids.map((id) => {
15
+ const target = createCanvasRenderTarget(4, 4);
16
+ target.canvas.id = id;
17
+ return target;
18
+ });
19
+ return { pool: { free: [...targets].reverse(), inUse: [] }, targets };
20
+ }
21
+
22
+ function recordAll(log: string[], targets: readonly Readonly<CanvasRenderTarget>[]): void {
23
+ for (const target of targets) {
24
+ const context = target.context;
25
+ const into = target.canvas.id;
26
+ vi.spyOn(context, 'drawImage').mockImplementation(((image: CanvasImageSource) => {
27
+ log.push(`${(image as HTMLCanvasElement).id || 'canvas'}->${into}|${context.globalCompositeOperation}`);
28
+ }) as typeof context.drawImage);
29
+ }
30
+ }
31
+
32
+ function scene(): { source: CanvasRenderTarget; dest: CanvasRenderTarget } {
33
+ const source = createCanvasRenderTarget(4, 4);
34
+ const dest = createCanvasRenderTarget(4, 4);
35
+ source.canvas.id = 'source';
36
+ dest.canvas.id = 'dest';
37
+ return { source, dest };
38
+ }
39
+
40
+ const RAMP = { alphas: [1, 1], colors: [0xff0000, 0x0000ff], ratios: [0, 255] };
41
+
42
+ function effectOf(over: Partial<GradientBevelEffect> = {}): GradientBevelEffect {
43
+ return { kind: 'GradientBevelEffect', ...RAMP, ...over } as GradientBevelEffect;
44
+ }
45
+
46
+ afterEach(() => {
47
+ vi.restoreAllMocks();
48
+ });
49
+ const SCRATCH = ['blurred', 'lit', 'shade', 'side', 'ramped', 'band'];
50
+
51
+ describe('applyGradientBevelEffectToCanvas', () => {
52
+ it('builds both sides by knockout and accumulates the ramped result into the band', () => {
53
+ const { source, dest } = scene();
54
+ const { pool, targets } = seededPool(SCRATCH);
55
+ const log: string[] = [];
56
+ recordAll(log, [...targets, dest]);
57
+
58
+ applyGradientBevelEffectToCanvas(source, dest, pool, effectOf());
59
+
60
+ expect(log).toContain('shade->side|destination-out');
61
+ expect(log).toContain('lit->side|destination-out');
62
+ // Two ramp lookups, one per side, each composited into the band.
63
+ expect(log.filter((e) => e === 'ramped->band|source-over')).toHaveLength(2);
64
+ });
65
+
66
+ it('offsets the two copies in opposite directions', () => {
67
+ const { source, dest } = scene();
68
+ const { pool, targets } = seededPool(SCRATCH);
69
+ const offsets: Record<string, number[]> = {};
70
+ for (const target of [...targets, dest]) {
71
+ const context = target.context;
72
+ const into = target.canvas.id;
73
+ vi.spyOn(context, 'drawImage').mockImplementation(((image: CanvasImageSource, dx?: number, dy?: number) => {
74
+ offsets[`${(image as HTMLCanvasElement).id}->${into}`] = [dx ?? 0, dy ?? 0];
75
+ }) as typeof context.drawImage);
76
+ }
77
+
78
+ applyGradientBevelEffectToCanvas(source, dest, pool, effectOf({ angle: 0, distance: 6 }));
79
+
80
+ expect(offsets['blurred->lit'][0]).toBeCloseTo(-6);
81
+ expect(offsets['blurred->shade'][0]).toBeCloseTo(6);
82
+ });
83
+
84
+ it('clips the band inside the shape by default and outside it for outer', () => {
85
+ for (const [bevelType, operation] of [
86
+ [undefined, 'source->band|destination-in'],
87
+ ['outer', 'source->band|destination-out'],
88
+ ] as const) {
89
+ const { source, dest } = scene();
90
+ const { pool, targets } = seededPool(SCRATCH);
91
+ const log: string[] = [];
92
+ recordAll(log, [...targets, dest]);
93
+
94
+ applyGradientBevelEffectToCanvas(source, dest, pool, effectOf({ bevelType }));
95
+
96
+ expect(log).toContain(operation);
97
+ vi.restoreAllMocks();
98
+ }
99
+ });
100
+
101
+ it('reads the two sides from opposite ends of one ramp', () => {
102
+ // The signed ramp IS the effect: the shadow edge takes the ramp's low end and the highlight edge its
103
+ // high end. Both sides reading the same direction would give a bevel with two identically-coloured
104
+ // rims, which is a gradient glow with extra steps. Invisible to a drawImage log, because the lookup
105
+ // writes through putImageData — so this reads the buffers it wrote.
106
+ const { source, dest } = scene();
107
+ const { pool, targets } = seededPool(SCRATCH);
108
+ const side = targets[SCRATCH.indexOf('side')];
109
+ const ramped = targets[SCRATCH.indexOf('ramped')];
110
+
111
+ // A fully covered side, so each lookup lands at its extreme rather than both at the midpoint.
112
+ const covered = side.context.createImageData(4, 4);
113
+ for (let i = 0; i < 16; i++) covered.data[i * 4 + 3] = 255;
114
+ vi.spyOn(side.context, 'getImageData').mockReturnValue(covered);
115
+ const written: number[][] = [];
116
+ vi.spyOn(ramped.context, 'putImageData').mockImplementation(((image: ImageData) => {
117
+ written.push([image.data[0], image.data[1], image.data[2], image.data[3]]);
118
+ }) as typeof ramped.context.putImageData);
119
+
120
+ applyGradientBevelEffectToCanvas(source, dest, pool, effectOf());
121
+
122
+ // Red at ratio 0, blue at ratio 255: highlight reads blue, shadow reads red.
123
+ expect(written).toHaveLength(2);
124
+ expect(written[0]).toEqual([0, 0, 255, 255]);
125
+ expect(written[1]).toEqual([255, 0, 0, 255]);
126
+ });
127
+
128
+ it('draws the source before the band', () => {
129
+ const { source, dest } = scene();
130
+ const { pool, targets } = seededPool(SCRATCH);
131
+ const log: string[] = [];
132
+ recordAll(log, [...targets, dest]);
133
+
134
+ applyGradientBevelEffectToCanvas(source, dest, pool, effectOf());
135
+
136
+ expect(log.filter((e) => e.endsWith('->dest|source-over'))).toEqual([
137
+ 'source->dest|source-over',
138
+ 'band->dest|source-over',
139
+ ]);
140
+ });
141
+
142
+ it('returns every scratch target to the pool', () => {
143
+ const { source, dest } = scene();
144
+ const { pool } = seededPool(SCRATCH);
145
+
146
+ applyGradientBevelEffectToCanvas(source, dest, pool, effectOf());
147
+
148
+ expect(pool.inUse).toHaveLength(0);
149
+ expect(pool.free).toHaveLength(SCRATCH.length);
150
+ });
151
+
152
+ it('runs without a caller-supplied pool', () => {
153
+ const { source, dest } = scene();
154
+
155
+ expect(() => applyGradientBevelEffectToCanvas(source, dest, effectOf())).not.toThrow();
156
+ });
157
+ });
158
+
159
+ describe('defaultCanvasGradientBevelEffectRunner', () => {
160
+ it('applies the bevel through the pipeline context', () => {
161
+ const state = createCanvasRenderState(document.createElement('canvas'));
162
+ const { source, dest } = scene();
163
+ const { pool, targets } = seededPool(SCRATCH);
164
+ const log: string[] = [];
165
+ recordAll(log, [...targets, dest]);
166
+
167
+ defaultCanvasGradientBevelEffectRunner({ state, source, dest, pool }, effectOf());
168
+
169
+ expect(log).toContain('band->dest|source-over');
170
+ });
171
+ });
172
+
173
+ describe('registerCanvasGradientBevelEffect', () => {
174
+ it('registers the default runner under the GradientBevelEffect kind', () => {
175
+ const state = createCanvasRenderState(document.createElement('canvas'));
176
+ registerCanvasGradientBevelEffect(state);
177
+
178
+ expect(getCanvasRenderEffectRunner(state, 'GradientBevelEffect')).toBe(defaultCanvasGradientBevelEffectRunner);
179
+ });
180
+ });
@@ -0,0 +1,125 @@
1
+ import { createCanvasRenderState, createCanvasRenderTarget } from '@flighthq/scene2d-canvas/contract';
2
+ import type { CanvasRenderTarget, CanvasRenderTargetPool, GradientGlowEffect } from '@flighthq/types/contract';
3
+
4
+ import {
5
+ applyGradientGlowEffectToCanvas,
6
+ defaultCanvasGradientGlowEffectRunner,
7
+ registerCanvasGradientGlowEffect,
8
+ } from './canvasGradientGlowEffect';
9
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
10
+
11
+ // Recipe assertions rather than pixels — see canvasBlendEffect.test.ts. Scratch targets are pre-seeded so
12
+ // each pass is identifiable; the pool pops from the end, so the seed order reverses the acquire order.
13
+ function seededPool(ids: readonly string[]): { pool: CanvasRenderTargetPool; targets: CanvasRenderTarget[] } {
14
+ const targets = ids.map((id) => {
15
+ const target = createCanvasRenderTarget(4, 4);
16
+ target.canvas.id = id;
17
+ return target;
18
+ });
19
+ return { pool: { free: [...targets].reverse(), inUse: [] }, targets };
20
+ }
21
+
22
+ function recordAll(log: string[], targets: readonly Readonly<CanvasRenderTarget>[]): void {
23
+ for (const target of targets) {
24
+ const context = target.context;
25
+ const into = target.canvas.id;
26
+ vi.spyOn(context, 'drawImage').mockImplementation(((image: CanvasImageSource) => {
27
+ log.push(`${(image as HTMLCanvasElement).id || 'canvas'}->${into}|${context.globalCompositeOperation}`);
28
+ }) as typeof context.drawImage);
29
+ }
30
+ }
31
+
32
+ function scene(): { source: CanvasRenderTarget; dest: CanvasRenderTarget } {
33
+ const source = createCanvasRenderTarget(4, 4);
34
+ const dest = createCanvasRenderTarget(4, 4);
35
+ source.canvas.id = 'source';
36
+ dest.canvas.id = 'dest';
37
+ return { source, dest };
38
+ }
39
+
40
+ const RAMP = { alphas: [1, 1], colors: [0xff0000, 0x0000ff], ratios: [0, 255] };
41
+
42
+ function effectOf(over: Partial<GradientGlowEffect> = {}): GradientGlowEffect {
43
+ return { kind: 'GradientGlowEffect', ...RAMP, ...over } as GradientGlowEffect;
44
+ }
45
+
46
+ afterEach(() => {
47
+ vi.restoreAllMocks();
48
+ });
49
+ const SCRATCH = ['blurred', 'glow'];
50
+
51
+ describe('applyGradientGlowEffectToCanvas', () => {
52
+ it('blurs the silhouette, then draws the glow under the source', () => {
53
+ const { source, dest } = scene();
54
+ const { pool, targets } = seededPool(SCRATCH);
55
+ const log: string[] = [];
56
+ recordAll(log, [...targets, dest]);
57
+
58
+ applyGradientGlowEffectToCanvas(source, dest, pool, effectOf());
59
+
60
+ // Glow first, source over it: this is an OUTER effect, so the shape occludes the glow beneath it.
61
+ // The ramp lookup between them writes via putImageData and so does not appear in a drawImage log.
62
+ expect(log).toEqual(['source->blurred|source-over', 'glow->dest|source-over', 'source->dest|source-over']);
63
+ });
64
+
65
+ it('drops the source when sourceMode is hide', () => {
66
+ const { source, dest } = scene();
67
+ const { pool, targets } = seededPool(SCRATCH);
68
+ const log: string[] = [];
69
+ recordAll(log, [...targets, dest]);
70
+
71
+ applyGradientGlowEffectToCanvas(source, dest, pool, effectOf({ sourceMode: 'hide' }));
72
+
73
+ expect(log.filter((e) => e.endsWith('->dest|source-over'))).toEqual(['glow->dest|source-over']);
74
+ });
75
+
76
+ it('draws the glow once per whole unit of strength', () => {
77
+ const { source, dest } = scene();
78
+ const { pool, targets } = seededPool(SCRATCH);
79
+ const log: string[] = [];
80
+ recordAll(log, [...targets, dest]);
81
+
82
+ applyGradientGlowEffectToCanvas(source, dest, pool, effectOf({ strength: 3 }));
83
+
84
+ expect(log.filter((e) => e.startsWith('glow->dest'))).toHaveLength(3);
85
+ });
86
+
87
+ it('returns both scratch targets to the pool', () => {
88
+ const { source, dest } = scene();
89
+ const { pool } = seededPool(SCRATCH);
90
+
91
+ applyGradientGlowEffectToCanvas(source, dest, pool, effectOf());
92
+
93
+ expect(pool.inUse).toHaveLength(0);
94
+ expect(pool.free).toHaveLength(SCRATCH.length);
95
+ });
96
+
97
+ it('runs without a caller-supplied pool', () => {
98
+ const { source, dest } = scene();
99
+
100
+ expect(() => applyGradientGlowEffectToCanvas(source, dest, effectOf())).not.toThrow();
101
+ });
102
+ });
103
+
104
+ describe('defaultCanvasGradientGlowEffectRunner', () => {
105
+ it('applies the glow through the pipeline context', () => {
106
+ const state = createCanvasRenderState(document.createElement('canvas'));
107
+ const { source, dest } = scene();
108
+ const { pool, targets } = seededPool(SCRATCH);
109
+ const log: string[] = [];
110
+ recordAll(log, [...targets, dest]);
111
+
112
+ defaultCanvasGradientGlowEffectRunner({ state, source, dest, pool }, effectOf());
113
+
114
+ expect(log).toContain('glow->dest|source-over');
115
+ });
116
+ });
117
+
118
+ describe('registerCanvasGradientGlowEffect', () => {
119
+ it('registers the default runner under the GradientGlowEffect kind', () => {
120
+ const state = createCanvasRenderState(document.createElement('canvas'));
121
+ registerCanvasGradientGlowEffect(state);
122
+
123
+ expect(getCanvasRenderEffectRunner(state, 'GradientGlowEffect')).toBe(defaultCanvasGradientGlowEffectRunner);
124
+ });
125
+ });
@@ -0,0 +1,98 @@
1
+ import { createCanvasRenderTarget } from '@flighthq/scene2d-canvas/contract';
2
+
3
+ import { applyCanvasGradientRampLookup, buildCanvasGradientRamp } from './canvasGradientRamp';
4
+
5
+ function entry(ramp: Readonly<Uint8ClampedArray>, index: number): number[] {
6
+ return [ramp[index * 4], ramp[index * 4 + 1], ramp[index * 4 + 2], ramp[index * 4 + 3]];
7
+ }
8
+
9
+ describe('applyCanvasGradientRampLookup', () => {
10
+ // jsdom's 2D context does not rasterize, but getImageData/putImageData round-trip a real buffer, so a
11
+ // per-pixel pass IS verifiable here in a way a drawImage recipe is not. Seed the source buffer, run the
12
+ // pass, read the destination back.
13
+ function runLookup(alphas: readonly number[], ramp: Uint8ClampedArray, bias?: number, scale?: number): number[][] {
14
+ const source = createCanvasRenderTarget(alphas.length, 1);
15
+ const dest = createCanvasRenderTarget(alphas.length, 1);
16
+ const seeded = source.context.createImageData(alphas.length, 1);
17
+ for (let i = 0; i < alphas.length; i++) seeded.data[i * 4 + 3] = alphas[i];
18
+ vi.spyOn(source.context, 'getImageData').mockReturnValue(seeded);
19
+ let written: ImageData | null = null;
20
+ vi.spyOn(dest.context, 'putImageData').mockImplementation(((image: ImageData) => {
21
+ written = image;
22
+ }) as typeof dest.context.putImageData);
23
+
24
+ applyCanvasGradientRampLookup(dest, source, ramp, bias, scale);
25
+
26
+ const out = written as ImageData | null;
27
+ const result: number[][] = [];
28
+ for (let i = 0; i < alphas.length; i++) {
29
+ result.push([out!.data[i * 4], out!.data[i * 4 + 1], out!.data[i * 4 + 2], out!.data[i * 4 + 3]]);
30
+ }
31
+ vi.restoreAllMocks();
32
+ return result;
33
+ }
34
+
35
+ it('replaces each pixel with the ramp entry its alpha indexes', () => {
36
+ const ramp = buildCanvasGradientRamp([0x000000, 0xff0000], [0, 1], [0, 255]);
37
+
38
+ // Alpha 0 reads the ramp's first entry, alpha 255 its last — the index is the VALUE at the pixel,
39
+ // which is what makes this a lookup rather than a spatial gradient.
40
+ expect(runLookup([0, 255], ramp)).toEqual([entry(ramp, 0), entry(ramp, 255)]);
41
+ });
42
+
43
+ it('maps through bias and scale so a signed band can read one ramp from its midpoint', () => {
44
+ const ramp = buildCanvasGradientRamp([0x000000, 0xffffff], [1, 1], [0, 255]);
45
+
46
+ // The bevel's two halves: highlight runs upward from the midpoint, shadow downward. A full-strength
47
+ // pixel therefore lands at opposite ends of the same ramp depending on which side built it.
48
+ expect(runLookup([255], ramp, 0.5, 0.5)).toEqual([entry(ramp, 255)]);
49
+ expect(runLookup([255], ramp, 0.5, -0.5)).toEqual([entry(ramp, 0)]);
50
+ expect(runLookup([0], ramp, 0.5, 0.5)).toEqual([entry(ramp, 128)]);
51
+ });
52
+
53
+ it('clamps an index the bias and scale push outside the ramp', () => {
54
+ const ramp = buildCanvasGradientRamp([0x102030, 0x405060], [1, 1], [0, 255]);
55
+
56
+ expect(runLookup([255], ramp, 2, 1)).toEqual([entry(ramp, 255)]);
57
+ expect(runLookup([255], ramp, -2, 1)).toEqual([entry(ramp, 0)]);
58
+ });
59
+ });
60
+
61
+ describe('buildCanvasGradientRamp', () => {
62
+ it('interpolates linearly between stops', () => {
63
+ const ramp = buildCanvasGradientRamp([0x000000, 0xffffff], [0, 1], [0, 255]);
64
+
65
+ expect(entry(ramp, 0)).toEqual([0, 0, 0, 0]);
66
+ expect(entry(ramp, 255)).toEqual([255, 255, 255, 255]);
67
+ // Midpoint is the average of both ends, per channel and in alpha.
68
+ const mid = entry(ramp, 128);
69
+ expect(mid[0]).toBeGreaterThan(120);
70
+ expect(mid[0]).toBeLessThan(136);
71
+ expect(mid[3]).toBeGreaterThan(120);
72
+ expect(mid[3]).toBeLessThan(136);
73
+ });
74
+
75
+ it('extends the first and last stop to the ends of the ramp', () => {
76
+ // Stops covering only the middle: everything below the first ratio holds the first colour and
77
+ // everything above the last holds the last, rather than fading to transparent black.
78
+ const ramp = buildCanvasGradientRamp([0xff0000, 0x00ff00], [1, 1], [64, 192]);
79
+
80
+ expect(entry(ramp, 0)).toEqual([255, 0, 0, 255]);
81
+ expect(entry(ramp, 255)).toEqual([0, 255, 0, 255]);
82
+ });
83
+
84
+ it('returns a fully transparent ramp when there are no stops', () => {
85
+ const ramp = buildCanvasGradientRamp([], [], []);
86
+
87
+ expect(ramp).toHaveLength(256 * 4);
88
+ expect(entry(ramp, 0)).toEqual([0, 0, 0, 0]);
89
+ expect(entry(ramp, 255)).toEqual([0, 0, 0, 0]);
90
+ });
91
+
92
+ it('takes the later stop when two share a ratio rather than dividing by zero', () => {
93
+ const ramp = buildCanvasGradientRamp([0xff0000, 0x00ff00, 0x0000ff], [1, 1, 1], [0, 128, 128]);
94
+
95
+ expect(entry(ramp, 200).every((c) => Number.isFinite(c))).toBe(true);
96
+ expect(entry(ramp, 255)).toEqual([0, 0, 255, 255]);
97
+ });
98
+ });
@@ -0,0 +1,152 @@
1
+ import { createCanvasRenderState, createCanvasRenderTarget } from '@flighthq/scene2d-canvas/contract';
2
+ import type { CanvasRenderTarget, CanvasRenderTargetPool, InnerGlowEffect } from '@flighthq/types/contract';
3
+
4
+ import {
5
+ applyInnerGlowEffectToCanvas,
6
+ defaultCanvasInnerGlowEffectRunner,
7
+ registerCanvasInnerGlowEffect,
8
+ } from './canvasInnerGlowEffect';
9
+ import { getCanvasRenderEffectRunner } from './canvasRenderEffectRegistry';
10
+
11
+ // Recipe assertions rather than pixels, for the reason in canvasBlendEffect.test.ts: jsdom's 2D context
12
+ // rasterizes nothing. The scratch targets are pre-seeded into the pool so each one is identifiable by id,
13
+ // which turns "the right multi-pass recipe ran" into an assertion instead of a hope. Pool acquisition
14
+ // pops from the end, so the seed order is the reverse of the acquisition order.
15
+ function seededPool(ids: readonly string[]): { pool: CanvasRenderTargetPool; targets: CanvasRenderTarget[] } {
16
+ const targets = ids.map((id) => {
17
+ const target = createCanvasRenderTarget(4, 4);
18
+ target.canvas.id = id;
19
+ return target;
20
+ });
21
+ return { pool: { free: [...targets].reverse(), inUse: [] }, targets };
22
+ }
23
+
24
+ // One transcript across every target, so ordering between passes is visible and not just within one.
25
+ function recordAll(log: string[], targets: readonly Readonly<CanvasRenderTarget>[]): void {
26
+ for (const target of targets) {
27
+ const context = target.context;
28
+ const into = target.canvas.id;
29
+ vi.spyOn(context, 'drawImage').mockImplementation(((image: CanvasImageSource, dx?: number, dy?: number) => {
30
+ const from = (image as HTMLCanvasElement).id || 'canvas';
31
+ log.push(`${from}->${into}|${context.globalCompositeOperation}|${dx ?? 0},${dy ?? 0}`);
32
+ }) as typeof context.drawImage);
33
+ vi.spyOn(context, 'fillRect').mockImplementation((() => {
34
+ log.push(`fill->${into}|${context.globalCompositeOperation}`);
35
+ }) as typeof context.fillRect);
36
+ }
37
+ }
38
+
39
+ function scene(): { source: CanvasRenderTarget; dest: CanvasRenderTarget } {
40
+ const source = createCanvasRenderTarget(4, 4);
41
+ const dest = createCanvasRenderTarget(4, 4);
42
+ source.canvas.id = 'source';
43
+ dest.canvas.id = 'dest';
44
+ return { source, dest };
45
+ }
46
+
47
+ function innerGlow(over: Partial<InnerGlowEffect> = {}): InnerGlowEffect {
48
+ return { kind: 'InnerGlowEffect', ...over } as InnerGlowEffect;
49
+ }
50
+
51
+ afterEach(() => {
52
+ vi.restoreAllMocks();
53
+ });
54
+
55
+ describe('applyInnerGlowEffectToCanvas', () => {
56
+ it('inverts the silhouette, blurs it, clips it back to the source, and draws it over the source', () => {
57
+ const { source, dest } = scene();
58
+ const { pool, targets } = seededPool(['mask', 'glow']);
59
+ const log: string[] = [];
60
+ recordAll(log, [...targets, dest]);
61
+
62
+ applyInnerGlowEffectToCanvas(source, dest, pool, innerGlow());
63
+
64
+ expect(log).toEqual([
65
+ // Inversion: fill the whole target with the tint, then knock the silhouette out of it.
66
+ 'fill->mask|source-over',
67
+ 'source->mask|destination-out|0,0',
68
+ // Blur the inversion so it carries inward across the boundary.
69
+ 'mask->glow|source-over|0,0',
70
+ // Clip back to the shape, discarding everything that never made it inside.
71
+ 'source->glow|destination-in|0,0',
72
+ // Source first, glow over it — an inner effect sits ON the shape.
73
+ 'source->dest|source-over|0,0',
74
+ 'glow->dest|source-over|0,0',
75
+ ]);
76
+ });
77
+
78
+ it('drops the source and keeps only the glow when sourceMode is hide', () => {
79
+ const { source, dest } = scene();
80
+ const { pool, targets } = seededPool(['mask', 'glow']);
81
+ const log: string[] = [];
82
+ recordAll(log, [...targets, dest]);
83
+
84
+ applyInnerGlowEffectToCanvas(source, dest, pool, innerGlow({ sourceMode: 'hide' }));
85
+
86
+ expect(log.filter((entry) => entry.endsWith('->dest|source-over|0,0'))).toEqual(['glow->dest|source-over|0,0']);
87
+ });
88
+
89
+ it('draws the glow once per whole unit of strength', () => {
90
+ const { source, dest } = scene();
91
+ const { pool, targets } = seededPool(['mask', 'glow']);
92
+ const log: string[] = [];
93
+ recordAll(log, [...targets, dest]);
94
+
95
+ applyInnerGlowEffectToCanvas(source, dest, pool, innerGlow({ strength: 3 }));
96
+
97
+ expect(log.filter((entry) => entry.startsWith('glow->dest'))).toHaveLength(3);
98
+ });
99
+
100
+ it('still draws the glow once for a fractional strength below one', () => {
101
+ // Math.floor would give zero passes and silently drop the effect; the floor is clamped to one so a
102
+ // partial strength weakens the tint rather than removing the glow.
103
+ const { source, dest } = scene();
104
+ const { pool, targets } = seededPool(['mask', 'glow']);
105
+ const log: string[] = [];
106
+ recordAll(log, [...targets, dest]);
107
+
108
+ applyInnerGlowEffectToCanvas(source, dest, pool, innerGlow({ strength: 0.5 }));
109
+
110
+ expect(log.filter((entry) => entry.startsWith('glow->dest'))).toHaveLength(1);
111
+ });
112
+
113
+ it('returns both scratch targets to the pool', () => {
114
+ // Every acquire must be matched by a release, or the pool grows a target per frame forever.
115
+ const { source, dest } = scene();
116
+ const { pool } = seededPool(['mask', 'glow']);
117
+
118
+ applyInnerGlowEffectToCanvas(source, dest, pool, innerGlow());
119
+
120
+ expect(pool.inUse).toHaveLength(0);
121
+ expect(pool.free).toHaveLength(2);
122
+ });
123
+
124
+ it('runs without a caller-supplied pool', () => {
125
+ const { source, dest } = scene();
126
+
127
+ expect(() => applyInnerGlowEffectToCanvas(source, dest, innerGlow())).not.toThrow();
128
+ });
129
+ });
130
+
131
+ describe('defaultCanvasInnerGlowEffectRunner', () => {
132
+ it('applies the glow through the pipeline context', () => {
133
+ const state = createCanvasRenderState(document.createElement('canvas'));
134
+ const { source, dest } = scene();
135
+ const { pool, targets } = seededPool(['mask', 'glow']);
136
+ const log: string[] = [];
137
+ recordAll(log, [...targets, dest]);
138
+
139
+ defaultCanvasInnerGlowEffectRunner({ state, source, dest, pool }, innerGlow());
140
+
141
+ expect(log).toContain('source->glow|destination-in|0,0');
142
+ });
143
+ });
144
+
145
+ describe('registerCanvasInnerGlowEffect', () => {
146
+ it('registers the default runner under the InnerGlowEffect kind', () => {
147
+ const state = createCanvasRenderState(document.createElement('canvas'));
148
+ registerCanvasInnerGlowEffect(state);
149
+
150
+ expect(getCanvasRenderEffectRunner(state, 'InnerGlowEffect')).toBe(defaultCanvasInnerGlowEffectRunner);
151
+ });
152
+ });