@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,119 @@
1
+ import { createSurface } from './surface';
2
+ import { getSurfacePixel } from './surfacePixel';
3
+ import {
4
+ rotateSurface,
5
+ rotateSurface180,
6
+ rotateSurfaceClockwise,
7
+ rotateSurfaceCounterClockwise,
8
+ } from './surfaceRotate';
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
+ // 2x1 surface: pixel (0,0) = A, pixel (1,0) = B.
21
+ function ab() {
22
+ const surface = createSurface(2, 1);
23
+ surface.data.set([0xa0, 0, 0, 255], 0);
24
+ surface.data.set([0xb0, 0, 0, 255], 4);
25
+ return surface;
26
+ }
27
+
28
+ describe('rotateSurface', () => {
29
+ it('with angle 0 copies source into dest', () => {
30
+ const source = createSurface(3, 3, 0xff0000ff);
31
+ const out = createSurface(3, 3);
32
+ rotateSurface(region(out), region(source), 0);
33
+ expect(out.data[0]).toBe(0xff);
34
+ expect(out.data[3]).toBe(0xff);
35
+ });
36
+
37
+ it('with angle π reverses source similar to rotateSurface180', () => {
38
+ const source = ab();
39
+ const out = createSurface(2, 1);
40
+ rotateSurface(region(out), region(source), Math.PI);
41
+ // Bilinear introduces slight blending, so just check direction
42
+ expect(out.data[0]).toBeGreaterThan(out.data[4]); // B-side (brighter) on left
43
+ });
44
+
45
+ it('clamp edge mode (default) repeats edge pixels for out-of-bounds', () => {
46
+ const source = createSurface(1, 1, 0xff0000ff);
47
+ const out = createSurface(5, 5);
48
+ rotateSurface(region(out), region(source), 0);
49
+ // With clamp, all pixels map to the single source pixel
50
+ expect(out.data[(0 * 5 + 0) * 4 + 3]).toBe(0xff);
51
+ expect(out.data[(2 * 5 + 2) * 4 + 3]).toBe(0xff);
52
+ });
53
+
54
+ it('transparent edge mode writes transparent black for out-of-bounds', () => {
55
+ const source = createSurface(1, 1, 0xff0000ff);
56
+ const out = createSurface(5, 5);
57
+ rotateSurface(region(out), region(source), 0, undefined, undefined, 'transparent');
58
+ // Only center pixel maps back to source; corners are out of bounds
59
+ expect(out.data[(0 * 5 + 0) * 4 + 3]).toBe(0);
60
+ expect(out.data[(2 * 5 + 2) * 4 + 3]).toBe(0xff);
61
+ });
62
+
63
+ it('wrap edge mode tiles the source', () => {
64
+ const source = createSurface(2, 2, 0xff0000ff);
65
+ const out = createSurface(4, 4);
66
+ rotateSurface(region(out), region(source), 0, undefined, undefined, 'wrap');
67
+ // All pixels should be opaque red (wraps the 2x2 source)
68
+ expect(out.data[3]).toBe(0xff);
69
+ expect(out.data[(3 * 4 + 3) * 4 + 3]).toBe(0xff);
70
+ });
71
+
72
+ it('nearest sample mode uses nearest-neighbor interpolation', () => {
73
+ const source = createSurface(2, 1);
74
+ source.data.set([0, 0, 0, 255], 0);
75
+ source.data.set([200, 200, 200, 255], 4);
76
+ const out = createSurface(2, 1);
77
+ rotateSurface(region(out), region(source), 0, undefined, undefined, 'clamp', 'nearest');
78
+ // Nearest should produce exact pixel values, no blending
79
+ expect(out.data[0]).toBe(0);
80
+ expect(out.data[4]).toBe(200);
81
+ });
82
+ });
83
+
84
+ describe('rotateSurface180', () => {
85
+ it('reverses pixel order with matching dimensions', () => {
86
+ const source = ab();
87
+ const out = createSurface(2, 1);
88
+ rotateSurface180(region(out), region(source));
89
+ expect(out.data[0]).toBe(0xb0);
90
+ expect(out.data[4]).toBe(0xa0);
91
+ });
92
+
93
+ it('rotates in place when dest and source are the same region', () => {
94
+ const surface = ab();
95
+ rotateSurface180(region(surface), region(surface));
96
+ expect(surface.data[0]).toBe(0xb0);
97
+ expect(surface.data[4]).toBe(0xa0);
98
+ });
99
+ });
100
+
101
+ describe('rotateSurfaceClockwise', () => {
102
+ it('rotates a row into a column, left element on top', () => {
103
+ const source = ab();
104
+ const out = createSurface(1, 2);
105
+ rotateSurfaceClockwise(region(out), region(source));
106
+ expect(getSurfacePixel(out, 0, 0)).toBe(getSurfacePixel(source, 0, 0));
107
+ expect(getSurfacePixel(out, 0, 1)).toBe(getSurfacePixel(source, 1, 0));
108
+ });
109
+ });
110
+
111
+ describe('rotateSurfaceCounterClockwise', () => {
112
+ it('rotates a row into a column, right element on top', () => {
113
+ const source = ab();
114
+ const out = createSurface(1, 2);
115
+ rotateSurfaceCounterClockwise(region(out), region(source));
116
+ expect(getSurfacePixel(out, 0, 0)).toBe(getSurfacePixel(source, 1, 0));
117
+ expect(getSurfacePixel(out, 0, 1)).toBe(getSurfacePixel(source, 0, 0));
118
+ });
119
+ });
@@ -0,0 +1,165 @@
1
+ import { createSurface } from './surface';
2
+ import { compositeSurfacePixels, compositeSurfaceRegion } from './surfaceComposite';
3
+ import { dropShadowSurface, glowSurface, innerGlowSurface, innerShadowSurface } from './surfaceShadow';
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('dropShadowSurface', () => {
16
+ it('produces a tinted alpha mask in out', () => {
17
+ const source = createSurface(1, 1, 0xffffffff);
18
+ const out = new Uint8ClampedArray(4);
19
+ const scratch = new Uint8ClampedArray(4);
20
+ dropShadowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
21
+ expect(out[0]).toBe(0);
22
+ expect(out[2]).toBe(0xff);
23
+ expect(out[3]).toBe(0xff);
24
+ });
25
+
26
+ it('compositing out at an offset places the shadow correctly', () => {
27
+ const source = createSurface(1, 1, 0xffffffff);
28
+ const dest = createSurface(4, 4);
29
+ const out = new Uint8ClampedArray(4);
30
+ const scratch = new Uint8ClampedArray(4);
31
+ dropShadowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
32
+ const angle = (0 * Math.PI) / 180;
33
+ const offsetX = Math.round(Math.cos(angle) * 1);
34
+ const offsetY = Math.round(Math.sin(angle) * 1);
35
+ compositeSurfacePixels(region(dest, 1 + offsetX, 1 + offsetY, 1, 1), out);
36
+ const i = (1 * dest.width + 2) * 4;
37
+ expect(dest.data[i + 2]).toBe(0xff);
38
+ expect(dest.data[i + 3]).toBe(0xff);
39
+ });
40
+
41
+ it('compositing shadow then source produces source-over-shadow', () => {
42
+ const source = createSurface(1, 1, 0xff0000ff);
43
+ const dest = createSurface(2, 1);
44
+ const out = new Uint8ClampedArray(4);
45
+ const scratch = new Uint8ClampedArray(4);
46
+ dropShadowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
47
+ compositeSurfacePixels(region(dest, 1, 0, 1, 1), out);
48
+ compositeSurfaceRegion(region(dest, 0, 0, 1, 1), region(source));
49
+ expect(dest.data[0]).toBe(0xff);
50
+ expect(dest.data[3]).toBe(0xff);
51
+ });
52
+
53
+ it('source.surface.data can be used as out for a full-surface region', () => {
54
+ const surface = createSurface(1, 1, 0xffffffff);
55
+ const scratch = new Uint8ClampedArray(4);
56
+ dropShadowSurface(surface.data, scratch, region(surface), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
57
+ expect(surface.data[2]).toBe(0xff); // tinted blue
58
+ expect(surface.data[3]).toBe(0xff); // alpha carried from source
59
+ });
60
+ });
61
+
62
+ describe('glowSurface', () => {
63
+ it('produces a tinted alpha mask in out', () => {
64
+ const source = createSurface(1, 1, 0xffffffff);
65
+ const out = new Uint8ClampedArray(4);
66
+ const scratch = new Uint8ClampedArray(4);
67
+ glowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
68
+ expect(out[1]).toBe(0xff);
69
+ expect(out[3]).toBe(0xff);
70
+ });
71
+
72
+ it('compositing out places the glow at the same position', () => {
73
+ const source = createSurface(1, 1, 0xffffffff);
74
+ const dest = createSurface(1, 1);
75
+ const out = new Uint8ClampedArray(4);
76
+ const scratch = new Uint8ClampedArray(4);
77
+ glowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
78
+ compositeSurfacePixels(region(dest), out);
79
+ expect(dest.data[1]).toBe(0xff);
80
+ expect(dest.data[3]).toBe(0xff);
81
+ });
82
+
83
+ it('compositing source over glow produces source-over-glow', () => {
84
+ const source = createSurface(1, 1, 0xff0000ff);
85
+ const dest = createSurface(1, 1);
86
+ const out = new Uint8ClampedArray(4);
87
+ const scratch = new Uint8ClampedArray(4);
88
+ glowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
89
+ compositeSurfacePixels(region(dest), out);
90
+ compositeSurfaceRegion(region(dest), region(source));
91
+ expect(dest.data[0]).toBe(0xff);
92
+ expect(dest.data[3]).toBe(0xff);
93
+ });
94
+
95
+ it('source.surface.data can be used as out for a full-surface region', () => {
96
+ const surface = createSurface(1, 1, 0xffffffff);
97
+ const scratch = new Uint8ClampedArray(4);
98
+ glowSurface(surface.data, scratch, region(surface), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
99
+ expect(surface.data[1]).toBe(0xff);
100
+ expect(surface.data[3]).toBe(0xff);
101
+ });
102
+ });
103
+
104
+ describe('innerGlowSurface', () => {
105
+ it('clips the glow to inside the shape and tints it', () => {
106
+ // 3x1: transparent | opaque | transparent. The inner glow appears only on
107
+ // the opaque pixel; transparent pixels (outside the shape) stay at 0 alpha.
108
+ const source = createSurface(3, 1);
109
+ source.data[1 * 4 + 3] = 255;
110
+ const out = new Uint8ClampedArray(3 * 4);
111
+ const scratch = new Uint8ClampedArray(3 * 4);
112
+ innerGlowSurface(out, scratch, region(source), { radiusX: 2, radiusY: 0, color: 0x00ff00ff });
113
+ expect(out[0 * 4 + 3]).toBe(0);
114
+ expect(out[2 * 4 + 3]).toBe(0);
115
+ expect(out[1 * 4 + 3]).toBe(170);
116
+ expect(out[1 * 4 + 0]).toBe(0);
117
+ expect(out[1 * 4 + 1]).toBe(0xff);
118
+ expect(out[1 * 4 + 2]).toBe(0);
119
+ });
120
+
121
+ it('produces no inner glow when blur is zero', () => {
122
+ const source = createSurface(1, 1, 0x0000ffff);
123
+ const out = new Uint8ClampedArray(4);
124
+ const scratch = new Uint8ClampedArray(4);
125
+ innerGlowSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0 });
126
+ expect(out[3]).toBe(0);
127
+ });
128
+ });
129
+
130
+ describe('innerShadowSurface', () => {
131
+ it('defaults to a black tint', () => {
132
+ const source = createSurface(3, 1);
133
+ source.data[1 * 4 + 3] = 255;
134
+ const out = new Uint8ClampedArray(3 * 4);
135
+ const scratch = new Uint8ClampedArray(3 * 4);
136
+ innerShadowSurface(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
137
+ expect(out[1 * 4 + 0]).toBe(0);
138
+ expect(out[1 * 4 + 1]).toBe(0);
139
+ expect(out[1 * 4 + 2]).toBe(0);
140
+ expect(out[1 * 4 + 3]).toBe(170);
141
+ });
142
+
143
+ it('gathers the shadow toward one edge when given a directional offset', () => {
144
+ // A 3-px-wide filled bar (px 1..3) inside a 5-px row; px 0 and 4 are exterior.
145
+ const source = createSurface(5, 1);
146
+ for (let px = 1; px <= 3; px++) source.data[px * 4 + 3] = 255;
147
+ const alphaAt = (offsetX: number): number[] => {
148
+ const out = new Uint8ClampedArray(5 * 4);
149
+ const scratch = new Uint8ClampedArray(5 * 4);
150
+ innerShadowSurface(out, scratch, region(source), { offsetX, radiusX: 1, radiusY: 0 });
151
+ return [out[1 * 4 + 3], out[2 * 4 + 3], out[3 * 4 + 3]];
152
+ };
153
+ // Zero offset rings the boundary evenly: the two interior edges match.
154
+ const centered = alphaAt(0);
155
+ expect(centered[0]).toBe(centered[2]);
156
+ // A positive offset pulls the exterior in from the left, so the shadow gathers on the left edge.
157
+ const shifted = alphaAt(2);
158
+ expect(shifted[0]).toBeGreaterThan(shifted[2]);
159
+ // The opposite offset mirrors it.
160
+ const mirrored = alphaAt(-2);
161
+ expect(mirrored[2]).toBeGreaterThan(mirrored[0]);
162
+ expect(mirrored[0]).toBe(shifted[2]);
163
+ expect(mirrored[2]).toBe(shifted[0]);
164
+ });
165
+ });
@@ -0,0 +1,50 @@
1
+ import { createSurface } from './surface';
2
+ import { sharpenSurface } from './surfaceSharpen';
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('sharpenSurface', () => {
15
+ it('amount 0 returns the source RGB and preserves alpha', () => {
16
+ const source = createSurface(3, 1);
17
+ source.data.set([10, 20, 30, 120], 4);
18
+ const out = new Uint8ClampedArray(3 * 4);
19
+ const scratch = new Uint8ClampedArray(3 * 4);
20
+ sharpenSurface(out, scratch, region(source), { amount: 0 });
21
+ expect(out[4]).toBe(10);
22
+ expect(out[5]).toBe(20);
23
+ expect(out[6]).toBe(30);
24
+ expect(out[7]).toBe(120);
25
+ });
26
+
27
+ it('accentuates a bright center pixel above its original value', () => {
28
+ const source = createSurface(5, 1);
29
+ source.data[2 * 4] = 100;
30
+ for (let i = 0; i < 5; i++) source.data[i * 4 + 3] = 255;
31
+ const out = new Uint8ClampedArray(5 * 4);
32
+ const scratch = new Uint8ClampedArray(5 * 4);
33
+ sharpenSurface(out, scratch, region(source), { amount: 1, radiusX: 4, radiusY: 0 });
34
+ expect(out[2 * 4]).toBeGreaterThan(100);
35
+ });
36
+
37
+ it('preserves source alpha while sharpening RGB', () => {
38
+ const source = createSurface(3, 1);
39
+ for (let i = 0; i < 3; i++) {
40
+ source.data[i * 4] = i * 40;
41
+ source.data[i * 4 + 3] = 77;
42
+ }
43
+ const out = new Uint8ClampedArray(3 * 4);
44
+ const scratch = new Uint8ClampedArray(3 * 4);
45
+ sharpenSurface(out, scratch, region(source), { amount: 2 });
46
+ expect(out[3]).toBe(77);
47
+ expect(out[7]).toBe(77);
48
+ expect(out[11]).toBe(77);
49
+ });
50
+ });
@@ -0,0 +1,104 @@
1
+ import { createSurface } from './surface';
2
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
3
+ import { createSurfaceRegion } from './surfaceRegion';
4
+ import { applySurfaceCurve, applySurfaceLevels } from './surfaceTone';
5
+
6
+ function buildIdentityLut() {
7
+ return Uint8Array.from({ length: 256 }, (_, i) => i);
8
+ }
9
+
10
+ function buildInvertLut() {
11
+ return Uint8Array.from({ length: 256 }, (_, i) => 255 - i);
12
+ }
13
+
14
+ describe('applySurfaceCurve', () => {
15
+ it('identity LUT leaves pixels unchanged', () => {
16
+ const surf = createSurface(2, 2, 0x11223344);
17
+ const lut = buildIdentityLut();
18
+ applySurfaceCurve(createSurfaceRegion(surf), createSurfaceRegion(surf), lut, lut, lut, lut);
19
+ expect(getSurfacePixel(surf, 0, 0)).toBe(0x11223344);
20
+ });
21
+
22
+ it('invert LUT inverts RGB channels', () => {
23
+ const src = createSurface(1, 1);
24
+ setSurfacePixel(src, 0, 0, 0x00_ff_80_ff);
25
+ const dst = createSurface(1, 1);
26
+ const lut = buildInvertLut();
27
+ applySurfaceCurve(createSurfaceRegion(dst), createSurfaceRegion(src), lut, lut, lut, null);
28
+ const p = getSurfacePixel(dst, 0, 0);
29
+ expect((p >>> 24) & 0xff).toBe(0xff); // 0x00 inverted
30
+ expect((p >> 16) & 0xff).toBe(0x00); // 0xff inverted
31
+ expect((p >> 8) & 0xff).toBe(0x7f); // 0x80 inverted
32
+ expect(p & 0xff).toBe(0xff); // alpha passed through
33
+ });
34
+
35
+ it('null LUT passes channel through unchanged', () => {
36
+ const src = createSurface(1, 1, 0x80_80_80_80);
37
+ const dst = createSurface(1, 1);
38
+ const lut = buildInvertLut();
39
+ // Only invert R, leave G, B, A through.
40
+ applySurfaceCurve(createSurfaceRegion(dst), createSurfaceRegion(src), lut, null, null, null);
41
+ const p = getSurfacePixel(dst, 0, 0);
42
+ expect((p >>> 24) & 0xff).toBe(0x7f); // R inverted
43
+ expect((p >> 16) & 0xff).toBe(0x80); // G unchanged
44
+ expect((p >> 8) & 0xff).toBe(0x80); // B unchanged
45
+ expect(p & 0xff).toBe(0x80); // A unchanged
46
+ });
47
+
48
+ it('is alias-safe when out aliases source', () => {
49
+ const surf = createSurface(2, 1, 0xff_00_00_ff);
50
+ const lut = buildInvertLut();
51
+ applySurfaceCurve(createSurfaceRegion(surf), createSurfaceRegion(surf), lut, null, null, null);
52
+ // R (0xff) inverted to 0x00; others unchanged.
53
+ const p = getSurfacePixel(surf, 0, 0);
54
+ expect((p >>> 24) & 0xff).toBe(0x00);
55
+ expect((p >> 8) & 0xff).toBe(0x00);
56
+ });
57
+ });
58
+
59
+ describe('applySurfaceLevels', () => {
60
+ it('default black/white/gamma is an identity', () => {
61
+ const surf = createSurface(1, 1, 0x80_80_80_ff);
62
+ applySurfaceLevels(createSurfaceRegion(surf), createSurfaceRegion(surf));
63
+ expect((getSurfacePixel(surf, 0, 0) >>> 24) & 0xff).toBeCloseTo(0x80, -1);
64
+ });
65
+
66
+ it('black and white points clip the range', () => {
67
+ const surf = createSurface(1, 1);
68
+ setSurfacePixel(surf, 0, 0, 0x00_00_00_ff); // black pixel
69
+ applySurfaceLevels(createSurfaceRegion(surf), createSurfaceRegion(surf), 0, 128);
70
+ // A value at black point maps to 0 — stays 0.
71
+ expect((getSurfacePixel(surf, 0, 0) >>> 24) & 0xff).toBe(0);
72
+ setSurfacePixel(surf, 0, 0, 0x80_80_80_ff); // mid pixel at half of range
73
+ applySurfaceLevels(createSurfaceRegion(surf), createSurfaceRegion(surf), 0, 128);
74
+ // 128/128 = 1.0 → output 255.
75
+ expect((getSurfacePixel(surf, 0, 0) >>> 24) & 0xff).toBe(255);
76
+ });
77
+
78
+ it('gamma < 1 lightens midtones', () => {
79
+ const src = createSurface(1, 1);
80
+ setSurfacePixel(src, 0, 0, 0x80_80_80_ff);
81
+ const dst = createSurface(1, 1);
82
+ applySurfaceLevels(createSurfaceRegion(dst), createSurfaceRegion(src), 0, 255, 0.5);
83
+ // gamma=0.5 → pow(0.5, 2) = 0.25 — darker than 0.5.
84
+ const v = (getSurfacePixel(dst, 0, 0) >>> 24) & 0xff;
85
+ expect(v).toBeLessThan(0x80);
86
+ });
87
+
88
+ it('gamma > 1 darkens midtones', () => {
89
+ const src = createSurface(1, 1);
90
+ setSurfacePixel(src, 0, 0, 0x80_80_80_ff);
91
+ const dst = createSurface(1, 1);
92
+ applySurfaceLevels(createSurfaceRegion(dst), createSurfaceRegion(src), 0, 255, 2);
93
+ // gamma=2 → pow(0.5, 0.5) ≈ 0.707 → lighter.
94
+ const v = (getSurfacePixel(dst, 0, 0) >>> 24) & 0xff;
95
+ expect(v).toBeGreaterThan(0x80);
96
+ });
97
+
98
+ it('alpha channel is passed through unchanged', () => {
99
+ const src = createSurface(1, 1, 0x80_80_80_aa);
100
+ const dst = createSurface(1, 1);
101
+ applySurfaceLevels(createSurfaceRegion(dst), createSurfaceRegion(src), 0, 255, 1);
102
+ expect(getSurfacePixel(dst, 0, 0) & 0xff).toBe(0xaa);
103
+ });
104
+ });
@@ -0,0 +1,134 @@
1
+ import { createSurface } from './surface';
2
+ import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
3
+ import { applySurfaceColorTransform, applySurfaceThreshold, mergeSurface, scrollSurface } from './surfaceTransform';
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
+ const identity = {
16
+ redMultiplier: 1,
17
+ greenMultiplier: 1,
18
+ blueMultiplier: 1,
19
+ alphaMultiplier: 1,
20
+ redOffset: 0,
21
+ greenOffset: 0,
22
+ blueOffset: 0,
23
+ alphaOffset: 0,
24
+ };
25
+
26
+ describe('applySurfaceColorTransform', () => {
27
+ it('applies multiplier', () => {
28
+ const img = createSurface(2, 2, 0x808080ff);
29
+ applySurfaceColorTransform(region(img), region(img), { ...identity, redMultiplier: 0 });
30
+ expect(img.data[0]).toBe(0);
31
+ expect(img.data[1]).toBe(0x80);
32
+ });
33
+
34
+ it('applies offset', () => {
35
+ const img = createSurface(1, 1);
36
+ setSurfacePixel(img, 0, 0, 0x000000ff);
37
+ applySurfaceColorTransform(region(img), region(img), { ...identity, redOffset: 100 });
38
+ expect(img.data[0]).toBe(100);
39
+ });
40
+
41
+ it('clamps to 0-255', () => {
42
+ const img = createSurface(1, 1, 0x808080ff);
43
+ applySurfaceColorTransform(region(img), region(img), { ...identity, redMultiplier: 10, redOffset: 100 });
44
+ expect(img.data[0]).toBe(255);
45
+ applySurfaceColorTransform(region(img), region(img), { ...identity, redMultiplier: 0, redOffset: -100 });
46
+ expect(img.data[0]).toBe(0);
47
+ });
48
+
49
+ it('only affects the specified rect', () => {
50
+ const img = createSurface(2, 2, 0x808080ff);
51
+ applySurfaceColorTransform(region(img, 0, 0, 1, 1), region(img, 0, 0, 1, 1), { ...identity, redMultiplier: 0 });
52
+ expect(img.data[0]).toBe(0);
53
+ expect(img.data[4]).toBe(0x80);
54
+ });
55
+
56
+ it('can write to a separate output surface', () => {
57
+ const source = createSurface(1, 1, 0x808080ff);
58
+ const out = createSurface(1, 1);
59
+ applySurfaceColorTransform(region(out), region(source), { ...identity, redMultiplier: 0 });
60
+ expect(out.data[0]).toBe(0);
61
+ expect(out.data[1]).toBe(0x80);
62
+ expect(source.data[0]).toBe(0x80); // source unchanged
63
+ });
64
+ });
65
+
66
+ describe('applySurfaceThreshold', () => {
67
+ it('replaces pixels that pass the test', () => {
68
+ const src = createSurface(2, 1);
69
+ setSurfacePixel(src, 0, 0, 0x808080ff);
70
+ setSurfacePixel(src, 1, 0, 0x404040ff);
71
+ const dst = createSurface(2, 1);
72
+ const count = applySurfaceThreshold(region(dst), region(src), '>', 0x607060ff, 0xffffffff);
73
+ expect(count).toBe(1);
74
+ expect(getSurfacePixel(dst, 0, 0)).toBe(0xffffffff);
75
+ expect(getSurfacePixel(dst, 1, 0)).toBe(0x00000000);
76
+ });
77
+
78
+ it('copies source when copySource is true and test fails', () => {
79
+ const src = createSurface(1, 1, 0x112233ff);
80
+ const dst = createSurface(1, 1);
81
+ applySurfaceThreshold(region(dst), region(src), '>', 0xffffffff, 0xffffffff, 0xffffffff, true);
82
+ expect(getSurfacePixel(dst, 0, 0)).toBe(0x112233ff);
83
+ });
84
+
85
+ it('returns zero when no pixels pass', () => {
86
+ const src = createSurface(2, 2, 0x000000ff);
87
+ const dst = createSurface(2, 2);
88
+ const count = applySurfaceThreshold(region(dst), region(src), '>', 0xffffffff);
89
+ expect(count).toBe(0);
90
+ });
91
+ });
92
+
93
+ describe('mergeSurface', () => {
94
+ it('with multiplier 1 copies source', () => {
95
+ const src = createSurface(1, 1, 0xff0000ff);
96
+ const dst = createSurface(1, 1, 0x0000ffff);
97
+ mergeSurface(region(dst), region(src), 1, 1, 1, 1);
98
+ expect(dst.data[0]).toBe(0xff);
99
+ expect(dst.data[2]).toBe(0x00);
100
+ });
101
+
102
+ it('with multiplier 0 keeps destination', () => {
103
+ const src = createSurface(1, 1, 0xff0000ff);
104
+ const dst = createSurface(1, 1, 0x0000ffff);
105
+ mergeSurface(region(dst), region(src), 0, 0, 0, 0);
106
+ expect(dst.data[0]).toBe(0x00);
107
+ expect(dst.data[2]).toBe(0xff);
108
+ });
109
+
110
+ it('with multiplier 0.5 blends evenly', () => {
111
+ const src = createSurface(1, 1, 0x200000ff);
112
+ const dst = createSurface(1, 1, 0x000020ff);
113
+ mergeSurface(region(dst), region(src), 0.5, 0, 0.5, 0);
114
+ expect(dst.data[0]).toBeCloseTo(16, 0);
115
+ expect(dst.data[2]).toBeCloseTo(16, 0);
116
+ });
117
+ });
118
+
119
+ describe('scrollSurface', () => {
120
+ it('shifts content right with wrapping', () => {
121
+ const img = createSurface(4, 1);
122
+ setSurfacePixel(img, 0, 0, 0xff0000ff);
123
+ scrollSurface(img, 1, 0);
124
+ expect(getSurfacePixel(img, 1, 0)).toBe(0xff0000ff);
125
+ expect(getSurfacePixel(img, 0, 0)).toBe(0x00000000);
126
+ });
127
+
128
+ it('wraps pixels around the edge', () => {
129
+ const img = createSurface(4, 1);
130
+ setSurfacePixel(img, 3, 0, 0xaabbccff);
131
+ scrollSurface(img, 1, 0);
132
+ expect(getSurfacePixel(img, 0, 0)).toBe(0xaabbccff);
133
+ });
134
+ });