@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,54 @@
1
+ import { createSurface } from './surface';
2
+ import { copySurfaceChannel, copySurfacePixels } from './surfaceCopy';
3
+ import { ImageChannel } from './surfaceImageChannel';
4
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
5
+
6
+ function region(
7
+ surface: ReturnType<typeof createSurface>,
8
+ x = 0,
9
+ y = 0,
10
+ width = surface.width,
11
+ height = surface.height,
12
+ ) {
13
+ return { surface, x, y, width, height };
14
+ }
15
+
16
+ describe('copySurfaceChannel', () => {
17
+ it('copies the red channel to the blue channel', () => {
18
+ const src = createSurface(2, 2);
19
+ src.data[0] = 0xab;
20
+ const dst = createSurface(2, 2);
21
+ copySurfaceChannel(region(dst), ImageChannel.Blue, region(src), ImageChannel.Red);
22
+ expect(dst.data[2]).toBe(src.data[0]);
23
+ });
24
+
25
+ it('copies the alpha channel independently', () => {
26
+ const src = createSurface(1, 1);
27
+ setSurfacePixel(src, 0, 0, 0x000000de);
28
+ const dst = createSurface(1, 1);
29
+ copySurfaceChannel(region(dst), ImageChannel.Alpha, region(src), ImageChannel.Alpha);
30
+ expect(dst.data[3]).toBe(0xde);
31
+ });
32
+ });
33
+
34
+ describe('copySurfacePixels', () => {
35
+ it('copies a region without alpha blend', () => {
36
+ const src = createSurface(2, 2, 0xaabbccff);
37
+ const dst = createSurface(4, 4);
38
+ copySurfacePixels(region(dst, 1, 1, 2, 2), region(src, 0, 0, 2, 2));
39
+ expect(getSurfacePixel(dst, 1, 1)).toBe(0xaabbccff);
40
+ expect(getSurfacePixel(dst, 0, 0)).toBe(0x00000000);
41
+ });
42
+
43
+ it('alpha-composites when composite is true', () => {
44
+ const src = createSurface(1, 1);
45
+ setSurfacePixel(src, 0, 0, 0xff000080);
46
+ const dst = createSurface(1, 1);
47
+ setSurfacePixel(dst, 0, 0, 0x0000ffff);
48
+ copySurfacePixels(region(dst), region(src), true);
49
+ const result = getSurfacePixel(dst, 0, 0);
50
+ expect(result & 0xff).toBe(0xff); // alpha opaque after composite
51
+ expect((result >>> 24) & 0xff).toBeGreaterThan(0); // red blended in
52
+ expect((result >> 8) & 0xff).toBeGreaterThan(0); // blue retained
53
+ });
54
+ });
@@ -0,0 +1,39 @@
1
+ import { createSurface } from './surface';
2
+ import { getSurfaceCoverage } from './surfaceCoverage';
3
+ import { setSurfacePixel } from './surfacePixel';
4
+
5
+ describe('getSurfaceCoverage', () => {
6
+ it('returns 0 for a surface still entirely the background colour', () => {
7
+ expect(getSurfaceCoverage(createSurface(4, 4, 0x123456ff), 0x123456ff)).toBe(0);
8
+ });
9
+
10
+ it('returns 0 for a fully transparent surface against a transparent background', () => {
11
+ expect(getSurfaceCoverage(createSurface(4, 4, 0x00000000), 0x00000000)).toBe(0);
12
+ });
13
+
14
+ it('counts pixels that differ from the background on any channel', () => {
15
+ const surface = createSurface(2, 2, 0x000000ff);
16
+ setSurfacePixel(surface, 0, 0, 0xff0000ff);
17
+ expect(getSurfaceCoverage(surface, 0x000000ff)).toBe(0.25);
18
+ });
19
+
20
+ it('measures a fully painted surface as full coverage', () => {
21
+ const surface = createSurface(2, 2, 0x000000ff);
22
+ setSurfacePixel(surface, 0, 0, 0xffffffff);
23
+ setSurfacePixel(surface, 1, 0, 0xffffffff);
24
+ setSurfacePixel(surface, 0, 1, 0xffffffff);
25
+ setSurfacePixel(surface, 1, 1, 0xffffffff);
26
+ expect(getSurfaceCoverage(surface, 0x000000ff)).toBe(1);
27
+ });
28
+
29
+ it('ignores differences within the channel tolerance', () => {
30
+ const surface = createSurface(2, 1, 0x000000ff);
31
+ setSurfacePixel(surface, 0, 0, 0x050505ff); // 5 per channel
32
+ expect(getSurfaceCoverage(surface, 0x000000ff, 5)).toBe(0);
33
+ expect(getSurfaceCoverage(surface, 0x000000ff, 4)).toBe(0.5);
34
+ });
35
+
36
+ it('returns 0 for an empty surface', () => {
37
+ expect(getSurfaceCoverage(createSurface(0, 0), 0x000000ff)).toBe(0);
38
+ });
39
+ });
@@ -0,0 +1,116 @@
1
+ import { createSurface } from './surface';
2
+ import { cropSurface, extendSurface, trimSurface } from './surfaceCrop';
3
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
4
+
5
+ describe('cropSurface', () => {
6
+ it('returns a surface with the requested dimensions', () => {
7
+ const src = createSurface(10, 10, 0xff0000ff);
8
+ const out = cropSurface(src, { x: 2, y: 3, width: 4, height: 5 });
9
+ expect(out.width).toBe(4);
10
+ expect(out.height).toBe(5);
11
+ });
12
+
13
+ it('copies pixels from the source region', () => {
14
+ const src = createSurface(4, 4);
15
+ setSurfacePixel(src, 2, 1, 0x00ff00ff);
16
+ const out = cropSurface(src, { x: 2, y: 1, width: 2, height: 2 });
17
+ expect(getSurfacePixel(out, 0, 0)).toBe(0x00ff00ff);
18
+ });
19
+
20
+ it('fills pixels outside the source with transparent black', () => {
21
+ const src = createSurface(2, 2, 0xffffffff);
22
+ // Crop region extends beyond source.
23
+ const out = cropSurface(src, { x: 1, y: 1, width: 3, height: 3 });
24
+ // (0,0) is inside source — opaque white.
25
+ expect(getSurfacePixel(out, 0, 0)).toBe(0xffffffff);
26
+ // (2,2) is outside source — transparent black.
27
+ expect(getSurfacePixel(out, 2, 2)).toBe(0x00000000);
28
+ });
29
+
30
+ it('preserves source colorSpace and alphaType', () => {
31
+ const src = createSurface(2, 2, 0xff0000ff);
32
+ const out = cropSurface(src, { x: 0, y: 0, width: 2, height: 2 });
33
+ expect(out.colorSpace).toBe(src.colorSpace);
34
+ expect(out.alphaType).toBe(src.alphaType);
35
+ });
36
+
37
+ it('returns a distinct surface object', () => {
38
+ const src = createSurface(2, 2, 0xff0000ff);
39
+ const out = cropSurface(src, { x: 0, y: 0, width: 2, height: 2 });
40
+ expect(out).not.toBe(src);
41
+ expect(out.data).not.toBe(src.data);
42
+ });
43
+ });
44
+
45
+ describe('extendSurface', () => {
46
+ it('returns a surface padded by the correct number of pixels', () => {
47
+ const src = createSurface(2, 2, 0x0000ffff);
48
+ const out = extendSurface(src, 1, 2, 3, 4);
49
+ expect(out.width).toBe(2 + 1 + 3);
50
+ expect(out.height).toBe(2 + 2 + 4);
51
+ });
52
+
53
+ it('copies source pixels into the center region', () => {
54
+ const src = createSurface(2, 2, 0xabcdefff);
55
+ const out = extendSurface(src, 1, 1, 1, 1);
56
+ expect(getSurfacePixel(out, 1, 1)).toBe(0xabcdefff);
57
+ });
58
+
59
+ it('transparent edge mode fills padding with transparent black by default', () => {
60
+ const src = createSurface(2, 2, 0xff0000ff);
61
+ const out = extendSurface(src, 1, 1, 1, 1);
62
+ expect(getSurfacePixel(out, 0, 0)).toBe(0x00000000);
63
+ });
64
+
65
+ it('transparent edge mode uses fillColor when provided', () => {
66
+ const src = createSurface(2, 2, 0xff0000ff);
67
+ const out = extendSurface(src, 1, 1, 1, 1, 'transparent', 0xffffffff);
68
+ expect(getSurfacePixel(out, 0, 0)).toBe(0xffffffff);
69
+ });
70
+
71
+ it('clamp edge mode repeats border pixels in padding', () => {
72
+ const src = createSurface(2, 2, 0x0000ffff);
73
+ // Set a distinct border pixel.
74
+ setSurfacePixel(src, 0, 0, 0x123456ff);
75
+ const out = extendSurface(src, 1, 1, 0, 0, 'clamp');
76
+ // Top-left padding comes from source (0,0).
77
+ expect(getSurfacePixel(out, 0, 0)).toBe(0x123456ff);
78
+ });
79
+
80
+ it('wrap edge mode tiles the source in padding', () => {
81
+ const src = createSurface(2, 2);
82
+ setSurfacePixel(src, 0, 0, 0xff0000ff);
83
+ setSurfacePixel(src, 1, 0, 0x00ff00ff);
84
+ // Extend by 2 on the right → first padding column maps to source col 0.
85
+ const out = extendSurface(src, 0, 0, 2, 0, 'wrap');
86
+ expect(getSurfacePixel(out, 2, 0)).toBe(0xff0000ff);
87
+ expect(getSurfacePixel(out, 3, 0)).toBe(0x00ff00ff);
88
+ });
89
+ });
90
+
91
+ describe('trimSurface', () => {
92
+ it('removes transparent border rows and columns', () => {
93
+ const src = createSurface(5, 5);
94
+ // Only pixel (2,2) is non-transparent.
95
+ setSurfacePixel(src, 2, 2, 0xff0000ff);
96
+ const out = trimSurface(src);
97
+ expect(out.width).toBe(1);
98
+ expect(out.height).toBe(1);
99
+ expect(getSurfacePixel(out, 0, 0)).toBe(0xff0000ff);
100
+ });
101
+
102
+ it('returns a 1×1 transparent surface for a fully transparent input', () => {
103
+ const src = createSurface(3, 3);
104
+ const out = trimSurface(src);
105
+ expect(out.width).toBe(1);
106
+ expect(out.height).toBe(1);
107
+ expect(getSurfacePixel(out, 0, 0) & 0xff).toBe(0);
108
+ });
109
+
110
+ it('returns a surface equal to the source when there is no transparent border', () => {
111
+ const src = createSurface(2, 2, 0xff0000ff);
112
+ const out = trimSurface(src);
113
+ expect(out.width).toBe(2);
114
+ expect(out.height).toBe(2);
115
+ });
116
+ });
@@ -0,0 +1,142 @@
1
+ import { createSurface } from './surface';
2
+ import { displaceSurface } from './surfaceDisplacement';
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
+ // A 3x1 strip: red | green | blue.
15
+ function rgbStrip() {
16
+ const s = createSurface(3, 1);
17
+ s.data.set([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]);
18
+ return s;
19
+ }
20
+
21
+ describe('displaceSurface', () => {
22
+ it('is a no-op copy when scaleX and scaleY are 0', () => {
23
+ const source = rgbStrip();
24
+ const map = createSurface(3, 1);
25
+ map.data.fill(128);
26
+ const out = new Uint8ClampedArray(3 * 4);
27
+ displaceSurface(out, region(source), { map: region(map), scaleX: 0, scaleY: 0, mode: 'clamp' });
28
+ // With zero scale, every output pixel samples from its exact position.
29
+ expect(out[0]).toBe(255); // px0 = red
30
+ expect(out[4 + 1]).toBe(255); // px1 = green
31
+ expect(out[8 + 2]).toBe(255); // px2 = blue
32
+ });
33
+
34
+ it('shifts the sample position by the scaled map displacement', () => {
35
+ // map value 255 → dx = (255/255 - 0.5) * 2 = +1.0 exactly; scaleX=2.
36
+ const source = rgbStrip();
37
+ const map = createSurface(3, 1);
38
+ map.data[0] = 255;
39
+ map.data[4] = 255;
40
+ map.data[8] = 255;
41
+ const out = new Uint8ClampedArray(3 * 4);
42
+ displaceSurface(out, region(source), {
43
+ map: region(map),
44
+ componentX: 0,
45
+ scaleX: 2,
46
+ scaleY: 0,
47
+ mode: 'clamp',
48
+ });
49
+ // px0←src1 (green), px1←src2 (blue), px2←src3 oob clamped to src2 (blue)
50
+ expect(out[1]).toBe(255); // green channel of px0
51
+ expect(out[4 + 2]).toBe(255); // blue channel of px1
52
+ expect(out[8 + 2]).toBe(255); // blue channel of px2 (clamped)
53
+ });
54
+
55
+ it("'color' mode fills out-of-range samples with fillColor", () => {
56
+ // Map value 255 with scaleX=4 → dx=+2; px2 samples at x=4 (oob)
57
+ const source = rgbStrip();
58
+ const map = createSurface(3, 1);
59
+ map.data.fill(255);
60
+ const out = new Uint8ClampedArray(3 * 4);
61
+ displaceSurface(out, region(source), {
62
+ map: region(map),
63
+ scaleX: 4,
64
+ scaleY: 0,
65
+ mode: 'color',
66
+ fillColor: 0xff00ffff,
67
+ });
68
+ // px2 displaces to x=4 (out of range) → magenta fill
69
+ expect(out[2 * 4 + 0]).toBe(0xff);
70
+ expect(out[2 * 4 + 1]).toBe(0);
71
+ expect(out[2 * 4 + 2]).toBe(0xff);
72
+ expect(out[2 * 4 + 3]).toBe(255);
73
+ });
74
+
75
+ it("'ignore' mode keeps the undisplaced source pixel for out-of-range samples", () => {
76
+ // Map value 255 with scaleX=4 → px2 samples at x=4 (oob) → keep blue
77
+ const source = rgbStrip();
78
+ const map = createSurface(3, 1);
79
+ map.data.fill(255);
80
+ const out = new Uint8ClampedArray(3 * 4);
81
+ displaceSurface(out, region(source), {
82
+ map: region(map),
83
+ scaleX: 4,
84
+ scaleY: 0,
85
+ mode: 'ignore',
86
+ });
87
+ // px2 displaces out of range → keeps src2 (blue) unchanged
88
+ expect(out[2 * 4 + 2]).toBe(255);
89
+ expect(out[2 * 4 + 3]).toBe(255);
90
+ });
91
+
92
+ it("edgeMode 'clamp' clamps out-of-range samples to the edge", () => {
93
+ const source = rgbStrip();
94
+ const map = createSurface(3, 1);
95
+ map.data[0] = 255;
96
+ map.data[4] = 255;
97
+ map.data[8] = 255;
98
+ const out = new Uint8ClampedArray(3 * 4);
99
+ displaceSurface(out, region(source), {
100
+ map: region(map),
101
+ componentX: 0,
102
+ scaleX: 2,
103
+ scaleY: 0,
104
+ edgeMode: 'clamp',
105
+ });
106
+ // px2 displaces to x=3 (oob) → clamped to x=2 (blue)
107
+ expect(out[8 + 2]).toBe(255);
108
+ });
109
+
110
+ it("edgeMode 'transparent' fills out-of-range samples with transparent black", () => {
111
+ const source = rgbStrip();
112
+ const map = createSurface(3, 1);
113
+ map.data.fill(255);
114
+ const out = new Uint8ClampedArray(3 * 4);
115
+ displaceSurface(out, region(source), {
116
+ map: region(map),
117
+ scaleX: 4,
118
+ scaleY: 0,
119
+ edgeMode: 'transparent',
120
+ });
121
+ // px2 displaces to x=4 (oob) → transparent black
122
+ expect(out[2 * 4 + 0]).toBe(0);
123
+ expect(out[2 * 4 + 3]).toBe(0);
124
+ });
125
+
126
+ it('samples through the source region offset', () => {
127
+ // 4px [red, green, blue, white]; zero displacement over region (1,0,2,1) copies
128
+ // source pixels 1 and 2 (green, blue), proving the offset is applied.
129
+ const source = createSurface(4, 1);
130
+ source.data.set([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255]);
131
+ const map = createSurface(2, 1);
132
+ map.data.fill(128); // neutral — minimal displacement at scale=0
133
+ const out = new Uint8ClampedArray(2 * 4);
134
+ displaceSurface(out, region(source, 1, 0, 2, 1), {
135
+ map: region(map),
136
+ scaleX: 0,
137
+ scaleY: 0,
138
+ });
139
+ expect(out[1]).toBe(255); // green from source[1]
140
+ expect(out[4 + 2]).toBe(255); // blue from source[2]
141
+ });
142
+ });
@@ -0,0 +1,99 @@
1
+ import { createSurface } from './surface';
2
+ import { dissolveSurfacePixels } from './surfaceDissolve';
3
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
4
+ import { createSurfaceRegion } from './surfaceRegion';
5
+
6
+ function countChangedPixels(data: Readonly<Uint8ClampedArray>, original: Readonly<Uint8ClampedArray>): number {
7
+ let changed = 0;
8
+ for (let i = 0; i < data.length; i += 4) {
9
+ if (
10
+ data[i] !== original[i] ||
11
+ data[i + 1] !== original[i + 1] ||
12
+ data[i + 2] !== original[i + 2] ||
13
+ data[i + 3] !== original[i + 3]
14
+ ) {
15
+ changed++;
16
+ }
17
+ }
18
+ return changed;
19
+ }
20
+
21
+ describe('dissolveSurfacePixels', () => {
22
+ it('dissolves at most pixelCount pixels per call', () => {
23
+ const source = createSurface(4, 4, 0x112233ff);
24
+ const dest = createSurface(4, 4, 0x00000000);
25
+ const original = dest.data.slice();
26
+ dissolveSurfacePixels(createSurfaceRegion(dest), createSurfaceRegion(source), 0, 5);
27
+ expect(countChangedPixels(dest.data, original)).toBe(5);
28
+ });
29
+
30
+ it('eventually covers every pixel exactly once so dest matches source', () => {
31
+ const source = createSurface(5, 3);
32
+ for (let i = 0; i < 5 * 3; i++) setSurfacePixel(source, i % 5, (i / 5) | 0, (0x01010100 * (i + 1)) >>> 0);
33
+ const dest = createSurface(5, 3, 0x00000000);
34
+ const sourceRegion = createSurfaceRegion(source);
35
+ const destRegion = createSurfaceRegion(dest);
36
+
37
+ let seed = 0;
38
+ for (let call = 0; call < 8; call++) seed = dissolveSurfacePixels(destRegion, sourceRegion, seed, 2);
39
+
40
+ expect(Array.from(dest.data)).toEqual(Array.from(source.data));
41
+ });
42
+
43
+ it('is deterministic for the same seed sequence', () => {
44
+ const source = createSurface(6, 6, 0xaabbccff);
45
+ const a = createSurface(6, 6, 0x00000000);
46
+ const b = createSurface(6, 6, 0x00000000);
47
+ const sourceRegion = createSurfaceRegion(source);
48
+ dissolveSurfacePixels(createSurfaceRegion(a), sourceRegion, 3, 7);
49
+ dissolveSurfacePixels(createSurfaceRegion(b), sourceRegion, 3, 7);
50
+ expect(Array.from(a.data)).toEqual(Array.from(b.data));
51
+ });
52
+
53
+ it('returns a terminal seed once fully dissolved that no-ops on reuse', () => {
54
+ const source = createSurface(4, 4, 0x445566ff);
55
+ const dest = createSurface(4, 4, 0x00000000);
56
+ const sourceRegion = createSurfaceRegion(source);
57
+ const destRegion = createSurfaceRegion(dest);
58
+
59
+ let seed = 0;
60
+ for (let call = 0; call < 16; call++) seed = dissolveSurfacePixels(destRegion, sourceRegion, seed, 1);
61
+ expect(Array.from(dest.data)).toEqual(Array.from(source.data));
62
+
63
+ const afterComplete = dissolveSurfacePixels(destRegion, sourceRegion, seed, 4);
64
+ expect(afterComplete).toBe(seed);
65
+ });
66
+
67
+ it('dissolves toward fillColor when source and dest are the same region', () => {
68
+ const surface = createSurface(3, 3, 0x112233ff);
69
+ const region = createSurfaceRegion(surface);
70
+ let seed = 0;
71
+ for (let call = 0; call < 9; call++) seed = dissolveSurfacePixels(region, region, seed, 1, 0x99887766);
72
+ for (let i = 0; i < 9; i++) expect(getSurfacePixel(surface, i % 3, (i / 3) | 0)).toBe(0x99887766);
73
+ });
74
+
75
+ it('ignores fillColor and copies from source when regions differ', () => {
76
+ const source = createSurface(2, 2, 0x0a0b0c0d);
77
+ const dest = createSurface(2, 2, 0x00000000);
78
+ let seed = 0;
79
+ for (let call = 0; call < 4; call++) {
80
+ seed = dissolveSurfacePixels(createSurfaceRegion(dest), createSurfaceRegion(source), seed, 1, 0xffffffff);
81
+ }
82
+ expect(Array.from(dest.data)).toEqual(Array.from(source.data));
83
+ });
84
+
85
+ it('returns the seed unchanged for a non-positive pixelCount without writing', () => {
86
+ const source = createSurface(4, 4, 0x112233ff);
87
+ const dest = createSurface(4, 4, 0x00000000);
88
+ const original = dest.data.slice();
89
+ expect(dissolveSurfacePixels(createSurfaceRegion(dest), createSurfaceRegion(source), 2, 0)).toBe(2);
90
+ expect(countChangedPixels(dest.data, original)).toBe(0);
91
+ });
92
+
93
+ it('returns the seed for a zero-area region', () => {
94
+ const source = createSurface(4, 4, 0x112233ff);
95
+ const dest = createSurface(4, 4, 0x00000000);
96
+ const region = createSurfaceRegion(dest, 0, 0, 0, 0);
97
+ expect(dissolveSurfacePixels(region, createSurfaceRegion(source), 5, 10)).toBe(5);
98
+ });
99
+ });
@@ -0,0 +1,38 @@
1
+ import { createSurface } from './surface';
2
+ import { drawSurface } from './surfaceDraw';
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
+ describe('drawSurface', () => {
15
+ it('does not throw when drawing onto a canvas', () => {
16
+ const src = createSurface(2, 2, 0x112233ff);
17
+ const canvas = document.createElement('canvas');
18
+ canvas.width = 4;
19
+ canvas.height = 4;
20
+ expect(() => drawSurface(canvas, region(src), 0, 0)).not.toThrow();
21
+ });
22
+
23
+ it('does not throw when drawing at an offset', () => {
24
+ const src = createSurface(2, 2, 0x112233ff);
25
+ const canvas = document.createElement('canvas');
26
+ canvas.width = 8;
27
+ canvas.height = 8;
28
+ expect(() => drawSurface(canvas, region(src), 2, 2)).not.toThrow();
29
+ });
30
+
31
+ it('is a no-op for a zero-dimension region', () => {
32
+ const src = createSurface(2, 2, 0x112233ff);
33
+ const canvas = document.createElement('canvas');
34
+ canvas.width = 4;
35
+ canvas.height = 4;
36
+ expect(() => drawSurface(canvas, region(src, 0, 0, 0, 0), 0, 0)).not.toThrow();
37
+ });
38
+ });
@@ -0,0 +1,18 @@
1
+ import { createSurface } from './surface';
2
+ import { encodeSurface } from './surfaceEncode';
3
+
4
+ describe('encodeSurface', () => {
5
+ it('returns a non-empty Uint8Array for a simple image', () => {
6
+ const img = createSurface(2, 2, 0x112233ff);
7
+ const bytes = encodeSurface(img);
8
+ expect(bytes).toBeInstanceOf(Uint8Array);
9
+ expect(bytes.length).toBeGreaterThan(0);
10
+ });
11
+
12
+ it('accepts jpeg format', () => {
13
+ const img = createSurface(2, 2, 0x112233ff);
14
+ const bytes = encodeSurface(img, 'jpeg');
15
+ expect(bytes).toBeInstanceOf(Uint8Array);
16
+ expect(bytes.length).toBeGreaterThan(0);
17
+ });
18
+ });
@@ -0,0 +1,89 @@
1
+ import { createSurface } from './surface';
2
+ import { fillSurfaceRectangle, floodFillSurface } from './surfaceFill';
3
+ import { getSurfacePixel } from './surfacePixel';
4
+
5
+ function region(
6
+ surface: ReturnType<typeof createSurface>,
7
+ x = 0,
8
+ y = 0,
9
+ width = surface.width,
10
+ height = surface.height,
11
+ ) {
12
+ return { surface, x, y, width, height };
13
+ }
14
+
15
+ describe('fillSurfaceRectangle', () => {
16
+ it('fills the specified region', () => {
17
+ const img = createSurface(4, 4);
18
+ fillSurfaceRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
19
+ expect(getSurfacePixel(img, 1, 1)).toBe(0xaabbccff);
20
+ expect(getSurfacePixel(img, 2, 2)).toBe(0xaabbccff);
21
+ });
22
+
23
+ it('bumps the surface version (self-invalidation)', () => {
24
+ const img = createSurface(4, 4);
25
+ const before = img.version;
26
+ fillSurfaceRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
27
+ expect(img.version).toBe(before + 1);
28
+ });
29
+
30
+ it('does not affect pixels outside the region', () => {
31
+ const img = createSurface(4, 4);
32
+ fillSurfaceRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
33
+ expect(getSurfacePixel(img, 0, 0)).toBe(0x00000000);
34
+ expect(getSurfacePixel(img, 3, 3)).toBe(0x00000000);
35
+ });
36
+
37
+ it('skips pixels outside the surface bounds', () => {
38
+ const img = createSurface(2, 2, 0x000000ff);
39
+ fillSurfaceRectangle(region(img, -1, -1, 4, 4), 0xffffffff);
40
+ expect(getSurfacePixel(img, 0, 0)).toBe(0xffffffff);
41
+ expect(getSurfacePixel(img, 1, 1)).toBe(0xffffffff);
42
+ });
43
+ });
44
+
45
+ describe('floodFillSurface', () => {
46
+ it('fills a connected region', () => {
47
+ const img = createSurface(3, 3, 0xffffffff);
48
+ floodFillSurface(img, 0, 0, 0x000000ff);
49
+ for (let py = 0; py < 3; py++) {
50
+ for (let px = 0; px < 3; px++) {
51
+ expect(getSurfacePixel(img, px, py)).toBe(0x000000ff);
52
+ }
53
+ }
54
+ });
55
+
56
+ it('does not cross a barrier', () => {
57
+ const img = createSurface(3, 3, 0xffffffff);
58
+ for (let py = 0; py < 3; py++) {
59
+ const i = (py * 3 + 1) * 4;
60
+ img.data[i] = 0;
61
+ img.data[i + 1] = 0;
62
+ img.data[i + 2] = 0;
63
+ img.data[i + 3] = 0xff;
64
+ }
65
+ floodFillSurface(img, 0, 0, 0x0000ffff);
66
+ expect(getSurfacePixel(img, 0, 0)).toBe(0x0000ffff);
67
+ expect(getSurfacePixel(img, 2, 0)).toBe(0xffffffff);
68
+ });
69
+
70
+ it('is a no-op when fill color matches target', () => {
71
+ const img = createSurface(2, 2, 0x112233ff);
72
+ floodFillSurface(img, 0, 0, 0x112233ff);
73
+ expect(getSurfacePixel(img, 0, 0)).toBe(0x112233ff);
74
+ });
75
+
76
+ it('reuses the scratch buffer across calls', () => {
77
+ const img = createSurface(4, 4, 0xffffffff);
78
+ floodFillSurface(img, 0, 0, 0x000000ff);
79
+ floodFillSurface(img, 0, 0, 0xffffffff); // second call reuses buffer
80
+ expect(getSurfacePixel(img, 0, 0)).toBe(0xffffffff);
81
+ });
82
+
83
+ it('reuses the scratch buffer across calls', () => {
84
+ const img = createSurface(4, 4, 0xffffffff);
85
+ floodFillSurface(img, 0, 0, 0x000000ff);
86
+ floodFillSurface(img, 0, 0, 0xffffffff); // second call reuses buffer
87
+ expect(getSurfacePixel(img, 0, 0)).toBe(0xffffffff);
88
+ });
89
+ });