@flighthq/surface 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 (210) hide show
  1. package/dist/index.d.ts +43 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +42 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/surface.d.ts +16 -0
  6. package/dist/surface.d.ts.map +1 -0
  7. package/dist/surface.js +84 -0
  8. package/dist/surface.js.map +1 -0
  9. package/dist/surfaceAffine.d.ts +27 -0
  10. package/dist/surfaceAffine.d.ts.map +1 -0
  11. package/dist/surfaceAffine.js +159 -0
  12. package/dist/surfaceAffine.js.map +1 -0
  13. package/dist/surfaceAlpha.d.ts +27 -0
  14. package/dist/surfaceAlpha.d.ts.map +1 -0
  15. package/dist/surfaceAlpha.js +83 -0
  16. package/dist/surfaceAlpha.js.map +1 -0
  17. package/dist/surfaceBevel.d.ts +42 -0
  18. package/dist/surfaceBevel.d.ts.map +1 -0
  19. package/dist/surfaceBevel.js +102 -0
  20. package/dist/surfaceBevel.js.map +1 -0
  21. package/dist/surfaceBlur.d.ts +62 -0
  22. package/dist/surfaceBlur.d.ts.map +1 -0
  23. package/dist/surfaceBlur.js +263 -0
  24. package/dist/surfaceBlur.js.map +1 -0
  25. package/dist/surfaceChannel.d.ts +24 -0
  26. package/dist/surfaceChannel.d.ts.map +1 -0
  27. package/dist/surfaceChannel.js +126 -0
  28. package/dist/surfaceChannel.js.map +1 -0
  29. package/dist/surfaceColorMatrix.d.ts +57 -0
  30. package/dist/surfaceColorMatrix.d.ts.map +1 -0
  31. package/dist/surfaceColorMatrix.js +127 -0
  32. package/dist/surfaceColorMatrix.js.map +1 -0
  33. package/dist/surfaceCompare.d.ts +18 -0
  34. package/dist/surfaceCompare.d.ts.map +1 -0
  35. package/dist/surfaceCompare.js +64 -0
  36. package/dist/surfaceCompare.js.map +1 -0
  37. package/dist/surfaceComposite.d.ts +53 -0
  38. package/dist/surfaceComposite.d.ts.map +1 -0
  39. package/dist/surfaceComposite.js +230 -0
  40. package/dist/surfaceComposite.js.map +1 -0
  41. package/dist/surfaceConvolution.d.ts +27 -0
  42. package/dist/surfaceConvolution.d.ts.map +1 -0
  43. package/dist/surfaceConvolution.js +103 -0
  44. package/dist/surfaceConvolution.js.map +1 -0
  45. package/dist/surfaceCopy.d.ts +23 -0
  46. package/dist/surfaceCopy.d.ts.map +1 -0
  47. package/dist/surfaceCopy.js +78 -0
  48. package/dist/surfaceCopy.js.map +1 -0
  49. package/dist/surfaceCoverage.d.ts +11 -0
  50. package/dist/surfaceCoverage.d.ts.map +1 -0
  51. package/dist/surfaceCoverage.js +29 -0
  52. package/dist/surfaceCoverage.js.map +1 -0
  53. package/dist/surfaceCrop.d.ts +33 -0
  54. package/dist/surfaceCrop.d.ts.map +1 -0
  55. package/dist/surfaceCrop.js +174 -0
  56. package/dist/surfaceCrop.js.map +1 -0
  57. package/dist/surfaceDisplacement.d.ts +48 -0
  58. package/dist/surfaceDisplacement.d.ts.map +1 -0
  59. package/dist/surfaceDisplacement.js +131 -0
  60. package/dist/surfaceDisplacement.js.map +1 -0
  61. package/dist/surfaceDissolve.d.ts +21 -0
  62. package/dist/surfaceDissolve.d.ts.map +1 -0
  63. package/dist/surfaceDissolve.js +97 -0
  64. package/dist/surfaceDissolve.js.map +1 -0
  65. package/dist/surfaceDraw.d.ts +8 -0
  66. package/dist/surfaceDraw.d.ts.map +1 -0
  67. package/dist/surfaceDraw.js +14 -0
  68. package/dist/surfaceDraw.js.map +1 -0
  69. package/dist/surfaceEncode.d.ts +3 -0
  70. package/dist/surfaceEncode.d.ts.map +1 -0
  71. package/dist/surfaceEncode.js +18 -0
  72. package/dist/surfaceEncode.js.map +1 -0
  73. package/dist/surfaceFill.d.ts +12 -0
  74. package/dist/surfaceFill.d.ts.map +1 -0
  75. package/dist/surfaceFill.js +85 -0
  76. package/dist/surfaceFill.js.map +1 -0
  77. package/dist/surfaceFingerprint.d.ts +26 -0
  78. package/dist/surfaceFingerprint.d.ts.map +1 -0
  79. package/dist/surfaceFingerprint.js +99 -0
  80. package/dist/surfaceFingerprint.js.map +1 -0
  81. package/dist/surfaceFlip.d.ts +16 -0
  82. package/dist/surfaceFlip.d.ts.map +1 -0
  83. package/dist/surfaceFlip.js +98 -0
  84. package/dist/surfaceFlip.js.map +1 -0
  85. package/dist/surfaceFormat.d.ts +26 -0
  86. package/dist/surfaceFormat.d.ts.map +1 -0
  87. package/dist/surfaceFormat.js +80 -0
  88. package/dist/surfaceFormat.js.map +1 -0
  89. package/dist/surfaceFrom.d.ts +14 -0
  90. package/dist/surfaceFrom.d.ts.map +1 -0
  91. package/dist/surfaceFrom.js +87 -0
  92. package/dist/surfaceFrom.js.map +1 -0
  93. package/dist/surfaceGradient.d.ts +57 -0
  94. package/dist/surfaceGradient.d.ts.map +1 -0
  95. package/dist/surfaceGradient.js +176 -0
  96. package/dist/surfaceGradient.js.map +1 -0
  97. package/dist/surfaceGradientFill.d.ts +32 -0
  98. package/dist/surfaceGradientFill.d.ts.map +1 -0
  99. package/dist/surfaceGradientFill.js +120 -0
  100. package/dist/surfaceGradientFill.js.map +1 -0
  101. package/dist/surfaceHistogram.d.ts +20 -0
  102. package/dist/surfaceHistogram.d.ts.map +1 -0
  103. package/dist/surfaceHistogram.js +61 -0
  104. package/dist/surfaceHistogram.js.map +1 -0
  105. package/dist/surfaceImageChannel.d.ts +2 -0
  106. package/dist/surfaceImageChannel.d.ts.map +1 -0
  107. package/dist/surfaceImageChannel.js +2 -0
  108. package/dist/surfaceImageChannel.js.map +1 -0
  109. package/dist/surfaceMedian.d.ts +14 -0
  110. package/dist/surfaceMedian.d.ts.map +1 -0
  111. package/dist/surfaceMedian.js +77 -0
  112. package/dist/surfaceMedian.js.map +1 -0
  113. package/dist/surfaceMorphological.d.ts +29 -0
  114. package/dist/surfaceMorphological.d.ts.map +1 -0
  115. package/dist/surfaceMorphological.js +81 -0
  116. package/dist/surfaceMorphological.js.map +1 -0
  117. package/dist/surfaceNoise.d.ts +48 -0
  118. package/dist/surfaceNoise.d.ts.map +1 -0
  119. package/dist/surfaceNoise.js +245 -0
  120. package/dist/surfaceNoise.js.map +1 -0
  121. package/dist/surfacePaletteMap.d.ts +17 -0
  122. package/dist/surfacePaletteMap.d.ts.map +1 -0
  123. package/dist/surfacePaletteMap.js +45 -0
  124. package/dist/surfacePaletteMap.js.map +1 -0
  125. package/dist/surfacePixel.d.ts +9 -0
  126. package/dist/surfacePixel.d.ts.map +1 -0
  127. package/dist/surfacePixel.js +36 -0
  128. package/dist/surfacePixel.js.map +1 -0
  129. package/dist/surfacePixelate.d.ts +13 -0
  130. package/dist/surfacePixelate.d.ts.map +1 -0
  131. package/dist/surfacePixelate.js +61 -0
  132. package/dist/surfacePixelate.js.map +1 -0
  133. package/dist/surfaceQuery.d.ts +13 -0
  134. package/dist/surfaceQuery.d.ts.map +1 -0
  135. package/dist/surfaceQuery.js +46 -0
  136. package/dist/surfaceQuery.js.map +1 -0
  137. package/dist/surfaceRegion.d.ts +20 -0
  138. package/dist/surfaceRegion.d.ts.map +1 -0
  139. package/dist/surfaceRegion.js +28 -0
  140. package/dist/surfaceRegion.js.map +1 -0
  141. package/dist/surfaceResize.d.ts +27 -0
  142. package/dist/surfaceResize.d.ts.map +1 -0
  143. package/dist/surfaceResize.js +191 -0
  144. package/dist/surfaceResize.js.map +1 -0
  145. package/dist/surfaceRotate.d.ts +32 -0
  146. package/dist/surfaceRotate.d.ts.map +1 -0
  147. package/dist/surfaceRotate.js +143 -0
  148. package/dist/surfaceRotate.js.map +1 -0
  149. package/dist/surfaceShadow.d.ts +107 -0
  150. package/dist/surfaceShadow.d.ts.map +1 -0
  151. package/dist/surfaceShadow.js +173 -0
  152. package/dist/surfaceShadow.js.map +1 -0
  153. package/dist/surfaceSharpen.d.ts +25 -0
  154. package/dist/surfaceSharpen.d.ts.map +1 -0
  155. package/dist/surfaceSharpen.js +47 -0
  156. package/dist/surfaceSharpen.js.map +1 -0
  157. package/dist/surfaceTone.d.ts +28 -0
  158. package/dist/surfaceTone.d.ts.map +1 -0
  159. package/dist/surfaceTone.js +69 -0
  160. package/dist/surfaceTone.js.map +1 -0
  161. package/dist/surfaceTransform.d.ts +40 -0
  162. package/dist/surfaceTransform.d.ts.map +1 -0
  163. package/dist/surfaceTransform.js +163 -0
  164. package/dist/surfaceTransform.js.map +1 -0
  165. package/dist/surfaceWarp.d.ts +48 -0
  166. package/dist/surfaceWarp.d.ts.map +1 -0
  167. package/dist/surfaceWarp.js +284 -0
  168. package/dist/surfaceWarp.js.map +1 -0
  169. package/package.json +39 -0
  170. package/src/surface.test.ts +78 -0
  171. package/src/surfaceAffine.test.ts +98 -0
  172. package/src/surfaceAlpha.test.ts +113 -0
  173. package/src/surfaceBevel.test.ts +67 -0
  174. package/src/surfaceBlur.test.ts +240 -0
  175. package/src/surfaceChannel.test.ts +97 -0
  176. package/src/surfaceColorMatrix.test.ts +220 -0
  177. package/src/surfaceCompare.test.ts +92 -0
  178. package/src/surfaceComposite.test.ts +256 -0
  179. package/src/surfaceConvolution.test.ts +136 -0
  180. package/src/surfaceCopy.test.ts +54 -0
  181. package/src/surfaceCoverage.test.ts +39 -0
  182. package/src/surfaceCrop.test.ts +116 -0
  183. package/src/surfaceDisplacement.test.ts +142 -0
  184. package/src/surfaceDissolve.test.ts +99 -0
  185. package/src/surfaceDraw.test.ts +38 -0
  186. package/src/surfaceEncode.test.ts +18 -0
  187. package/src/surfaceFill.test.ts +89 -0
  188. package/src/surfaceFingerprint.test.ts +103 -0
  189. package/src/surfaceFlip.test.ts +59 -0
  190. package/src/surfaceFormat.test.ts +124 -0
  191. package/src/surfaceFrom.test.ts +62 -0
  192. package/src/surfaceGradient.test.ts +91 -0
  193. package/src/surfaceGradientFill.test.ts +107 -0
  194. package/src/surfaceHistogram.test.ts +70 -0
  195. package/src/surfaceImageChannel.test.ts +19 -0
  196. package/src/surfaceMedian.test.ts +45 -0
  197. package/src/surfaceMorphological.test.ts +75 -0
  198. package/src/surfaceNoise.test.ts +169 -0
  199. package/src/surfacePaletteMap.test.ts +55 -0
  200. package/src/surfacePixel.test.ts +99 -0
  201. package/src/surfacePixelate.test.ts +46 -0
  202. package/src/surfaceQuery.test.ts +48 -0
  203. package/src/surfaceRegion.test.ts +34 -0
  204. package/src/surfaceResize.test.ts +112 -0
  205. package/src/surfaceRotate.test.ts +119 -0
  206. package/src/surfaceShadow.test.ts +165 -0
  207. package/src/surfaceSharpen.test.ts +50 -0
  208. package/src/surfaceTone.test.ts +104 -0
  209. package/src/surfaceTransform.test.ts +134 -0
  210. package/src/surfaceWarp.test.ts +124 -0
