@flighthq/bitmap 0.3.0-edge.1458.0c01b63

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 (219) hide show
  1. package/dist/bitmap.d.ts +17 -0
  2. package/dist/bitmap.d.ts.map +1 -0
  3. package/dist/bitmap.js +87 -0
  4. package/dist/bitmap.js.map +1 -0
  5. package/dist/bitmapAffine.d.ts +27 -0
  6. package/dist/bitmapAffine.d.ts.map +1 -0
  7. package/dist/bitmapAffine.js +159 -0
  8. package/dist/bitmapAffine.js.map +1 -0
  9. package/dist/bitmapAlpha.d.ts +27 -0
  10. package/dist/bitmapAlpha.d.ts.map +1 -0
  11. package/dist/bitmapAlpha.js +83 -0
  12. package/dist/bitmapAlpha.js.map +1 -0
  13. package/dist/bitmapBevel.d.ts +24 -0
  14. package/dist/bitmapBevel.d.ts.map +1 -0
  15. package/dist/bitmapBevel.js +102 -0
  16. package/dist/bitmapBevel.js.map +1 -0
  17. package/dist/bitmapBlur.d.ts +57 -0
  18. package/dist/bitmapBlur.d.ts.map +1 -0
  19. package/dist/bitmapBlur.js +263 -0
  20. package/dist/bitmapBlur.js.map +1 -0
  21. package/dist/bitmapChannel.d.ts +24 -0
  22. package/dist/bitmapChannel.d.ts.map +1 -0
  23. package/dist/bitmapChannel.js +127 -0
  24. package/dist/bitmapChannel.js.map +1 -0
  25. package/dist/bitmapColorMatrix.d.ts +57 -0
  26. package/dist/bitmapColorMatrix.d.ts.map +1 -0
  27. package/dist/bitmapColorMatrix.js +127 -0
  28. package/dist/bitmapColorMatrix.js.map +1 -0
  29. package/dist/bitmapCompare.d.ts +18 -0
  30. package/dist/bitmapCompare.d.ts.map +1 -0
  31. package/dist/bitmapCompare.js +64 -0
  32. package/dist/bitmapCompare.js.map +1 -0
  33. package/dist/bitmapComposite.d.ts +54 -0
  34. package/dist/bitmapComposite.d.ts.map +1 -0
  35. package/dist/bitmapComposite.js +262 -0
  36. package/dist/bitmapComposite.js.map +1 -0
  37. package/dist/bitmapConvolution.d.ts +17 -0
  38. package/dist/bitmapConvolution.d.ts.map +1 -0
  39. package/dist/bitmapConvolution.js +103 -0
  40. package/dist/bitmapConvolution.js.map +1 -0
  41. package/dist/bitmapCopy.d.ts +23 -0
  42. package/dist/bitmapCopy.d.ts.map +1 -0
  43. package/dist/bitmapCopy.js +78 -0
  44. package/dist/bitmapCopy.js.map +1 -0
  45. package/dist/bitmapCoverage.d.ts +11 -0
  46. package/dist/bitmapCoverage.d.ts.map +1 -0
  47. package/dist/bitmapCoverage.js +29 -0
  48. package/dist/bitmapCoverage.js.map +1 -0
  49. package/dist/bitmapCrop.d.ts +33 -0
  50. package/dist/bitmapCrop.d.ts.map +1 -0
  51. package/dist/bitmapCrop.js +175 -0
  52. package/dist/bitmapCrop.js.map +1 -0
  53. package/dist/bitmapDisplacement.d.ts +22 -0
  54. package/dist/bitmapDisplacement.d.ts.map +1 -0
  55. package/dist/bitmapDisplacement.js +111 -0
  56. package/dist/bitmapDisplacement.js.map +1 -0
  57. package/dist/bitmapDissolve.d.ts +21 -0
  58. package/dist/bitmapDissolve.d.ts.map +1 -0
  59. package/dist/bitmapDissolve.js +97 -0
  60. package/dist/bitmapDissolve.js.map +1 -0
  61. package/dist/bitmapDraw.d.ts +8 -0
  62. package/dist/bitmapDraw.d.ts.map +1 -0
  63. package/dist/bitmapDraw.js +14 -0
  64. package/dist/bitmapDraw.js.map +1 -0
  65. package/dist/bitmapEncode.d.ts +3 -0
  66. package/dist/bitmapEncode.d.ts.map +1 -0
  67. package/dist/bitmapEncode.js +18 -0
  68. package/dist/bitmapEncode.js.map +1 -0
  69. package/dist/bitmapFill.d.ts +12 -0
  70. package/dist/bitmapFill.d.ts.map +1 -0
  71. package/dist/bitmapFill.js +85 -0
  72. package/dist/bitmapFill.js.map +1 -0
  73. package/dist/bitmapFingerprint.d.ts +26 -0
  74. package/dist/bitmapFingerprint.d.ts.map +1 -0
  75. package/dist/bitmapFingerprint.js +99 -0
  76. package/dist/bitmapFingerprint.js.map +1 -0
  77. package/dist/bitmapFlip.d.ts +16 -0
  78. package/dist/bitmapFlip.d.ts.map +1 -0
  79. package/dist/bitmapFlip.js +98 -0
  80. package/dist/bitmapFlip.js.map +1 -0
  81. package/dist/bitmapFormat.d.ts +26 -0
  82. package/dist/bitmapFormat.d.ts.map +1 -0
  83. package/dist/bitmapFormat.js +80 -0
  84. package/dist/bitmapFormat.js.map +1 -0
  85. package/dist/bitmapFrom.d.ts +23 -0
  86. package/dist/bitmapFrom.d.ts.map +1 -0
  87. package/dist/bitmapFrom.js +90 -0
  88. package/dist/bitmapFrom.js.map +1 -0
  89. package/dist/bitmapGradient.d.ts +36 -0
  90. package/dist/bitmapGradient.d.ts.map +1 -0
  91. package/dist/bitmapGradient.js +176 -0
  92. package/dist/bitmapGradient.js.map +1 -0
  93. package/dist/bitmapGradientFill.d.ts +32 -0
  94. package/dist/bitmapGradientFill.d.ts.map +1 -0
  95. package/dist/bitmapGradientFill.js +120 -0
  96. package/dist/bitmapGradientFill.js.map +1 -0
  97. package/dist/bitmapHistogram.d.ts +20 -0
  98. package/dist/bitmapHistogram.d.ts.map +1 -0
  99. package/dist/bitmapHistogram.js +61 -0
  100. package/dist/bitmapHistogram.js.map +1 -0
  101. package/dist/bitmapImageChannel.d.ts +2 -0
  102. package/dist/bitmapImageChannel.d.ts.map +1 -0
  103. package/dist/bitmapImageChannel.js +2 -0
  104. package/dist/bitmapImageChannel.js.map +1 -0
  105. package/dist/bitmapMedian.d.ts +14 -0
  106. package/dist/bitmapMedian.d.ts.map +1 -0
  107. package/dist/bitmapMedian.js +77 -0
  108. package/dist/bitmapMedian.js.map +1 -0
  109. package/dist/bitmapMorphological.d.ts +29 -0
  110. package/dist/bitmapMorphological.d.ts.map +1 -0
  111. package/dist/bitmapMorphological.js +81 -0
  112. package/dist/bitmapMorphological.js.map +1 -0
  113. package/dist/bitmapNoise.d.ts +48 -0
  114. package/dist/bitmapNoise.d.ts.map +1 -0
  115. package/dist/bitmapNoise.js +245 -0
  116. package/dist/bitmapNoise.js.map +1 -0
  117. package/dist/bitmapPaletteMap.d.ts +17 -0
  118. package/dist/bitmapPaletteMap.d.ts.map +1 -0
  119. package/dist/bitmapPaletteMap.js +45 -0
  120. package/dist/bitmapPaletteMap.js.map +1 -0
  121. package/dist/bitmapPixel.d.ts +9 -0
  122. package/dist/bitmapPixel.d.ts.map +1 -0
  123. package/dist/bitmapPixel.js +36 -0
  124. package/dist/bitmapPixel.js.map +1 -0
  125. package/dist/bitmapPixelate.d.ts +13 -0
  126. package/dist/bitmapPixelate.d.ts.map +1 -0
  127. package/dist/bitmapPixelate.js +61 -0
  128. package/dist/bitmapPixelate.js.map +1 -0
  129. package/dist/bitmapQuery.d.ts +13 -0
  130. package/dist/bitmapQuery.d.ts.map +1 -0
  131. package/dist/bitmapQuery.js +46 -0
  132. package/dist/bitmapQuery.js.map +1 -0
  133. package/dist/bitmapRegion.d.ts +20 -0
  134. package/dist/bitmapRegion.d.ts.map +1 -0
  135. package/dist/bitmapRegion.js +28 -0
  136. package/dist/bitmapRegion.js.map +1 -0
  137. package/dist/bitmapResize.d.ts +17 -0
  138. package/dist/bitmapResize.d.ts.map +1 -0
  139. package/dist/bitmapResize.js +191 -0
  140. package/dist/bitmapResize.js.map +1 -0
  141. package/dist/bitmapRotate.d.ts +32 -0
  142. package/dist/bitmapRotate.d.ts.map +1 -0
  143. package/dist/bitmapRotate.js +143 -0
  144. package/dist/bitmapRotate.js.map +1 -0
  145. package/dist/bitmapShadow.d.ts +72 -0
  146. package/dist/bitmapShadow.d.ts.map +1 -0
  147. package/dist/bitmapShadow.js +175 -0
  148. package/dist/bitmapShadow.js.map +1 -0
  149. package/dist/bitmapSharpen.d.ts +16 -0
  150. package/dist/bitmapSharpen.d.ts.map +1 -0
  151. package/dist/bitmapSharpen.js +47 -0
  152. package/dist/bitmapSharpen.js.map +1 -0
  153. package/dist/bitmapTone.d.ts +28 -0
  154. package/dist/bitmapTone.d.ts.map +1 -0
  155. package/dist/bitmapTone.js +69 -0
  156. package/dist/bitmapTone.js.map +1 -0
  157. package/dist/bitmapTransform.d.ts +40 -0
  158. package/dist/bitmapTransform.d.ts.map +1 -0
  159. package/dist/bitmapTransform.js +163 -0
  160. package/dist/bitmapTransform.js.map +1 -0
  161. package/dist/bitmapWarp.d.ts +48 -0
  162. package/dist/bitmapWarp.d.ts.map +1 -0
  163. package/dist/bitmapWarp.js +284 -0
  164. package/dist/bitmapWarp.js.map +1 -0
  165. package/dist/contract.d.ts +43 -0
  166. package/dist/contract.d.ts.map +1 -0
  167. package/dist/contract.js +43 -0
  168. package/dist/contract.js.map +1 -0
  169. package/dist/explainBitmapReadback.d.ts +3 -0
  170. package/dist/explainBitmapReadback.d.ts.map +1 -0
  171. package/dist/explainBitmapReadback.js +39 -0
  172. package/dist/explainBitmapReadback.js.map +1 -0
  173. package/dist/index.d.ts +2 -0
  174. package/dist/index.d.ts.map +1 -0
  175. package/dist/index.js +2 -0
  176. package/dist/index.js.map +1 -0
  177. package/package.json +48 -0
  178. package/src/bitmap.test.ts +88 -0
  179. package/src/bitmapAffine.test.ts +98 -0
  180. package/src/bitmapAlpha.test.ts +110 -0
  181. package/src/bitmapBevel.test.ts +61 -0
  182. package/src/bitmapBlur.test.ts +234 -0
  183. package/src/bitmapChannel.test.ts +97 -0
  184. package/src/bitmapColorMatrix.test.ts +214 -0
  185. package/src/bitmapCompare.test.ts +92 -0
  186. package/src/bitmapComposite.test.ts +259 -0
  187. package/src/bitmapConvolution.test.ts +130 -0
  188. package/src/bitmapCopy.test.ts +48 -0
  189. package/src/bitmapCoverage.test.ts +39 -0
  190. package/src/bitmapCrop.test.ts +116 -0
  191. package/src/bitmapDisplacement.test.ts +181 -0
  192. package/src/bitmapDissolve.test.ts +99 -0
  193. package/src/bitmapDraw.test.ts +68 -0
  194. package/src/bitmapEncode.test.ts +18 -0
  195. package/src/bitmapFill.test.ts +83 -0
  196. package/src/bitmapFingerprint.test.ts +103 -0
  197. package/src/bitmapFlip.test.ts +53 -0
  198. package/src/bitmapFormat.test.ts +124 -0
  199. package/src/bitmapFrom.test.ts +86 -0
  200. package/src/bitmapGradient.test.ts +85 -0
  201. package/src/bitmapGradientFill.test.ts +107 -0
  202. package/src/bitmapHistogram.test.ts +64 -0
  203. package/src/bitmapImageChannel.test.ts +19 -0
  204. package/src/bitmapMedian.test.ts +39 -0
  205. package/src/bitmapMorphological.test.ts +69 -0
  206. package/src/bitmapNoise.test.ts +163 -0
  207. package/src/bitmapPaletteMap.test.ts +49 -0
  208. package/src/bitmapPixel.test.ts +117 -0
  209. package/src/bitmapPixelate.test.ts +40 -0
  210. package/src/bitmapQuery.test.ts +42 -0
  211. package/src/bitmapRegion.test.ts +34 -0
  212. package/src/bitmapResize.test.ts +106 -0
  213. package/src/bitmapRotate.test.ts +108 -0
  214. package/src/bitmapShadow.test.ts +159 -0
  215. package/src/bitmapSharpen.test.ts +44 -0
  216. package/src/bitmapTone.test.ts +104 -0
  217. package/src/bitmapTransform.test.ts +128 -0
  218. package/src/bitmapWarp.test.ts +118 -0
  219. package/src/explainBitmapReadback.test.ts +84 -0
