@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,75 @@
1
+ import { createSurface } from './surface';
2
+ import { dilateSurface, erodeSurface } from './surfaceMorphological';
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('dilateSurface', () => {
15
+ it('expands a single bright pixel into its neighborhood', () => {
16
+ const source = createSurface(3, 1);
17
+ source.data[1 * 4 + 3] = 255; // center pixel fully opaque
18
+ const out = new Uint8ClampedArray(3 * 4);
19
+ dilateSurface(out, region(source), 1);
20
+ expect(out[0 * 4 + 3]).toBe(255);
21
+ expect(out[1 * 4 + 3]).toBe(255);
22
+ expect(out[2 * 4 + 3]).toBe(255);
23
+ });
24
+
25
+ it('with radius 0 is an identity copy', () => {
26
+ const source = createSurface(3, 1);
27
+ source.data.set([10, 20, 30, 100], 0);
28
+ source.data.set([50, 60, 70, 200], 4);
29
+ const out = new Uint8ClampedArray(3 * 4);
30
+ dilateSurface(out, region(source), 0);
31
+ expect(out[0]).toBe(10);
32
+ expect(out[4]).toBe(50);
33
+ });
34
+
35
+ it('is dual to erodeSurface (dilate after erode has no-smaller effect)', () => {
36
+ const source = createSurface(5, 1);
37
+ for (let i = 1; i < 4; i++) source.data[i * 4 + 3] = 255; // pixels 1-3 opaque
38
+ const eroded = new Uint8ClampedArray(5 * 4);
39
+ const dilated = new Uint8ClampedArray(5 * 4);
40
+ erodeSurface(eroded, region(source), 1);
41
+ dilateSurface(dilated, region(source), 1);
42
+ // Dilation always >= source, erosion always <= source for alpha
43
+ expect(dilated[1 * 4 + 3]).toBeGreaterThanOrEqual(source.data[1 * 4 + 3]);
44
+ expect(eroded[1 * 4 + 3]).toBeLessThanOrEqual(source.data[1 * 4 + 3]);
45
+ });
46
+ });
47
+
48
+ describe('erodeSurface', () => {
49
+ it('shrinks an isolated bright pixel to dark when radius covers neighbors', () => {
50
+ const source = createSurface(3, 1);
51
+ source.data[1 * 4 + 3] = 255; // only center opaque
52
+ const out = new Uint8ClampedArray(3 * 4);
53
+ erodeSurface(out, region(source), 1);
54
+ // Center eroded to 0 because transparent neighbors dominate the min
55
+ expect(out[1 * 4 + 3]).toBe(0);
56
+ });
57
+
58
+ it('with radius 0 is an identity copy', () => {
59
+ const source = createSurface(3, 1);
60
+ source.data.set([10, 20, 30, 100], 4);
61
+ const out = new Uint8ClampedArray(3 * 4);
62
+ erodeSurface(out, region(source), 0);
63
+ expect(out[4]).toBe(10);
64
+ expect(out[7]).toBe(100);
65
+ });
66
+
67
+ it('preserves a fully opaque field', () => {
68
+ const source = createSurface(5, 1, 0xffffffff);
69
+ const out = new Uint8ClampedArray(5 * 4);
70
+ erodeSurface(out, region(source), 2);
71
+ for (let i = 0; i < 5; i++) {
72
+ expect(out[i * 4 + 3]).toBe(255);
73
+ }
74
+ });
75
+ });
@@ -0,0 +1,169 @@
1
+ import { createSurface } from './surface';
2
+ import {
3
+ fillSurfaceNoise,
4
+ fillSurfacePerlinNoise,
5
+ fillSurfaceTurbulence,
6
+ SURFACE_NOISE_CHANNEL_A,
7
+ SURFACE_NOISE_CHANNEL_R,
8
+ } from './surfaceNoise';
9
+
10
+ function region(
11
+ surface: ReturnType<typeof createSurface>,
12
+ x = 0,
13
+ y = 0,
14
+ width = surface.width,
15
+ height = surface.height,
16
+ ) {
17
+ return { surface, x, y, width, height };
18
+ }
19
+
20
+ describe('fillSurfaceNoise', () => {
21
+ it('is deterministic for the same seed', () => {
22
+ const a = createSurface(4, 4);
23
+ const b = createSurface(4, 4);
24
+ fillSurfaceNoise(region(a), 12345);
25
+ fillSurfaceNoise(region(b), 12345);
26
+ expect(Array.from(a.data)).toEqual(Array.from(b.data));
27
+ });
28
+
29
+ it('produces different output for different seeds', () => {
30
+ const a = createSurface(8, 8);
31
+ const b = createSurface(8, 8);
32
+ fillSurfaceNoise(region(a), 1);
33
+ fillSurfaceNoise(region(b), 2);
34
+ expect(Array.from(a.data)).not.toEqual(Array.from(b.data));
35
+ });
36
+
37
+ it('grayscale gives every pixel equal R, G, B', () => {
38
+ const surface = createSurface(4, 4);
39
+ fillSurfaceNoise(region(surface), 99, 0, 255, true);
40
+ for (let i = 0; i < surface.data.length; i += 4) {
41
+ expect(surface.data[i]).toBe(surface.data[i + 1]);
42
+ expect(surface.data[i + 1]).toBe(surface.data[i + 2]);
43
+ }
44
+ });
45
+
46
+ it('respects the low/high range and sets alpha opaque', () => {
47
+ const surface = createSurface(8, 8);
48
+ fillSurfaceNoise(region(surface), 7, 100, 110);
49
+ for (let i = 0; i < surface.data.length; i += 4) {
50
+ expect(surface.data[i]).toBeGreaterThanOrEqual(100);
51
+ expect(surface.data[i]).toBeLessThanOrEqual(110);
52
+ expect(surface.data[i + 3]).toBe(255);
53
+ }
54
+ });
55
+
56
+ it('fills only the given sub-region', () => {
57
+ const surface = createSurface(2, 1, 0x000000ff);
58
+ fillSurfaceNoise(region(surface, 1, 0, 1, 1), 5, 200, 255);
59
+ expect(surface.data[0]).toBe(0); // left pixel untouched
60
+ expect(surface.data[4]).toBeGreaterThanOrEqual(200);
61
+ });
62
+ });
63
+
64
+ describe('fillSurfacePerlinNoise', () => {
65
+ it('is deterministic for the same seed', () => {
66
+ const a = createSurface(8, 8);
67
+ const b = createSurface(8, 8);
68
+ fillSurfacePerlinNoise(region(a), 16, 16, 3, 42);
69
+ fillSurfacePerlinNoise(region(b), 16, 16, 3, 42);
70
+ expect(Array.from(a.data)).toEqual(Array.from(b.data));
71
+ });
72
+
73
+ it('keeps values in range and alpha opaque', () => {
74
+ const surface = createSurface(8, 8);
75
+ fillSurfacePerlinNoise(region(surface), 8, 8, 2, 5);
76
+ for (let i = 0; i < surface.data.length; i += 4) {
77
+ expect(surface.data[i]).toBeGreaterThanOrEqual(0);
78
+ expect(surface.data[i]).toBeLessThanOrEqual(255);
79
+ expect(surface.data[i + 3]).toBe(255);
80
+ }
81
+ });
82
+
83
+ it('grayscale gives every pixel equal R, G, B', () => {
84
+ const surface = createSurface(8, 8);
85
+ fillSurfacePerlinNoise(region(surface), 8, 8, 2, 5, true);
86
+ for (let i = 0; i < surface.data.length; i += 4) {
87
+ expect(surface.data[i]).toBe(surface.data[i + 1]);
88
+ expect(surface.data[i + 1]).toBe(surface.data[i + 2]);
89
+ }
90
+ });
91
+
92
+ it('produces smooth low-frequency fields (neighbors stay close)', () => {
93
+ const surface = createSurface(16, 1);
94
+ fillSurfacePerlinNoise(region(surface), 32, 32, 1, 3, true);
95
+ let maxStep = 0;
96
+ for (let x = 1; x < 16; x++) {
97
+ maxStep = Math.max(maxStep, Math.abs(surface.data[x * 4] - surface.data[(x - 1) * 4]));
98
+ }
99
+ expect(maxStep).toBeLessThan(64);
100
+ });
101
+
102
+ it('fills selected channels only and leaves alpha opaque by default', () => {
103
+ const surface = createSurface(8, 8, 0x000000ff);
104
+ fillSurfacePerlinNoise(region(surface), 8, 8, 2, 5, false, false, SURFACE_NOISE_CHANNEL_R);
105
+ for (let i = 0; i < surface.data.length; i += 4) {
106
+ // G and B were not selected, so they stay at their initial 0.
107
+ expect(surface.data[i + 1]).toBe(0);
108
+ expect(surface.data[i + 2]).toBe(0);
109
+ // Alpha not selected -> forced opaque.
110
+ expect(surface.data[i + 3]).toBe(255);
111
+ }
112
+ });
113
+ });
114
+
115
+ describe('fillSurfaceTurbulence', () => {
116
+ it('is deterministic for the same seed', () => {
117
+ const a = createSurface(8, 8);
118
+ const b = createSurface(8, 8);
119
+ fillSurfaceTurbulence(region(a), 16, 16, 3, 42);
120
+ fillSurfaceTurbulence(region(b), 16, 16, 3, 42);
121
+ expect(Array.from(a.data)).toEqual(Array.from(b.data));
122
+ });
123
+
124
+ it('keeps values in range and alpha opaque', () => {
125
+ const surface = createSurface(8, 8);
126
+ fillSurfaceTurbulence(region(surface), 8, 8, 2, 5);
127
+ for (let i = 0; i < surface.data.length; i += 4) {
128
+ expect(surface.data[i]).toBeGreaterThanOrEqual(0);
129
+ expect(surface.data[i]).toBeLessThanOrEqual(255);
130
+ expect(surface.data[i + 3]).toBe(255);
131
+ }
132
+ });
133
+
134
+ it('grayscale gives every pixel equal R, G, B', () => {
135
+ const surface = createSurface(8, 8);
136
+ fillSurfaceTurbulence(region(surface), 8, 8, 2, 5, true);
137
+ for (let i = 0; i < surface.data.length; i += 4) {
138
+ expect(surface.data[i]).toBe(surface.data[i + 1]);
139
+ expect(surface.data[i + 1]).toBe(surface.data[i + 2]);
140
+ }
141
+ });
142
+
143
+ it('writes noise into alpha when the A channel is selected', () => {
144
+ const surface = createSurface(8, 8, 0x000000ff);
145
+ fillSurfaceTurbulence(region(surface), 4, 4, 3, 9, false, false, SURFACE_NOISE_CHANNEL_A);
146
+ let sawNonOpaque = false;
147
+ for (let i = 0; i < surface.data.length; i += 4) {
148
+ if (surface.data[i + 3] !== 255) sawNonOpaque = true;
149
+ }
150
+ expect(sawNonOpaque).toBe(true);
151
+ });
152
+
153
+ it('differs from the smooth fractal sum for the same parameters', () => {
154
+ const turbulent = createSurface(8, 8);
155
+ const fractal = createSurface(8, 8);
156
+ fillSurfaceTurbulence(region(turbulent), 8, 8, 3, 7, true);
157
+ fillSurfacePerlinNoise(region(fractal), 8, 8, 3, 7, true);
158
+ expect(Array.from(turbulent.data)).not.toEqual(Array.from(fractal.data));
159
+ });
160
+
161
+ it('fills only the given sub-region', () => {
162
+ const surface = createSurface(2, 1, 0x010203ff);
163
+ fillSurfaceTurbulence(region(surface, 1, 0, 1, 1), 8, 8, 2, 3);
164
+ // Left pixel untouched.
165
+ expect(surface.data[0]).toBe(1);
166
+ expect(surface.data[1]).toBe(2);
167
+ expect(surface.data[2]).toBe(3);
168
+ });
169
+ });
@@ -0,0 +1,55 @@
1
+ import { createSurface } from './surface';
2
+ import { applySurfacePaletteMap } from './surfacePaletteMap';
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
+ function invertTable(): number[] {
15
+ const table = new Array<number>(256);
16
+ for (let i = 0; i < 256; i++) table[i] = 255 - i;
17
+ return table;
18
+ }
19
+
20
+ describe('applySurfacePaletteMap', () => {
21
+ it('remaps a channel through its lookup table', () => {
22
+ const source = createSurface(1, 1, 0x10203040);
23
+ const out = createSurface(1, 1);
24
+ applySurfacePaletteMap(region(out), region(source), invertTable(), null, null, null);
25
+ expect(out.data[0]).toBe(255 - 0x10);
26
+ expect(out.data[1]).toBe(0x20);
27
+ expect(out.data[2]).toBe(0x30);
28
+ expect(out.data[3]).toBe(0x40);
29
+ });
30
+
31
+ it('null maps leave every channel unchanged', () => {
32
+ const source = createSurface(1, 1, 0x11223344);
33
+ const out = createSurface(1, 1);
34
+ applySurfacePaletteMap(region(out), region(source), null, null, null, null);
35
+ expect(out.data[0]).toBe(0x11);
36
+ expect(out.data[3]).toBe(0x44);
37
+ });
38
+
39
+ it('is safe in place', () => {
40
+ const img = createSurface(1, 1, 0x00ff00ff);
41
+ applySurfacePaletteMap(region(img), region(img), invertTable(), invertTable(), invertTable(), null);
42
+ expect(img.data[0]).toBe(255);
43
+ expect(img.data[1]).toBe(0);
44
+ expect(img.data[2]).toBe(255);
45
+ expect(img.data[3]).toBe(0xff);
46
+ });
47
+
48
+ it('maps only the given sub-region', () => {
49
+ const source = createSurface(2, 1, 0x80808080);
50
+ const out = createSurface(2, 1);
51
+ applySurfacePaletteMap(region(out, 1, 0, 1, 1), region(source, 1, 0, 1, 1), invertTable(), null, null, null);
52
+ expect(out.data[4]).toBe(255 - 0x80);
53
+ expect(out.data[0]).toBe(0); // untouched left pixel
54
+ });
55
+ });
@@ -0,0 +1,99 @@
1
+ import { createSurface } from './surface';
2
+ import { ImageChannel } from './surfaceImageChannel';
3
+ import {
4
+ getSurfacePixel,
5
+ getSurfacePixelChannel,
6
+ getSurfacePixelLuminance,
7
+ getSurfacePixelRgb,
8
+ setSurfacePixel,
9
+ setSurfacePixelRgb,
10
+ } from './surfacePixel';
11
+
12
+ describe('getSurfacePixel', () => {
13
+ it('reads back a full 0xRRGGBBAA value written by setSurfacePixel', () => {
14
+ const img = createSurface(2, 2);
15
+ setSurfacePixel(img, 0, 0, 0xaabbcc80);
16
+ expect(getSurfacePixel(img, 0, 0)).toBe(0xaabbcc80);
17
+ });
18
+
19
+ it('round-trips an RGBA value', () => {
20
+ const img = createSurface(4, 4);
21
+ setSurfacePixel(img, 1, 2, 0x11223380);
22
+ expect(getSurfacePixel(img, 1, 2)).toBe(0x11223380);
23
+ });
24
+
25
+ it('stores alpha in the fourth byte', () => {
26
+ const img = createSurface(2, 2);
27
+ setSurfacePixel(img, 0, 0, 0x112233de);
28
+ expect(img.data[3]).toBe(0xde);
29
+ });
30
+ });
31
+
32
+ describe('getSurfacePixelChannel', () => {
33
+ it('reads a single channel by index', () => {
34
+ const img = createSurface(2, 2);
35
+ setSurfacePixel(img, 0, 0, 0xaabbccdd);
36
+ expect(getSurfacePixelChannel(img, 0, 0, ImageChannel.Red)).toBe(0xaa);
37
+ expect(getSurfacePixelChannel(img, 0, 0, ImageChannel.Green)).toBe(0xbb);
38
+ expect(getSurfacePixelChannel(img, 0, 0, ImageChannel.Blue)).toBe(0xcc);
39
+ expect(getSurfacePixelChannel(img, 0, 0, ImageChannel.Alpha)).toBe(0xdd);
40
+ });
41
+ });
42
+
43
+ describe('getSurfacePixelLuminance', () => {
44
+ it('returns 0 for black', () => {
45
+ const img = createSurface(1, 1, 0x000000ff);
46
+ expect(getSurfacePixelLuminance(img, 0, 0)).toBe(0);
47
+ });
48
+
49
+ it('returns 255 for white', () => {
50
+ const img = createSurface(1, 1, 0xffffffff);
51
+ expect(getSurfacePixelLuminance(img, 0, 0)).toBe(255);
52
+ });
53
+
54
+ it('uses W3C luma weighting (green is heaviest)', () => {
55
+ const red = createSurface(1, 1, 0xff0000ff);
56
+ const green = createSurface(1, 1, 0x00ff00ff);
57
+ const blue = createSurface(1, 1, 0x0000ffff);
58
+ expect(getSurfacePixelLuminance(green, 0, 0)).toBeGreaterThan(getSurfacePixelLuminance(red, 0, 0));
59
+ expect(getSurfacePixelLuminance(red, 0, 0)).toBeGreaterThan(getSurfacePixelLuminance(blue, 0, 0));
60
+ });
61
+ });
62
+
63
+ describe('getSurfacePixelRgb', () => {
64
+ it('reads back a 0xRRGGBB value without alpha', () => {
65
+ const img = createSurface(2, 2);
66
+ setSurfacePixelRgb(img, 0, 0, 0xaabbcc);
67
+ expect(getSurfacePixelRgb(img, 0, 0)).toBe(0xaabbcc);
68
+ });
69
+
70
+ it('round-trips an RGB value', () => {
71
+ const img = createSurface(4, 4);
72
+ setSurfacePixelRgb(img, 1, 2, 0x112233);
73
+ expect(getSurfacePixelRgb(img, 1, 2)).toBe(0x112233);
74
+ });
75
+ });
76
+
77
+ describe('setSurfacePixel', () => {
78
+ it('writes all four RGBA channels', () => {
79
+ const img = createSurface(2, 2);
80
+ setSurfacePixel(img, 1, 0, 0xadbeefde);
81
+ expect(getSurfacePixel(img, 1, 0)).toBe(0xadbeefde);
82
+ });
83
+
84
+ it('bumps the surface version (self-invalidation)', () => {
85
+ const img = createSurface(2, 2);
86
+ const before = img.version;
87
+ setSurfacePixel(img, 1, 0, 0xadbeefde);
88
+ expect(img.version).toBe(before + 1);
89
+ });
90
+ });
91
+
92
+ describe('setSurfacePixelRgb', () => {
93
+ it('writes RGB channels without touching alpha', () => {
94
+ const img = createSurface(2, 2, 0x000000ff);
95
+ setSurfacePixelRgb(img, 0, 0, 0x112233);
96
+ expect(img.data[3]).toBe(0xff);
97
+ expect(getSurfacePixelRgb(img, 0, 0)).toBe(0x112233);
98
+ });
99
+ });
@@ -0,0 +1,46 @@
1
+ import { createSurface } from './surface';
2
+ import { pixelateSurface } from './surfacePixelate';
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('pixelateSurface', () => {
15
+ it('averages a block to a single uniform color', () => {
16
+ // 2x1 row: R=0 and R=100; a block of 2 averages both to 50.
17
+ const source = createSurface(2, 1);
18
+ source.data[0] = 0;
19
+ source.data[4] = 100;
20
+ source.data[3] = 255;
21
+ source.data[7] = 255;
22
+ const out = new Uint8ClampedArray(2 * 4);
23
+ pixelateSurface(out, region(source), 2);
24
+ expect(out[0]).toBe(50);
25
+ expect(out[4]).toBe(50);
26
+ });
27
+
28
+ it('keeps each block independent', () => {
29
+ const source = createSurface(2, 1);
30
+ source.data[0] = 10;
31
+ source.data[4] = 200;
32
+ const out = new Uint8ClampedArray(2 * 4);
33
+ pixelateSurface(out, region(source), 1);
34
+ expect(out[0]).toBe(10);
35
+ expect(out[4]).toBe(200);
36
+ });
37
+
38
+ it('can use source.surface.data as out for a full-surface region', () => {
39
+ const surface = createSurface(2, 1);
40
+ surface.data[0] = 0;
41
+ surface.data[4] = 80;
42
+ pixelateSurface(surface.data, region(surface), 2);
43
+ expect(surface.data[0]).toBe(40);
44
+ expect(surface.data[4]).toBe(40);
45
+ });
46
+ });
@@ -0,0 +1,48 @@
1
+ import { createSurface } from './surface';
2
+ import { fillSurfaceRectangle } from './surfaceFill';
3
+ import { getSurfaceColorBoundsRectangle } from './surfaceQuery';
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('getSurfaceColorBoundsRectangle', () => {
16
+ it('returns null when no pixels match', () => {
17
+ const img = createSurface(4, 4);
18
+ expect(getSurfaceColorBoundsRectangle(region(img), 0xffffffff, 0xffffffff)).toBeNull();
19
+ });
20
+
21
+ it('finds the bounding rect of matching pixels', () => {
22
+ const img = createSurface(8, 8);
23
+ fillSurfaceRectangle(region(img, 2, 3, 3, 2), 0xff0000ff);
24
+ const rect = getSurfaceColorBoundsRectangle(region(img), 0xffffffff, 0xff0000ff);
25
+ expect(rect).toEqual({ x: 2, y: 3, width: 3, height: 2 });
26
+ });
27
+
28
+ it('finds non-matching pixels when findColor is false', () => {
29
+ const img = createSurface(4, 4, 0xffffffff);
30
+ fillSurfaceRectangle(region(img, 1, 1, 2, 2), 0x000000ff);
31
+ const rect = getSurfaceColorBoundsRectangle(region(img), 0xffffffff, 0xffffffff, false);
32
+ expect(rect).toEqual({ x: 1, y: 1, width: 2, height: 2 });
33
+ });
34
+
35
+ it('respects the mask', () => {
36
+ const img = createSurface(2, 2);
37
+ fillSurfaceRectangle(region(img), 0x112233ff);
38
+ const rect = getSurfaceColorBoundsRectangle(region(img), 0xff0000ff, 0x110000ff);
39
+ expect(rect).not.toBeNull();
40
+ });
41
+
42
+ it('reports surface-absolute coordinates when scanning a sub-region', () => {
43
+ const img = createSurface(8, 8);
44
+ fillSurfaceRectangle(region(img, 5, 5, 2, 2), 0xff0000ff);
45
+ const rect = getSurfaceColorBoundsRectangle(region(img, 4, 4, 4, 4), 0xffffffff, 0xff0000ff);
46
+ expect(rect).toEqual({ x: 5, y: 5, width: 2, height: 2 });
47
+ });
48
+ });
@@ -0,0 +1,34 @@
1
+ import { createSurface } from './surface';
2
+ import { createSurfaceRegion, setSurfaceRegion } from './surfaceRegion';
3
+
4
+ describe('createSurfaceRegion', () => {
5
+ it('covers the whole surface when no bounds are given', () => {
6
+ const surface = createSurface(7, 5);
7
+ const r = createSurfaceRegion(surface);
8
+ expect(r).toEqual({ surface, x: 0, y: 0, width: 7, height: 5 });
9
+ });
10
+
11
+ it('uses the supplied bounds', () => {
12
+ const surface = createSurface(8, 8);
13
+ const r = createSurfaceRegion(surface, 1, 2, 3, 4);
14
+ expect(r).toEqual({ surface, x: 1, y: 2, width: 3, height: 4 });
15
+ });
16
+ });
17
+
18
+ describe('setSurfaceRegion', () => {
19
+ it('mutates the existing region without allocating a new object', () => {
20
+ const surface = createSurface(8, 8);
21
+ const r = createSurfaceRegion(surface);
22
+ const returned = setSurfaceRegion(r, surface, 2, 3, 4, 5);
23
+ expect(returned).toBe(r);
24
+ expect(r).toEqual({ surface, x: 2, y: 3, width: 4, height: 5 });
25
+ });
26
+
27
+ it('covers the whole surface when no bounds are given', () => {
28
+ const a = createSurface(2, 2);
29
+ const b = createSurface(6, 9);
30
+ const r = createSurfaceRegion(a);
31
+ setSurfaceRegion(r, b);
32
+ expect(r).toEqual({ surface: b, x: 0, y: 0, width: 6, height: 9 });
33
+ });
34
+ });
@@ -0,0 +1,112 @@
1
+ import { createSurface } from './surface';
2
+ import { getSurfacePixel } from './surfacePixel';
3
+ import { resizeSurface } from './surfaceResize';
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('resizeSurface', () => {
16
+ it('nearest upscales by pixel duplication', () => {
17
+ const source = createSurface(2, 1);
18
+ source.data.set([10, 20, 30, 255], 0);
19
+ source.data.set([40, 50, 60, 255], 4);
20
+ const out = createSurface(4, 1);
21
+ resizeSurface(region(out), region(source), 'nearest');
22
+ expect(getSurfacePixel(out, 0, 0)).toBe(getSurfacePixel(source, 0, 0));
23
+ expect(getSurfacePixel(out, 1, 0)).toBe(getSurfacePixel(source, 0, 0));
24
+ expect(getSurfacePixel(out, 2, 0)).toBe(getSurfacePixel(source, 1, 0));
25
+ expect(getSurfacePixel(out, 3, 0)).toBe(getSurfacePixel(source, 1, 0));
26
+ });
27
+
28
+ it('bilinear interpolates between source pixels', () => {
29
+ const source = createSurface(2, 1);
30
+ source.data.set([0, 0, 0, 255], 0);
31
+ source.data.set([100, 100, 100, 255], 4);
32
+ const out = createSurface(4, 1);
33
+ resizeSurface(region(out), region(source), 'bilinear');
34
+ expect(out.data[0]).toBe(0);
35
+ expect(out.data[4]).toBeGreaterThanOrEqual(out.data[0]);
36
+ expect(out.data[8]).toBeGreaterThan(out.data[4]);
37
+ expect(out.data[12]).toBe(100);
38
+ });
39
+
40
+ it('bicubic produces a smoother interpolation than bilinear', () => {
41
+ const source = createSurface(2, 1);
42
+ source.data.set([0, 0, 0, 255], 0);
43
+ source.data.set([200, 200, 200, 255], 4);
44
+ const out = createSurface(4, 1);
45
+ resizeSurface(region(out), region(source), 'bicubic');
46
+ // Bicubic should produce a monotonically increasing sequence
47
+ expect(out.data[4]).toBeGreaterThanOrEqual(out.data[0]);
48
+ expect(out.data[8]).toBeGreaterThanOrEqual(out.data[4]);
49
+ expect(out.data[12]).toBeGreaterThanOrEqual(out.data[8]);
50
+ });
51
+
52
+ it('premultiplied option prevents dark-halo bleed at transparent edges', () => {
53
+ // Red pixel adjacent to fully transparent. Straight-alpha blend mixes the
54
+ // hidden black of the transparent pixel into the output. Premultiplied
55
+ // blending keeps the red pure on the opaque side.
56
+ const source = createSurface(2, 1);
57
+ source.data.set([255, 0, 0, 255], 0); // opaque red
58
+ source.data.set([0, 0, 0, 0], 4); // transparent
59
+ const straight = createSurface(4, 1);
60
+ resizeSurface(region(straight), region(source), 'bilinear');
61
+ const premul = createSurface(4, 1);
62
+ resizeSurface(region(premul), region(source), { mode: 'bilinear', premultiplied: true });
63
+ // At the boundary (pixel 1), premultiplied keeps red higher than straight
64
+ expect(premul.data[4]).toBeGreaterThanOrEqual(straight.data[4]);
65
+ });
66
+
67
+ it('downscales by averaging toward the source values', () => {
68
+ const source = createSurface(4, 1, 0x40608000);
69
+ const out = createSurface(2, 1);
70
+ resizeSurface(region(out), region(source), 'bilinear');
71
+ expect(out.data[0]).toBe(0x40);
72
+ expect(out.data[1]).toBe(0x60);
73
+ expect(out.data[2]).toBe(0x80);
74
+ });
75
+
76
+ it('resamples a source sub-region into a dest sub-region', () => {
77
+ const source = createSurface(4, 1);
78
+ source.data.set([10, 0, 0, 255], 2 * 4); // source pixel (2,0)
79
+ const out = createSurface(4, 1);
80
+ resizeSurface(region(out, 0, 0, 2, 1), region(source, 2, 0, 1, 1), 'nearest');
81
+ expect(out.data[0]).toBe(10);
82
+ expect(out.data[4]).toBe(10);
83
+ expect(out.data[8]).toBe(0); // outside the dest sub-region
84
+ });
85
+
86
+ it("edgeMode 'wrap' wraps boundary interpolation", () => {
87
+ // 2px source [black, white]. With wrap, the right boundary wraps to the left.
88
+ const source = createSurface(2, 1);
89
+ source.data.set([0, 0, 0, 255], 0);
90
+ source.data.set([200, 200, 200, 255], 4);
91
+ const outClamp = createSurface(4, 1);
92
+ resizeSurface(region(outClamp), region(source), { mode: 'bilinear', edgeMode: 'clamp' });
93
+ const outWrap = createSurface(4, 1);
94
+ resizeSurface(region(outWrap), region(source), { mode: 'bilinear', edgeMode: 'wrap' });
95
+ // The rightmost pixel should differ: clamp repeats edge, wrap uses the other side.
96
+ expect(outClamp.data[12]).not.toBe(outWrap.data[12]);
97
+ });
98
+
99
+ it("edgeMode 'transparent' fades boundary pixels to transparent", () => {
100
+ const source = createSurface(2, 1, 0xff0000ff);
101
+ const out = createSurface(4, 1);
102
+ resizeSurface(region(out), region(source), { mode: 'bilinear', edgeMode: 'transparent' });
103
+ // The rightmost pixel samples partially out-of-bounds → alpha drops
104
+ expect(out.data[12 + 3]).toBeLessThan(255);
105
+ });
106
+
107
+ it('is a no-op when a dimension is zero', () => {
108
+ const source = createSurface(2, 2, 0xffffffff);
109
+ const out = createSurface(0, 0);
110
+ expect(() => resizeSurface(region(out), region(source))).not.toThrow();
111
+ });
112
+ });