@@ -0,0 +1,113 @@
1
+ import { createSurface } from './surface';
2
+ import { copySurfaceAlpha, multiplySurfaceAlpha, setSurfaceAlpha } from './surfaceAlpha';
3
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
4
+ import { createSurfaceRegion } from './surfaceRegion';
5
+
6
+ describe('copySurfaceAlpha', () => {
7
+ it('copies alpha from source to dest, leaving RGB unchanged', () => {
8
+ const src = createSurface(2, 1);
9
+ setSurfacePixel(src, 0, 0, 0x00000080); // alpha 0x80
10
+ setSurfacePixel(src, 1, 0, 0x000000ff); // alpha 0xff
11
+ const dst = createSurface(2, 1, 0xff00ffff); // red pixels, fully opaque
12
+ copySurfaceAlpha(createSurfaceRegion(dst), createSurfaceRegion(src));
13
+ // RGB unchanged, alpha from source.
14
+ expect((getSurfacePixel(dst, 0, 0) >>> 24) & 0xff).toBe(0xff); // red channel
15
+ expect(getSurfacePixel(dst, 0, 0) & 0xff).toBe(0x80); // alpha
16
+ expect(getSurfacePixel(dst, 1, 0) & 0xff).toBe(0xff);
17
+ });
18
+
19
+ it('same-surface region is a no-op on alpha', () => {
20
+ const surf = createSurface(2, 1, 0x112233aa);
21
+ copySurfaceAlpha(createSurfaceRegion(surf), createSurfaceRegion(surf));
22
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xaa);
23
+ });
24
+
25
+ it('operates only on the overlap of both regions', () => {
26
+ const src = createSurface(3, 1, 0x00000080);
27
+ const dst = createSurface(3, 1, 0x00000000);
28
+ // Copy only 1 pixel wide.
29
+ copySurfaceAlpha(
30
+ { surface: dst, x: 0, y: 0, width: 1, height: 1 },
31
+ { surface: src, x: 0, y: 0, width: 1, height: 1 },
32
+ );
33
+ expect(getSurfacePixel(dst, 0, 0) & 0xff).toBe(0x80);
34
+ expect(getSurfacePixel(dst, 1, 0) & 0xff).toBe(0x00); // untouched
35
+ });
36
+ });
37
+
38
+ describe('multiplySurfaceAlpha', () => {
39
+ it('factor 0 makes region fully transparent', () => {
40
+ const surf = createSurface(2, 2, 0xff0000ff);
41
+ multiplySurfaceAlpha(createSurfaceRegion(surf), 0);
42
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0);
43
+ expect(getSurfacePixel(surf, 1, 1) & 0xff).toBe(0);
44
+ });
45
+
46
+ it('factor 1 leaves alpha unchanged', () => {
47
+ const surf = createSurface(1, 1, 0xff0000ab);
48
+ multiplySurfaceAlpha(createSurfaceRegion(surf), 1);
49
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xab);
50
+ });
51
+
52
+ it('factor 0.5 halves the alpha', () => {
53
+ const surf = createSurface(1, 1, 0xff0000fe);
54
+ multiplySurfaceAlpha(createSurfaceRegion(surf), 0.5);
55
+ // 0xfe * 0.5 = 127
56
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(127);
57
+ });
58
+
59
+ it('clamps factor to [0, 1]', () => {
60
+ const surf = createSurface(1, 1, 0xff0000aa);
61
+ multiplySurfaceAlpha(createSurfaceRegion(surf), 2);
62
+ // Factor clamped to 1, alpha unchanged.
63
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xaa);
64
+ });
65
+
66
+ it('leaves RGB channels unchanged', () => {
67
+ const surf = createSurface(1, 1, 0x123456ff);
68
+ multiplySurfaceAlpha(createSurfaceRegion(surf), 0.5);
69
+ const p = getSurfacePixel(surf, 0, 0);
70
+ expect((p >>> 24) & 0xff).toBe(0x12);
71
+ expect((p >> 16) & 0xff).toBe(0x34);
72
+ expect((p >> 8) & 0xff).toBe(0x56);
73
+ });
74
+ });
75
+
76
+ describe('setSurfaceAlpha', () => {
77
+ it('writes a constant alpha to all pixels in region', () => {
78
+ const surf = createSurface(3, 3, 0xff0000ff);
79
+ setSurfaceAlpha(createSurfaceRegion(surf), 0x40);
80
+ for (let y = 0; y < 3; y++) {
81
+ for (let x = 0; x < 3; x++) {
82
+ expect(getSurfacePixel(surf, x, y) & 0xff).toBe(0x40);
83
+ }
84
+ }
85
+ });
86
+
87
+ it('leaves RGB unchanged', () => {
88
+ const surf = createSurface(1, 1, 0x112233ff);
89
+ setSurfaceAlpha(createSurfaceRegion(surf), 0x80);
90
+ const p = getSurfacePixel(surf, 0, 0);
91
+ expect((p >>> 24) & 0xff).toBe(0x11);
92
+ expect((p >> 16) & 0xff).toBe(0x22);
93
+ expect((p >> 8) & 0xff).toBe(0x33);
94
+ expect(p & 0xff).toBe(0x80);
95
+ });
96
+
97
+ it('clamps alpha to [0, 255]', () => {
98
+ const surf = createSurface(1, 1, 0xff0000ff);
99
+ setSurfaceAlpha(createSurfaceRegion(surf), -10);
100
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0);
101
+ setSurfaceAlpha(createSurfaceRegion(surf), 300);
102
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(255);
103
+ });
104
+
105
+ it('only affects the specified sub-region', () => {
106
+ const surf = createSurface(4, 1, 0xff0000ff);
107
+ setSurfaceAlpha({ surface: surf, x: 1, y: 0, width: 2, height: 1 }, 0x00);
108
+ expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xff); // untouched
109
+ expect(getSurfacePixel(surf, 1, 0) & 0xff).toBe(0x00);
110
+ expect(getSurfacePixel(surf, 2, 0) & 0xff).toBe(0x00);
111
+ expect(getSurfacePixel(surf, 3, 0) & 0xff).toBe(0xff); // untouched
112
+ });
113
+ });
@@ -0,0 +1,67 @@
1
+ import { createSurface } from './surface';
2
+ import { bevelSurface } from './surfaceBevel';
3
+
4
+ function region(
5
+ surface: ReturnType<typeof createSurface>,
6
+ x = 0,
7
+ y = 0,
8
+ width = surface.width,
9
+ height = surface.height,
10
+ ) {
11
+ return { surface, x, y, width, height };
12
+ }
13
+
14
+ // 5x1 strip with a single alpha edge: transparent | transparent | opaque | opaque | opaque.
15
+ function edgeStrip() {
16
+ const s = createSurface(5, 1);
17
+ for (let x = 2; x < 5; x++) s.data[x * 4 + 3] = 255;
18
+ return s;
19
+ }
20
+
21
+ describe('bevelSurface', () => {
22
+ it('highlights the light-facing edge and shades the opposite edge', () => {
23
+ // Light from the left (angle π). With radiusX=1 the blurred alpha is
24
+ // [0, 85, 170, 255, 255]; the gradient highlights the left edge of the
25
+ // shape and shadows the right edge.
26
+ const source = edgeStrip();
27
+ const out = new Uint8ClampedArray(5 * 4);
28
+ const scratch = new Uint8ClampedArray(5 * 4);
29
+ bevelSurface(out, scratch, region(source), {
30
+ angle: Math.PI,
31
+ distance: 1,
32
+ radiusX: 1,
33
+ radiusY: 0,
34
+ type: 'both',
35
+ highlightColor: 0xffffffff,
36
+ shadowColor: 0x000000ff,
37
+ });
38
+ // Left edge: white highlight.
39
+ expect(out[2 * 4 + 0]).toBe(255);
40
+ expect(out[2 * 4 + 1]).toBe(255);
41
+ expect(out[2 * 4 + 2]).toBe(255);
42
+ expect(out[2 * 4 + 3]).toBe(170);
43
+ // Right edge of the strip faces away from the light: black shadow.
44
+ expect(out[4 * 4 + 0]).toBe(0);
45
+ expect(out[4 * 4 + 1]).toBe(0);
46
+ expect(out[4 * 4 + 2]).toBe(0);
47
+ expect(out[4 * 4 + 3]).toBe(255);
48
+ });
49
+
50
+ it("'inner' type clips the bevel to inside the shape", () => {
51
+ const source = edgeStrip();
52
+ const out = new Uint8ClampedArray(5 * 4);
53
+ const scratch = new Uint8ClampedArray(5 * 4);
54
+ bevelSurface(out, scratch, region(source), {
55
+ angle: Math.PI,
56
+ distance: 1,
57
+ radiusX: 1,
58
+ radiusY: 0,
59
+ type: 'inner',
60
+ });
61
+ // x0/x1 are outside the shape (source alpha 0) → clipped to 0.
62
+ expect(out[0 * 4 + 3]).toBe(0);
63
+ expect(out[1 * 4 + 3]).toBe(0);
64
+ // x2 is inside the shape → highlight survives.
65
+ expect(out[2 * 4 + 3]).toBe(170);
66
+ });
67
+ });
@@ -0,0 +1,240 @@
1
+ import { createSurface } from './surface';
2
+ import {
3
+ blurSurfacePixelsHorizontal,
4
+ blurSurfacePixelsHorizontalWeighted,
5
+ blurSurfacePixelsVertical,
6
+ blurSurfacePixelsVerticalWeighted,
7
+ boxBlurSurface,
8
+ computeGaussianKernel,
9
+ gaussianBlurSurface,
10
+ } from './surfaceBlur';
11
+ import { premultiplySurfacePixels, unpremultiplySurfacePixels } from './surfaceFormat';
12
+
13
+ function region(
14
+ surface: ReturnType<typeof createSurface>,
15
+ x = 0,
16
+ y = 0,
17
+ width = surface.width,
18
+ height = surface.height,
19
+ ) {
20
+ return { surface, x, y, width, height };
21
+ }
22
+
23
+ describe('blurSurfacePixelsHorizontal', () => {
24
+ it('spreads alpha into horizontal neighbors', () => {
25
+ const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
26
+ const out = new Uint8ClampedArray(12);
27
+ blurSurfacePixelsHorizontal(out, source, 3, 1, 1);
28
+ expect(out[3]).toBeGreaterThan(0);
29
+ expect(out[11]).toBeGreaterThan(0);
30
+ });
31
+
32
+ it('copies source to out without spreading when radius is 0', () => {
33
+ const source = new Uint8ClampedArray([0, 0, 0, 255, 0, 0, 0, 0]);
34
+ const out = new Uint8ClampedArray(8);
35
+ blurSurfacePixelsHorizontal(out, source, 2, 1, 0);
36
+ expect(out[3]).toBe(255);
37
+ expect(out[7]).toBe(0);
38
+ });
39
+
40
+ it('premultiplying before blur avoids the dark-halo artifact at transparent edges', () => {
41
+ // [opaque red, transparent]. Straight-alpha blur bleeds the transparent
42
+ // pixel's hidden black into the red; a premultiplied blur keeps it pure red.
43
+ const buf = new Uint8ClampedArray([255, 0, 0, 255, 0, 0, 0, 0]);
44
+
45
+ const straight = new Uint8ClampedArray(8);
46
+ blurSurfacePixelsHorizontal(straight, buf, 2, 1, 1);
47
+ expect(straight[4]).toBe(128); // transparent pixel's red darkened toward black
48
+
49
+ const premul = new Uint8ClampedArray(8);
50
+ premultiplySurfacePixels(premul, buf, 8);
51
+ const blurred = new Uint8ClampedArray(8);
52
+ blurSurfacePixelsHorizontal(blurred, premul, 2, 1, 1);
53
+ const result = new Uint8ClampedArray(8);
54
+ unpremultiplySurfacePixels(result, blurred, 8);
55
+ expect(result[4]).toBe(255); // red stays pure
56
+ expect(result[7]).toBe(128); // alpha still half
57
+ });
58
+
59
+ it('produces correct edge average for a single row', () => {
60
+ // pixels: [0, 255, 0] alpha only; radius=1
61
+ // x=0: avg([0,255])/2 = 128 (approx); x=1: avg([0,255,0])/3 = 85; x=2: avg([255,0])/2 = 128
62
+ const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
63
+ const out = new Uint8ClampedArray(12);
64
+ blurSurfacePixelsHorizontal(out, source, 3, 1, 1);
65
+ expect(out[3]).toBe(128); // (0+255)/2
66
+ expect(out[7]).toBe(85); // (0+255+0)/3
67
+ expect(out[11]).toBe(128); // (255+0)/2
68
+ });
69
+ });
70
+
71
+ describe('blurSurfacePixelsHorizontalWeighted', () => {
72
+ it('applies Gaussian weights to horizontal neighbors', () => {
73
+ const kernel = new Float32Array(3);
74
+ computeGaussianKernel(kernel, 1, 0.8);
75
+ const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
76
+ const out = new Uint8ClampedArray(12);
77
+ blurSurfacePixelsHorizontalWeighted(out, source, 3, 1, kernel);
78
+ // center weight >> edge; center alpha should be highest
79
+ expect(out[7]).toBeGreaterThan(out[3]);
80
+ expect(out[7]).toBeGreaterThan(out[11]);
81
+ });
82
+
83
+ it('with an identity kernel [1] leaves source unchanged', () => {
84
+ const kernel = new Float32Array([1]);
85
+ const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
86
+ const out = new Uint8ClampedArray(4);
87
+ blurSurfacePixelsHorizontalWeighted(out, source, 1, 1, kernel);
88
+ expect(out[0]).toBe(0x11);
89
+ expect(out[3]).toBe(0xff);
90
+ });
91
+ });
92
+
93
+ describe('blurSurfacePixelsVertical', () => {
94
+ it('spreads alpha into vertical neighbors', () => {
95
+ const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
96
+ const out = new Uint8ClampedArray(12);
97
+ blurSurfacePixelsVertical(out, source, 1, 3, 1);
98
+ expect(out[3]).toBeGreaterThan(0);
99
+ expect(out[11]).toBeGreaterThan(0);
100
+ });
101
+ });
102
+
103
+ describe('blurSurfacePixelsVerticalWeighted', () => {
104
+ it('applies Gaussian weights to vertical neighbors', () => {
105
+ const kernel = new Float32Array(3);
106
+ computeGaussianKernel(kernel, 1, 0.8);
107
+ const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
108
+ const out = new Uint8ClampedArray(12);
109
+ blurSurfacePixelsVerticalWeighted(out, source, 1, 3, kernel);
110
+ expect(out[7]).toBeGreaterThan(out[3]);
111
+ });
112
+ });
113
+
114
+ describe('boxBlurSurface', () => {
115
+ it('spreads alpha into neighboring pixels', () => {
116
+ const source = createSurface(3, 1);
117
+ source.data[7] = 255; // center pixel opaque
118
+ const out = new Uint8ClampedArray(3 * 4);
119
+ const scratch = new Uint8ClampedArray(3 * 4);
120
+ boxBlurSurface(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
121
+ expect(out[3]).toBeGreaterThan(0);
122
+ expect(out[11]).toBeGreaterThan(0);
123
+ });
124
+
125
+ it('writes source into out when blur is zero', () => {
126
+ const source = createSurface(1, 1, 0x336699ff);
127
+ const out = new Uint8ClampedArray(4);
128
+ const scratch = new Uint8ClampedArray(4);
129
+ boxBlurSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0 });
130
+ expect(out[0]).toBe(0x33);
131
+ expect(out[3]).toBe(0xff);
132
+ });
133
+
134
+ it('result always lands in out regardless of pass parity', () => {
135
+ const source = createSurface(3, 3, 0xffffff88);
136
+ const out = new Uint8ClampedArray(3 * 3 * 4);
137
+ const scratch = new Uint8ClampedArray(3 * 3 * 4);
138
+ boxBlurSurface(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
139
+ expect(out[3]).toBeGreaterThan(0);
140
+ });
141
+
142
+ it('source.surface.data can be used as out for a full-surface region', () => {
143
+ const surface = createSurface(3, 1);
144
+ surface.data[7] = 255;
145
+ const scratch = new Uint8ClampedArray(3 * 4);
146
+ boxBlurSurface(surface.data, scratch, region(surface), { radiusX: 2, radiusY: 0 });
147
+ expect(surface.data[3]).toBeGreaterThan(0);
148
+ expect(surface.data[11]).toBeGreaterThan(0);
149
+ });
150
+
151
+ it('blurs only the offset sub-region, reading from the right source pixels', () => {
152
+ // 4 px alpha: [_, 255, 100, _]. Blurring region (1,0,2,1) extracts [255,100]
153
+ // and averages them — proving the source x-offset is honored, not ignored.
154
+ const surface = createSurface(4, 1);
155
+ surface.data[1 * 4 + 3] = 255;
156
+ surface.data[2 * 4 + 3] = 100;
157
+ const out = new Uint8ClampedArray(2 * 4);
158
+ const scratch = new Uint8ClampedArray(2 * 4);
159
+ boxBlurSurface(out, scratch, region(surface, 1, 0, 2, 1), { radiusX: 2, radiusY: 0 });
160
+ expect(out[3]).toBe(178); // round((255 + 100) / 2)
161
+ expect(out[7]).toBe(178);
162
+ });
163
+ });
164
+
165
+ describe('computeGaussianKernel', () => {
166
+ it('produces a kernel that sums to 1', () => {
167
+ const kernel = new Float32Array(7);
168
+ computeGaussianKernel(kernel, 3, 1.0);
169
+ const sum = kernel.reduce((acc, v) => acc + v, 0);
170
+ expect(sum).toBeCloseTo(1, 5);
171
+ });
172
+
173
+ it('is symmetric', () => {
174
+ const kernel = new Float32Array(9);
175
+ computeGaussianKernel(kernel, 4, 1.5);
176
+ for (let i = 0; i < 4; i++) {
177
+ expect(kernel[i]).toBeCloseTo(kernel[8 - i], 10);
178
+ }
179
+ });
180
+
181
+ it('center weight is the largest', () => {
182
+ const kernel = new Float32Array(5);
183
+ computeGaussianKernel(kernel, 2, 1.0);
184
+ expect(kernel[2]).toBeGreaterThan(kernel[1]);
185
+ expect(kernel[1]).toBeGreaterThan(kernel[0]);
186
+ });
187
+
188
+ it('radius-0 kernel produces a single weight of 1', () => {
189
+ const kernel = new Float32Array(1);
190
+ computeGaussianKernel(kernel, 0, 1.0);
191
+ expect(kernel[0]).toBeCloseTo(1, 10);
192
+ });
193
+
194
+ it('sigma <= 0 produces a unit impulse rather than a NaN kernel', () => {
195
+ const kernel = new Float32Array(5).fill(9);
196
+ computeGaussianKernel(kernel, 2, 0);
197
+ expect(kernel[2]).toBe(1);
198
+ expect(kernel[0]).toBe(0);
199
+ expect(kernel[4]).toBe(0);
200
+ expect(kernel.every((v) => !Number.isNaN(v))).toBe(true);
201
+ });
202
+ });
203
+
204
+ describe('gaussianBlurSurface', () => {
205
+ it('spreads alpha into neighboring pixels', () => {
206
+ const source = createSurface(5, 1);
207
+ source.data[2 * 4 + 3] = 255; // center pixel opaque
208
+ const out = new Uint8ClampedArray(5 * 4);
209
+ const scratch = new Uint8ClampedArray(5 * 4);
210
+ gaussianBlurSurface(out, scratch, region(source), 1.0);
211
+ expect(out[3]).toBeGreaterThan(0);
212
+ expect(out[4 * 4 + 3]).toBeGreaterThan(0);
213
+ });
214
+
215
+ it('center pixel retains more weight than edges', () => {
216
+ const source = createSurface(5, 1);
217
+ source.data[2 * 4 + 3] = 255;
218
+ const out = new Uint8ClampedArray(5 * 4);
219
+ const scratch = new Uint8ClampedArray(5 * 4);
220
+ gaussianBlurSurface(out, scratch, region(source), 0.8);
221
+ expect(out[2 * 4 + 3]).toBeGreaterThan(out[3]);
222
+ });
223
+
224
+ it('with sigma=0 writes source unchanged', () => {
225
+ const source = createSurface(1, 1, 0x336699ff);
226
+ const out = new Uint8ClampedArray(4);
227
+ const scratch = new Uint8ClampedArray(4);
228
+ gaussianBlurSurface(out, scratch, region(source), 0);
229
+ expect(out[0]).toBe(0x33);
230
+ expect(out[3]).toBe(0xff);
231
+ });
232
+
233
+ it('result always lands in out regardless of pass parity', () => {
234
+ const source = createSurface(3, 3, 0xffffff88);
235
+ const out = new Uint8ClampedArray(3 * 3 * 4);
236
+ const scratch = new Uint8ClampedArray(3 * 3 * 4);
237
+ gaussianBlurSurface(out, scratch, region(source), 1.0);
238
+ expect(out[3]).toBeGreaterThan(0);
239
+ });
240
+ });
@@ -0,0 +1,97 @@
1
+ import { createSurface } from './surface';
2
+ import { mergeSurfaceChannels, splitSurfaceChannels } from './surfaceChannel';
3
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
4
+ import { createSurfaceRegion } from './surfaceRegion';
5
+
6
+ describe('mergeSurfaceChannels', () => {
7
+ it('combines one channel from each input into the output', () => {
8
+ const rSurf = createSurface(1, 1, 0xff000000); // R=0xff, G=0, B=0, A=0
9
+ const gSurf = createSurface(1, 1, 0x00ff0000); // R=0, G=0xff, B=0, A=0
10
+ const bSurf = createSurface(1, 1, 0x0000ff00); // R=0, G=0, B=0xff, A=0
11
+ const aSurf = createSurface(1, 1, 0x000000ff); // R=0, G=0, B=0, A=0xff
12
+ const out = createSurface(1, 1);
13
+ mergeSurfaceChannels(
14
+ createSurfaceRegion(out),
15
+ createSurfaceRegion(rSurf),
16
+ createSurfaceRegion(gSurf),
17
+ createSurfaceRegion(bSurf),
18
+ createSurfaceRegion(aSurf),
19
+ );
20
+ expect(getSurfacePixel(out, 0, 0)).toBe(0xff_ff_ff_ff);
21
+ });
22
+
23
+ it('uses the minimum dimension overlap', () => {
24
+ const r = createSurface(3, 1, 0xff000000);
25
+ const g = createSurface(3, 1, 0x00ff0000);
26
+ const b = createSurface(3, 1, 0x0000ff00);
27
+ const a = createSurface(3, 1, 0x000000ff);
28
+ const out = createSurface(3, 1);
29
+ // Restrict r region to width=1 — only pixel 0 should be written.
30
+ mergeSurfaceChannels(
31
+ createSurfaceRegion(out),
32
+ { surface: r, x: 0, y: 0, width: 1, height: 1 },
33
+ createSurfaceRegion(g),
34
+ createSurfaceRegion(b),
35
+ createSurfaceRegion(a),
36
+ );
37
+ // Pixel 0 is covered; pixel 1 should be untouched (0x00000000).
38
+ expect((getSurfacePixel(out, 0, 0) >>> 24) & 0xff).toBe(0xff);
39
+ expect(getSurfacePixel(out, 1, 0)).toBe(0x00000000);
40
+ });
41
+ });
42
+
43
+ describe('splitSurfaceChannels', () => {
44
+ it('returns four surfaces of the same dimensions', () => {
45
+ const src = createSurface(3, 2, 0x112233ff);
46
+ const [r, g, b, a] = splitSurfaceChannels(src);
47
+ expect(r.width).toBe(3);
48
+ expect(r.height).toBe(2);
49
+ expect(g.width).toBe(3);
50
+ expect(b.height).toBe(2);
51
+ expect(a.width).toBe(3);
52
+ });
53
+
54
+ it('each channel surface is a grayscale copy of that channel', () => {
55
+ const src = createSurface(1, 1);
56
+ setSurfacePixel(src, 0, 0, 0x11_22_33_44);
57
+ const [r, g, b, a] = splitSurfaceChannels(src);
58
+ // R surface: R=G=B=0x11, A=0xff.
59
+ expect((getSurfacePixel(r, 0, 0) >>> 24) & 0xff).toBe(0x11);
60
+ expect((getSurfacePixel(r, 0, 0) >> 16) & 0xff).toBe(0x11);
61
+ expect((getSurfacePixel(r, 0, 0) >> 8) & 0xff).toBe(0x11);
62
+ expect(getSurfacePixel(r, 0, 0) & 0xff).toBe(0xff);
63
+ // G surface: R=G=B=0x22, A=0xff.
64
+ expect((getSurfacePixel(g, 0, 0) >>> 24) & 0xff).toBe(0x22);
65
+ // B surface: R=G=B=0x33, A=0xff.
66
+ expect((getSurfacePixel(b, 0, 0) >>> 24) & 0xff).toBe(0x33);
67
+ // A surface: R=G=B=A=0x44 (alpha stored in all channels for round-trip fidelity).
68
+ expect((getSurfacePixel(a, 0, 0) >>> 24) & 0xff).toBe(0x44);
69
+ expect(getSurfacePixel(a, 0, 0) & 0xff).toBe(0x44);
70
+ });
71
+
72
+ it('returns distinct surface objects', () => {
73
+ const src = createSurface(2, 2, 0xffffffff);
74
+ const [r, g, b, a] = splitSurfaceChannels(src);
75
+ expect(r).not.toBe(src);
76
+ expect(r).not.toBe(g);
77
+ expect(g).not.toBe(b);
78
+ expect(b).not.toBe(a);
79
+ });
80
+
81
+ it('round-trip: split then merge reconstructs the original', () => {
82
+ const src = createSurface(2, 2);
83
+ setSurfacePixel(src, 0, 0, 0xaabbccdd);
84
+ setSurfacePixel(src, 1, 0, 0x11223344);
85
+ const [r, g, b, a] = splitSurfaceChannels(src);
86
+ const out = createSurface(2, 2);
87
+ mergeSurfaceChannels(
88
+ createSurfaceRegion(out),
89
+ createSurfaceRegion(r),
90
+ createSurfaceRegion(g),
91
+ createSurfaceRegion(b),
92
+ createSurfaceRegion(a),
93
+ );
94
+ expect(getSurfacePixel(out, 0, 0)).toBe(0xaabbccdd);
95
+ expect(getSurfacePixel(out, 1, 0)).toBe(0x11223344);
96
+ });
97
+ });