@@ -0,0 +1,159 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { compositeBitmapPixels, compositeBitmapRegion } from './bitmapComposite';
3
+ import { dropShadowBitmap, glowBitmap, innerGlowBitmap, innerShadowBitmap } from './bitmapShadow';
4
+
5
+ function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
6
+ return { bitmap, x, y, width, height };
7
+ }
8
+
9
+ describe('dropShadowBitmap', () => {
10
+ it('produces a tinted alpha mask in out', () => {
11
+ const source = createBitmap(1, 1, 0xffffffff);
12
+ const out = new Uint8ClampedArray(4);
13
+ const scratch = new Uint8ClampedArray(4);
14
+ dropShadowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
15
+ expect(out[0]).toBe(0);
16
+ expect(out[2]).toBe(0xff);
17
+ expect(out[3]).toBe(0xff);
18
+ });
19
+
20
+ it('compositing out at an offset places the shadow correctly', () => {
21
+ const source = createBitmap(1, 1, 0xffffffff);
22
+ const dest = createBitmap(4, 4);
23
+ const out = new Uint8ClampedArray(4);
24
+ const scratch = new Uint8ClampedArray(4);
25
+ dropShadowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
26
+ const angle = (0 * Math.PI) / 180;
27
+ const offsetX = Math.round(Math.cos(angle) * 1);
28
+ const offsetY = Math.round(Math.sin(angle) * 1);
29
+ compositeBitmapPixels(region(dest, 1 + offsetX, 1 + offsetY, 1, 1), out);
30
+ const i = (1 * dest.width + 2) * 4;
31
+ expect(dest.data[i + 2]).toBe(0xff);
32
+ expect(dest.data[i + 3]).toBe(0xff);
33
+ });
34
+
35
+ it('compositing shadow then source produces source-over-shadow', () => {
36
+ const source = createBitmap(1, 1, 0xff0000ff);
37
+ const dest = createBitmap(2, 1);
38
+ const out = new Uint8ClampedArray(4);
39
+ const scratch = new Uint8ClampedArray(4);
40
+ dropShadowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
41
+ compositeBitmapPixels(region(dest, 1, 0, 1, 1), out);
42
+ compositeBitmapRegion(region(dest, 0, 0, 1, 1), region(source));
43
+ expect(dest.data[0]).toBe(0xff);
44
+ expect(dest.data[3]).toBe(0xff);
45
+ });
46
+
47
+ it('source.bitmap.data can be used as out for a full-bitmap region', () => {
48
+ const bitmap = createBitmap(1, 1, 0xffffffff);
49
+ const scratch = new Uint8ClampedArray(4);
50
+ dropShadowBitmap(bitmap.data, scratch, region(bitmap), { radiusX: 0, radiusY: 0, color: 0x0000ffff });
51
+ expect(bitmap.data[2]).toBe(0xff); // tinted blue
52
+ expect(bitmap.data[3]).toBe(0xff); // alpha carried from source
53
+ });
54
+ });
55
+
56
+ describe('glowBitmap', () => {
57
+ it('produces a tinted alpha mask in out', () => {
58
+ const source = createBitmap(1, 1, 0xffffffff);
59
+ const out = new Uint8ClampedArray(4);
60
+ const scratch = new Uint8ClampedArray(4);
61
+ glowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
62
+ expect(out[1]).toBe(0xff);
63
+ expect(out[3]).toBe(0xff);
64
+ });
65
+
66
+ it('compositing out places the glow at the same position', () => {
67
+ const source = createBitmap(1, 1, 0xffffffff);
68
+ const dest = createBitmap(1, 1);
69
+ const out = new Uint8ClampedArray(4);
70
+ const scratch = new Uint8ClampedArray(4);
71
+ glowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
72
+ compositeBitmapPixels(region(dest), out);
73
+ expect(dest.data[1]).toBe(0xff);
74
+ expect(dest.data[3]).toBe(0xff);
75
+ });
76
+
77
+ it('compositing source over glow produces source-over-glow', () => {
78
+ const source = createBitmap(1, 1, 0xff0000ff);
79
+ const dest = createBitmap(1, 1);
80
+ const out = new Uint8ClampedArray(4);
81
+ const scratch = new Uint8ClampedArray(4);
82
+ glowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
83
+ compositeBitmapPixels(region(dest), out);
84
+ compositeBitmapRegion(region(dest), region(source));
85
+ expect(dest.data[0]).toBe(0xff);
86
+ expect(dest.data[3]).toBe(0xff);
87
+ });
88
+
89
+ it('source.bitmap.data can be used as out for a full-bitmap region', () => {
90
+ const bitmap = createBitmap(1, 1, 0xffffffff);
91
+ const scratch = new Uint8ClampedArray(4);
92
+ glowBitmap(bitmap.data, scratch, region(bitmap), { radiusX: 0, radiusY: 0, color: 0x00ff00ff });
93
+ expect(bitmap.data[1]).toBe(0xff);
94
+ expect(bitmap.data[3]).toBe(0xff);
95
+ });
96
+ });
97
+
98
+ describe('innerGlowBitmap', () => {
99
+ it('clips the glow to inside the shape and tints it', () => {
100
+ // 3x1: transparent | opaque | transparent. The inner glow appears only on
101
+ // the opaque pixel; transparent pixels (outside the shape) stay at 0 alpha.
102
+ const source = createBitmap(3, 1);
103
+ source.data[1 * 4 + 3] = 255;
104
+ const out = new Uint8ClampedArray(3 * 4);
105
+ const scratch = new Uint8ClampedArray(3 * 4);
106
+ innerGlowBitmap(out, scratch, region(source), { radiusX: 2, radiusY: 0, color: 0x00ff00ff });
107
+ expect(out[0 * 4 + 3]).toBe(0);
108
+ expect(out[2 * 4 + 3]).toBe(0);
109
+ expect(out[1 * 4 + 3]).toBe(170);
110
+ expect(out[1 * 4 + 0]).toBe(0);
111
+ expect(out[1 * 4 + 1]).toBe(0xff);
112
+ expect(out[1 * 4 + 2]).toBe(0);
113
+ });
114
+
115
+ it('produces no inner glow when blur is zero', () => {
116
+ const source = createBitmap(1, 1, 0x0000ffff);
117
+ const out = new Uint8ClampedArray(4);
118
+ const scratch = new Uint8ClampedArray(4);
119
+ innerGlowBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0 });
120
+ expect(out[3]).toBe(0);
121
+ });
122
+ });
123
+
124
+ describe('innerShadowBitmap', () => {
125
+ it('defaults to a black tint', () => {
126
+ const source = createBitmap(3, 1);
127
+ source.data[1 * 4 + 3] = 255;
128
+ const out = new Uint8ClampedArray(3 * 4);
129
+ const scratch = new Uint8ClampedArray(3 * 4);
130
+ innerShadowBitmap(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
131
+ expect(out[1 * 4 + 0]).toBe(0);
132
+ expect(out[1 * 4 + 1]).toBe(0);
133
+ expect(out[1 * 4 + 2]).toBe(0);
134
+ expect(out[1 * 4 + 3]).toBe(170);
135
+ });
136
+
137
+ it('gathers the shadow toward one edge when given a directional offset', () => {
138
+ // A 3-px-wide filled bar (px 1..3) inside a 5-px row; px 0 and 4 are exterior.
139
+ const source = createBitmap(5, 1);
140
+ for (let px = 1; px <= 3; px++) source.data[px * 4 + 3] = 255;
141
+ const alphaAt = (offsetX: number): number[] => {
142
+ const out = new Uint8ClampedArray(5 * 4);
143
+ const scratch = new Uint8ClampedArray(5 * 4);
144
+ innerShadowBitmap(out, scratch, region(source), { offsetX, radiusX: 1, radiusY: 0 });
145
+ return [out[1 * 4 + 3], out[2 * 4 + 3], out[3 * 4 + 3]];
146
+ };
147
+ // Zero offset rings the boundary evenly: the two interior edges match.
148
+ const centered = alphaAt(0);
149
+ expect(centered[0]).toBe(centered[2]);
150
+ // A positive offset pulls the exterior in from the left, so the shadow gathers on the left edge.
151
+ const shifted = alphaAt(2);
152
+ expect(shifted[0]).toBeGreaterThan(shifted[2]);
153
+ // The opposite offset mirrors it.
154
+ const mirrored = alphaAt(-2);
155
+ expect(mirrored[2]).toBeGreaterThan(mirrored[0]);
156
+ expect(mirrored[0]).toBe(shifted[2]);
157
+ expect(mirrored[2]).toBe(shifted[0]);
158
+ });
159
+ });
@@ -0,0 +1,44 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { sharpenBitmap } from './bitmapSharpen';
3
+
4
+ function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
5
+ return { bitmap, x, y, width, height };
6
+ }
7
+
8
+ describe('sharpenBitmap', () => {
9
+ it('amount 0 returns the source RGB and preserves alpha', () => {
10
+ const source = createBitmap(3, 1);
11
+ source.data.set([10, 20, 30, 120], 4);
12
+ const out = new Uint8ClampedArray(3 * 4);
13
+ const scratch = new Uint8ClampedArray(3 * 4);
14
+ sharpenBitmap(out, scratch, region(source), { amount: 0 });
15
+ expect(out[4]).toBe(10);
16
+ expect(out[5]).toBe(20);
17
+ expect(out[6]).toBe(30);
18
+ expect(out[7]).toBe(120);
19
+ });
20
+
21
+ it('accentuates a bright center pixel above its original value', () => {
22
+ const source = createBitmap(5, 1);
23
+ source.data[2 * 4] = 100;
24
+ for (let i = 0; i < 5; i++) source.data[i * 4 + 3] = 255;
25
+ const out = new Uint8ClampedArray(5 * 4);
26
+ const scratch = new Uint8ClampedArray(5 * 4);
27
+ sharpenBitmap(out, scratch, region(source), { amount: 1, radiusX: 4, radiusY: 0 });
28
+ expect(out[2 * 4]).toBeGreaterThan(100);
29
+ });
30
+
31
+ it('preserves source alpha while sharpening RGB', () => {
32
+ const source = createBitmap(3, 1);
33
+ for (let i = 0; i < 3; i++) {
34
+ source.data[i * 4] = i * 40;
35
+ source.data[i * 4 + 3] = 77;
36
+ }
37
+ const out = new Uint8ClampedArray(3 * 4);
38
+ const scratch = new Uint8ClampedArray(3 * 4);
39
+ sharpenBitmap(out, scratch, region(source), { amount: 2 });
40
+ expect(out[3]).toBe(77);
41
+ expect(out[7]).toBe(77);
42
+ expect(out[11]).toBe(77);
43
+ });
44
+ });
@@ -0,0 +1,104 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
3
+ import { createBitmapRegion } from './bitmapRegion';
4
+ import { applyBitmapCurve, applyBitmapLevels } from './bitmapTone';
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('applyBitmapCurve', () => {
15
+ it('identity LUT leaves pixels unchanged', () => {
16
+ const surf = createBitmap(2, 2, 0x11223344);
17
+ const lut = buildIdentityLut();
18
+ applyBitmapCurve(createBitmapRegion(surf), createBitmapRegion(surf), lut, lut, lut, lut);
19
+ expect(getBitmapPixel(surf, 0, 0)).toBe(0x11223344);
20
+ });
21
+
22
+ it('invert LUT inverts RGB channels', () => {
23
+ const src = createBitmap(1, 1);
24
+ setBitmapPixel(src, 0, 0, 0x00_ff_80_ff);
25
+ const dst = createBitmap(1, 1);
26
+ const lut = buildInvertLut();
27
+ applyBitmapCurve(createBitmapRegion(dst), createBitmapRegion(src), lut, lut, lut, null);
28
+ const p = getBitmapPixel(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 = createBitmap(1, 1, 0x80_80_80_80);
37
+ const dst = createBitmap(1, 1);
38
+ const lut = buildInvertLut();
39
+ // Only invert R, leave G, B, A through.
40
+ applyBitmapCurve(createBitmapRegion(dst), createBitmapRegion(src), lut, null, null, null);
41
+ const p = getBitmapPixel(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 = createBitmap(2, 1, 0xff_00_00_ff);
50
+ const lut = buildInvertLut();
51
+ applyBitmapCurve(createBitmapRegion(surf), createBitmapRegion(surf), lut, null, null, null);
52
+ // R (0xff) inverted to 0x00; others unchanged.
53
+ const p = getBitmapPixel(surf, 0, 0);
54
+ expect((p >>> 24) & 0xff).toBe(0x00);
55
+ expect((p >> 8) & 0xff).toBe(0x00);
56
+ });
57
+ });
58
+
59
+ describe('applyBitmapLevels', () => {
60
+ it('default black/white/gamma is an identity', () => {
61
+ const surf = createBitmap(1, 1, 0x80_80_80_ff);
62
+ applyBitmapLevels(createBitmapRegion(surf), createBitmapRegion(surf));
63
+ expect((getBitmapPixel(surf, 0, 0) >>> 24) & 0xff).toBeCloseTo(0x80, -1);
64
+ });
65
+
66
+ it('black and white points clip the range', () => {
67
+ const surf = createBitmap(1, 1);
68
+ setBitmapPixel(surf, 0, 0, 0x00_00_00_ff); // black pixel
69
+ applyBitmapLevels(createBitmapRegion(surf), createBitmapRegion(surf), 0, 128);
70
+ // A value at black point maps to 0 — stays 0.
71
+ expect((getBitmapPixel(surf, 0, 0) >>> 24) & 0xff).toBe(0);
72
+ setBitmapPixel(surf, 0, 0, 0x80_80_80_ff); // mid pixel at half of range
73
+ applyBitmapLevels(createBitmapRegion(surf), createBitmapRegion(surf), 0, 128);
74
+ // 128/128 = 1.0 → output 255.
75
+ expect((getBitmapPixel(surf, 0, 0) >>> 24) & 0xff).toBe(255);
76
+ });
77
+
78
+ it('gamma < 1 lightens midtones', () => {
79
+ const src = createBitmap(1, 1);
80
+ setBitmapPixel(src, 0, 0, 0x80_80_80_ff);
81
+ const dst = createBitmap(1, 1);
82
+ applyBitmapLevels(createBitmapRegion(dst), createBitmapRegion(src), 0, 255, 0.5);
83
+ // gamma=0.5 → pow(0.5, 2) = 0.25 — darker than 0.5.
84
+ const v = (getBitmapPixel(dst, 0, 0) >>> 24) & 0xff;
85
+ expect(v).toBeLessThan(0x80);
86
+ });
87
+
88
+ it('gamma > 1 darkens midtones', () => {
89
+ const src = createBitmap(1, 1);
90
+ setBitmapPixel(src, 0, 0, 0x80_80_80_ff);
91
+ const dst = createBitmap(1, 1);
92
+ applyBitmapLevels(createBitmapRegion(dst), createBitmapRegion(src), 0, 255, 2);
93
+ // gamma=2 → pow(0.5, 0.5) ≈ 0.707 → lighter.
94
+ const v = (getBitmapPixel(dst, 0, 0) >>> 24) & 0xff;
95
+ expect(v).toBeGreaterThan(0x80);
96
+ });
97
+
98
+ it('alpha channel is passed through unchanged', () => {
99
+ const src = createBitmap(1, 1, 0x80_80_80_aa);
100
+ const dst = createBitmap(1, 1);
101
+ applyBitmapLevels(createBitmapRegion(dst), createBitmapRegion(src), 0, 255, 1);
102
+ expect(getBitmapPixel(dst, 0, 0) & 0xff).toBe(0xaa);
103
+ });
104
+ });
@@ -0,0 +1,128 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
3
+ import { applyBitmapColorScaleBias, applyBitmapThreshold, mergeBitmap, scrollBitmap } from './bitmapTransform';
4
+
5
+ function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
6
+ return { bitmap, x, y, width, height };
7
+ }
8
+
9
+ const identity = {
10
+ redScale: 1,
11
+ greenScale: 1,
12
+ blueScale: 1,
13
+ alphaScale: 1,
14
+ redBias: 0,
15
+ greenBias: 0,
16
+ blueBias: 0,
17
+ alphaBias: 0,
18
+ };
19
+
20
+ describe('applyBitmapColorScaleBias', () => {
21
+ it('applies scale', () => {
22
+ const img = createBitmap(2, 2, 0x808080ff);
23
+ applyBitmapColorScaleBias(region(img), region(img), { ...identity, redScale: 0 });
24
+ expect(img.data[0]).toBe(0);
25
+ expect(img.data[1]).toBe(0x80);
26
+ });
27
+
28
+ it('applies normalized-linear bias', () => {
29
+ const img = createBitmap(1, 1);
30
+ setBitmapPixel(img, 0, 0, 0x000000ff);
31
+ applyBitmapColorScaleBias(region(img), region(img), { ...identity, redBias: 100 / 255 });
32
+ expect(img.data[0]).toBe(100);
33
+ });
34
+
35
+ it('clamps to 0-255', () => {
36
+ const img = createBitmap(1, 1, 0x808080ff);
37
+ applyBitmapColorScaleBias(region(img), region(img), { ...identity, redScale: 10, redBias: 100 / 255 });
38
+ expect(img.data[0]).toBe(255);
39
+ applyBitmapColorScaleBias(region(img), region(img), { ...identity, redScale: 0, redBias: -100 / 255 });
40
+ expect(img.data[0]).toBe(0);
41
+ });
42
+
43
+ it('only affects the specified rect', () => {
44
+ const img = createBitmap(2, 2, 0x808080ff);
45
+ applyBitmapColorScaleBias(region(img, 0, 0, 1, 1), region(img, 0, 0, 1, 1), { ...identity, redScale: 0 });
46
+ expect(img.data[0]).toBe(0);
47
+ expect(img.data[4]).toBe(0x80);
48
+ });
49
+
50
+ it('can write to a separate output bitmap', () => {
51
+ const source = createBitmap(1, 1, 0x808080ff);
52
+ const out = createBitmap(1, 1);
53
+ applyBitmapColorScaleBias(region(out), region(source), { ...identity, redScale: 0 });
54
+ expect(out.data[0]).toBe(0);
55
+ expect(out.data[1]).toBe(0x80);
56
+ expect(source.data[0]).toBe(0x80); // source unchanged
57
+ });
58
+ });
59
+
60
+ describe('applyBitmapThreshold', () => {
61
+ it('replaces pixels that pass the test', () => {
62
+ const src = createBitmap(2, 1);
63
+ setBitmapPixel(src, 0, 0, 0x808080ff);
64
+ setBitmapPixel(src, 1, 0, 0x404040ff);
65
+ const dst = createBitmap(2, 1);
66
+ const count = applyBitmapThreshold(region(dst), region(src), '>', 0x607060ff, 0xffffffff);
67
+ expect(count).toBe(1);
68
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0xffffffff);
69
+ expect(getBitmapPixel(dst, 1, 0)).toBe(0x00000000);
70
+ });
71
+
72
+ it('copies source when copySource is true and test fails', () => {
73
+ const src = createBitmap(1, 1, 0x112233ff);
74
+ const dst = createBitmap(1, 1);
75
+ applyBitmapThreshold(region(dst), region(src), '>', 0xffffffff, 0xffffffff, 0xffffffff, true);
76
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x112233ff);
77
+ });
78
+
79
+ it('returns zero when no pixels pass', () => {
80
+ const src = createBitmap(2, 2, 0x000000ff);
81
+ const dst = createBitmap(2, 2);
82
+ const count = applyBitmapThreshold(region(dst), region(src), '>', 0xffffffff);
83
+ expect(count).toBe(0);
84
+ });
85
+ });
86
+
87
+ describe('mergeBitmap', () => {
88
+ it('with multiplier 1 copies source', () => {
89
+ const src = createBitmap(1, 1, 0xff0000ff);
90
+ const dst = createBitmap(1, 1, 0x0000ffff);
91
+ mergeBitmap(region(dst), region(src), 1, 1, 1, 1);
92
+ expect(dst.data[0]).toBe(0xff);
93
+ expect(dst.data[2]).toBe(0x00);
94
+ });
95
+
96
+ it('with multiplier 0 keeps destination', () => {
97
+ const src = createBitmap(1, 1, 0xff0000ff);
98
+ const dst = createBitmap(1, 1, 0x0000ffff);
99
+ mergeBitmap(region(dst), region(src), 0, 0, 0, 0);
100
+ expect(dst.data[0]).toBe(0x00);
101
+ expect(dst.data[2]).toBe(0xff);
102
+ });
103
+
104
+ it('with multiplier 0.5 blends evenly', () => {
105
+ const src = createBitmap(1, 1, 0x200000ff);
106
+ const dst = createBitmap(1, 1, 0x000020ff);
107
+ mergeBitmap(region(dst), region(src), 0.5, 0, 0.5, 0);
108
+ expect(dst.data[0]).toBeCloseTo(16, 0);
109
+ expect(dst.data[2]).toBeCloseTo(16, 0);
110
+ });
111
+ });
112
+
113
+ describe('scrollBitmap', () => {
114
+ it('shifts content right with wrapping', () => {
115
+ const img = createBitmap(4, 1);
116
+ setBitmapPixel(img, 0, 0, 0xff0000ff);
117
+ scrollBitmap(img, 1, 0);
118
+ expect(getBitmapPixel(img, 1, 0)).toBe(0xff0000ff);
119
+ expect(getBitmapPixel(img, 0, 0)).toBe(0x00000000);
120
+ });
121
+
122
+ it('wraps pixels around the edge', () => {
123
+ const img = createBitmap(4, 1);
124
+ setBitmapPixel(img, 3, 0, 0xaabbccff);
125
+ scrollBitmap(img, 1, 0);
126
+ expect(getBitmapPixel(img, 0, 0)).toBe(0xaabbccff);
127
+ });
128
+ });
@@ -0,0 +1,118 @@
1
+ import type { Bitmap, BitmapRegion } from '@flighthq/types/contract';
2
+
3
+ import { createBitmap } from './bitmap';
4
+ import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
5
+ import { warpBitmap, warpBitmapQuad } from './bitmapWarp';
6
+
7
+ function region(bitmap: Readonly<Bitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height): BitmapRegion {
8
+ return { bitmap, x, y, width, height };
9
+ }
10
+
11
+ describe('warpBitmap', () => {
12
+ it('identity matrix copies source to dest', () => {
13
+ const src = createBitmap(4, 4, 0xff0000ff);
14
+ const dst = createBitmap(4, 4);
15
+ // Identity homography: dest pixel (x,y) maps to source (x,y).
16
+ warpBitmap(region(dst), region(src), [1, 0, 0, 0, 1, 0, 0, 0, 1]);
17
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0xff0000ff);
18
+ expect(getBitmapPixel(dst, 3, 3)).toBe(0xff0000ff);
19
+ });
20
+
21
+ it('translation matrix shifts source', () => {
22
+ const src = createBitmap(4, 4);
23
+ setBitmapPixel(src, 0, 0, 0xff0000ff);
24
+ const dst = createBitmap(4, 4);
25
+ // Translate source by (-1, -1): dest pixel (x,y) reads source (x-1, y-1).
26
+ warpBitmap(region(dst), region(src), [1, 0, -1, 0, 1, -1, 0, 0, 1], 'transparent', 'nearest');
27
+ // dst(1,1) should map to src(0,0)
28
+ expect(getBitmapPixel(dst, 1, 1)).toBe(0xff0000ff);
29
+ // dst(0,0) maps to src(-1,-1) → transparent
30
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x00000000);
31
+ });
32
+
33
+ it('clamp edgeMode extends border pixels', () => {
34
+ const src = createBitmap(2, 2, 0x00ff00ff);
35
+ const dst = createBitmap(4, 4);
36
+ // Translate source by (2,2): most dst pixels map outside src → clamp to border.
37
+ warpBitmap(region(dst), region(src), [1, 0, 2, 0, 1, 2, 0, 0, 1], 'clamp', 'nearest');
38
+ // dst(0,0) maps to src(-2,-2) → clamped to src(0,0) = green
39
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x00ff00ff);
40
+ });
41
+
42
+ it('transparent edgeMode gives transparent black outside bounds', () => {
43
+ const src = createBitmap(2, 2, 0xffffffff);
44
+ const dst = createBitmap(4, 4);
45
+ warpBitmap(region(dst), region(src), [1, 0, 3, 0, 1, 3, 0, 0, 1], 'transparent', 'nearest');
46
+ // All dst pixels map to src outside bounds.
47
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x00000000);
48
+ });
49
+
50
+ it('zero-size regions are a no-op', () => {
51
+ const src = createBitmap(4, 4, 0xffffffff);
52
+ const dst = createBitmap(4, 4);
53
+ warpBitmap(region(dst, 0, 0, 0, 4), region(src), [1, 0, 0, 0, 1, 0, 0, 0, 1]);
54
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x00000000);
55
+ });
56
+
57
+ it('near-zero w coefficient produces transparent pixel', () => {
58
+ const src = createBitmap(2, 2, 0xffffffff);
59
+ const dst = createBitmap(2, 2);
60
+ // Pathological matrix where w ≈ 0 at (0,0).
61
+ warpBitmap(region(dst), region(src), [1, 0, 0, 0, 1, 0, 1e-15, 0, 0], 'clamp', 'nearest');
62
+ // Should not throw; pixel at (0,0) with w≈0 becomes transparent.
63
+ expect(dst.data[3]).toBe(0);
64
+ });
65
+ });
66
+
67
+ describe('warpBitmapQuad', () => {
68
+ it('identity quad (src corners → dst corners) copies source', () => {
69
+ const src = createBitmap(4, 4, 0x0000ffff);
70
+ const dst = createBitmap(4, 4);
71
+ // Map source [4x4] corners to dest [4x4] corners unchanged.
72
+ warpBitmapQuad(region(dst), region(src), [0, 0, 4, 0, 4, 4, 0, 4], 'transparent', 'nearest');
73
+ // Interior pixels should be blue.
74
+ expect(getBitmapPixel(dst, 2, 2)).toBe(0x0000ffff);
75
+ });
76
+
77
+ it('degenerate quad (zero-area) produces no output', () => {
78
+ const src = createBitmap(4, 4, 0xffffffff);
79
+ const dst = createBitmap(4, 4);
80
+ // All four corners at the same point → degenerate homography.
81
+ warpBitmapQuad(region(dst), region(src), [2, 2, 2, 2, 2, 2, 2, 2], 'transparent', 'nearest');
82
+ // Should not throw; output can be transparent or unchanged.
83
+ expect(dst.data.every((v) => v === 0 || true)).toBe(true);
84
+ });
85
+
86
+ it('zero-size source or dest is a no-op', () => {
87
+ const src = createBitmap(4, 4, 0xffffffff);
88
+ const dst = createBitmap(4, 4);
89
+ warpBitmapQuad(region(dst, 0, 0, 0, 0), region(src), [0, 0, 4, 0, 4, 4, 0, 4]);
90
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x00000000);
91
+ });
92
+
93
+ it('horizontal shear quad produces non-trivial sampling', () => {
94
+ const src = createBitmap(8, 8);
95
+ // Fill left half red, right half green.
96
+ for (let py = 0; py < 8; py++) {
97
+ for (let px = 0; px < 8; px++) {
98
+ const i = (py * 8 + px) * 4;
99
+ src.data[i] = px < 4 ? 255 : 0;
100
+ src.data[i + 1] = px >= 4 ? 255 : 0;
101
+ src.data[i + 2] = 0;
102
+ src.data[i + 3] = 255;
103
+ }
104
+ }
105
+ const dst = createBitmap(8, 8);
106
+ // Shear: right edge shifted up by 2 pixels.
107
+ warpBitmapQuad(region(dst), region(src), [0, 0, 8, 2, 8, 8, 0, 8], 'transparent', 'nearest');
108
+ // Should not throw; output should be non-blank for inner pixels.
109
+ let hasNonZero = false;
110
+ for (let i = 0; i < dst.data.length; i++) {
111
+ if (dst.data[i] !== 0) {
112
+ hasNonZero = true;
113
+ break;
114
+ }
115
+ }
116
+ expect(hasNonZero).toBe(true);
117
+ });
118
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { createBitmapFromImageSource } from './bitmapFrom';
4
+ import { explainBitmapReadback } from './explainBitmapReadback';
5
+
6
+ function makeCanvas(width = 8, height = 4): HTMLCanvasElement {
7
+ const canvas = document.createElement('canvas');
8
+ canvas.width = width;
9
+ canvas.height = height;
10
+ return canvas;
11
+ }
12
+
13
+ describe('explainBitmapReadback', () => {
14
+ it('reports a readable source as ok', () => {
15
+ expect(explainBitmapReadback(makeCanvas(), 8, 4)).toEqual({ readable: true, reason: 'ok' });
16
+ });
17
+
18
+ it('reports an empty capture without touching the source', () => {
19
+ expect(explainBitmapReadback(makeCanvas(), 0, 4)).toEqual({ readable: false, reason: 'empty-size' });
20
+ expect(explainBitmapReadback(makeCanvas(), 8, 0)).toEqual({ readable: false, reason: 'empty-size' });
21
+ expect(explainBitmapReadback(makeCanvas(), -1, 4)).toEqual({ readable: false, reason: 'empty-size' });
22
+ });
23
+
24
+ it('reports a tainted source', () => {
25
+ const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'getImageData').mockImplementation(() => {
26
+ throw new DOMException('Tainted canvases may not be exported.', 'SecurityError');
27
+ });
28
+ try {
29
+ expect(explainBitmapReadback(makeCanvas(), 8, 4)).toEqual({ readable: false, reason: 'tainted-source' });
30
+ } finally {
31
+ spy.mockRestore();
32
+ }
33
+ });
34
+
35
+ it('prefers the reason knowable without drawing — an empty capture outranks a tainted source', () => {
36
+ const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'getImageData').mockImplementation(() => {
37
+ throw new DOMException('Tainted canvases may not be exported.', 'SecurityError');
38
+ });
39
+ try {
40
+ expect(explainBitmapReadback(makeCanvas(), 0, 4).reason).toBe('empty-size');
41
+ } finally {
42
+ spy.mockRestore();
43
+ }
44
+ });
45
+
46
+ it('agrees with what createBitmapFromImageSource actually does', () => {
47
+ // The pull query duplicates the constructor's failure conditions by design, so the pairing is the
48
+ // thing worth pinning: readable exactly when a bitmap comes back.
49
+ const canvas = makeCanvas();
50
+ expect(explainBitmapReadback(canvas, 8, 4).readable).toBe(createBitmapFromImageSource(canvas, 8, 4) !== null);
51
+ expect(explainBitmapReadback(canvas, 0, 4).readable).toBe(createBitmapFromImageSource(canvas, 0, 4) !== null);
52
+
53
+ const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'getImageData').mockImplementation(() => {
54
+ throw new DOMException('Tainted canvases may not be exported.', 'SecurityError');
55
+ });
56
+ try {
57
+ expect(explainBitmapReadback(canvas, 8, 4).readable).toBe(createBitmapFromImageSource(canvas, 8, 4) !== null);
58
+ } finally {
59
+ spy.mockRestore();
60
+ }
61
+ });
62
+
63
+ it('never throws, whatever the source does', () => {
64
+ const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'drawImage').mockImplementation(() => {
65
+ throw new TypeError('unsupported source');
66
+ });
67
+ try {
68
+ expect(() => explainBitmapReadback(makeCanvas(), 8, 4)).not.toThrow();
69
+ expect(explainBitmapReadback(makeCanvas(), 8, 4).readable).toBe(false);
70
+ } finally {
71
+ spy.mockRestore();
72
+ }
73
+ });
74
+
75
+ it('allocates no bitmap — it reads one pixel, not the whole capture', () => {
76
+ const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'getImageData');
77
+ try {
78
+ explainBitmapReadback(makeCanvas(1024, 1024), 1024, 1024);
79
+ expect(spy).toHaveBeenCalledWith(0, 0, 1, 1);
80
+ } finally {
81
+ spy.mockRestore();
82
+ }
83
+ });
84
+ });