@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,259 @@
1
+ import { BitmapCompositeMode } from '@flighthq/types/contract';
2
+
3
+ import { createBitmap } from './bitmap';
4
+ import {
5
+ compositeBitmapPixels,
6
+ compositeBitmapRegion,
7
+ extractBitmapPixels,
8
+ extractBitmapPixels32,
9
+ writeBitmapPixels,
10
+ writeBitmapPixels32,
11
+ } from './bitmapComposite';
12
+
13
+ function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
14
+ return { bitmap, x, y, width, height };
15
+ }
16
+
17
+ describe('compositeBitmapPixels', () => {
18
+ it('alpha-composites pixels over the destination', () => {
19
+ const dest = createBitmap(1, 1, 0x0000ffff);
20
+ const pixels = new Uint8ClampedArray([0xff, 0, 0, 0xff]);
21
+ compositeBitmapPixels(region(dest), pixels);
22
+ expect(dest.data[0]).toBe(0xff);
23
+ expect(dest.data[2]).toBe(0);
24
+ expect(dest.data[3]).toBe(0xff);
25
+ });
26
+
27
+ it('blends semi-transparent source over destination', () => {
28
+ const dest = createBitmap(1, 1, 0x000000ff);
29
+ const pixels = new Uint8ClampedArray([0xff, 0xff, 0xff, 0x80]);
30
+ compositeBitmapPixels(region(dest), pixels);
31
+ expect(dest.data[0]).toBeGreaterThan(0);
32
+ expect(dest.data[0]).toBeLessThan(0xff);
33
+ expect(dest.data[3]).toBe(0xff);
34
+ });
35
+
36
+ it('silently skips pixels outside destination bounds', () => {
37
+ const dest = createBitmap(1, 1);
38
+ const pixels = new Uint8ClampedArray([0xff, 0xff, 0xff, 0xff]);
39
+ compositeBitmapPixels(region(dest, 5, 5, 1, 1), pixels);
40
+ expect(dest.data[3]).toBe(0);
41
+ });
42
+
43
+ it('multiply blend mode multiplies source and destination channels', () => {
44
+ const dest = createBitmap(1, 1, 0xff0000ff); // RGBA: opaque red
45
+ const pixels = new Uint8ClampedArray([128, 128, 128, 255]);
46
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.Multiply);
47
+ expect(dest.data[0]).toBe(128); // 255 * 128 / 255
48
+ expect(dest.data[1]).toBe(0); // 0 * 128 / 255
49
+ expect(dest.data[2]).toBe(0);
50
+ expect(dest.data[3]).toBe(255);
51
+ });
52
+
53
+ it('add blend mode clamps the sum of channels', () => {
54
+ const dest = createBitmap(1, 1, 0x640000ff); // RGBA: opaque dark red (R=100)
55
+ const pixels = new Uint8ClampedArray([200, 0, 0, 255]);
56
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.Add);
57
+ expect(dest.data[0]).toBe(255); // min(255, 100 + 200)
58
+ });
59
+
60
+ it('defaults to source-over (Normal) when no blend mode is given', () => {
61
+ const dest = createBitmap(1, 1, 0xff0000ff); // RGBA: opaque red
62
+ const pixels = new Uint8ClampedArray([0, 0, 255, 255]);
63
+ compositeBitmapPixels(region(dest), pixels);
64
+ expect(dest.data[0]).toBe(0);
65
+ expect(dest.data[2]).toBe(255);
66
+ });
67
+
68
+ it('overlay blend mode darkens on dark backdrops', () => {
69
+ const dest = createBitmap(1, 1, 0x400000ff); // RGBA: R=64
70
+ const pixels = new Uint8ClampedArray([200, 0, 0, 255]);
71
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.Overlay);
72
+ expect(dest.data[0]).toBe(100); // 2 * 64 * 200 / 255
73
+ });
74
+
75
+ it('hardlight blend mode is overlay with operands swapped', () => {
76
+ const dest = createBitmap(1, 1, 0xc80000ff); // RGBA: R=200
77
+ const pixels = new Uint8ClampedArray([64, 0, 0, 255]);
78
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.HardLight);
79
+ expect(dest.data[0]).toBe(100); // 2 * 200 * 64 / 255
80
+ });
81
+
82
+ it('invert blend mode inverts the backdrop, ignoring source color', () => {
83
+ const dest = createBitmap(1, 1, 0xc80000ff); // RGBA: R=200
84
+ const pixels = new Uint8ClampedArray([0, 0, 0, 255]);
85
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.Invert);
86
+ expect(dest.data[0]).toBe(55); // 255 - 200
87
+ });
88
+
89
+ it('DestinationOut (erase) knocks alpha out of the backdrop, keeping color', () => {
90
+ const dest = createBitmap(1, 1, 0xff0000ff); // RGBA: opaque red
91
+ const pixels = new Uint8ClampedArray([0, 0, 0, 128]);
92
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.DestinationOut);
93
+ expect(dest.data[0]).toBe(255); // color untouched
94
+ expect(dest.data[3]).toBe(127); // 255 * (1 - 128/255)
95
+ });
96
+
97
+ it('DestinationIn (alpha) masks the backdrop to the source coverage', () => {
98
+ const dest = createBitmap(1, 1, 0xff0000ff); // RGBA: opaque red
99
+ const pixels = new Uint8ClampedArray([0, 0, 0, 128]);
100
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.DestinationIn);
101
+ expect(dest.data[0]).toBe(255); // backdrop color kept
102
+ expect(dest.data[3]).toBe(128); // 255 * (128/255)
103
+ });
104
+
105
+ it('Copy overwrites the backdrop with the source', () => {
106
+ const dest = createBitmap(1, 1, 0xff0000ff); // opaque red
107
+ const pixels = new Uint8ClampedArray([0, 0, 255, 128]); // semi-transparent blue
108
+ compositeBitmapPixels(region(dest), pixels, BitmapCompositeMode.Copy);
109
+ expect(dest.data[2]).toBe(255);
110
+ expect(dest.data[3]).toBe(128); // source alpha replaces, no source-over
111
+ });
112
+ });
113
+
114
+ describe('compositeBitmapRegion', () => {
115
+ it('alpha-composites a region of one bitmap over another', () => {
116
+ const source = createBitmap(1, 1, 0xff0000ff);
117
+ const dest = createBitmap(1, 1, 0x0000ffff);
118
+ compositeBitmapRegion(region(dest), region(source));
119
+ expect(dest.data[0]).toBe(0xff);
120
+ expect(dest.data[2]).toBe(0);
121
+ });
122
+
123
+ it('clips to the smaller of source and dest dimensions', () => {
124
+ const source = createBitmap(1, 1, 0xff0000ff);
125
+ const dest = createBitmap(3, 3);
126
+ compositeBitmapRegion(region(dest, 1, 1, 2, 2), region(source));
127
+ expect(dest.data[(1 * 3 + 1) * 4]).toBe(0xff);
128
+ expect(dest.data[(1 * 3 + 2) * 4]).toBe(0);
129
+ });
130
+
131
+ it('applies the blend mode to the source region', () => {
132
+ const source = createBitmap(1, 1, 0x808080ff); // RGBA: opaque gray
133
+ const dest = createBitmap(1, 1, 0xff0000ff); // RGBA: opaque red
134
+ compositeBitmapRegion(region(dest), region(source), BitmapCompositeMode.Multiply);
135
+ expect(dest.data[0]).toBe(128); // 255 * 128 / 255
136
+ expect(dest.data[1]).toBe(0);
137
+ });
138
+ });
139
+
140
+ describe('extractBitmapPixels', () => {
141
+ it('copies a bitmap region into a tightly-packed buffer', () => {
142
+ const source = createBitmap(2, 2);
143
+ const i = (1 * 2 + 1) * 4;
144
+ source.data[i] = 0xff;
145
+ source.data[i + 3] = 0xff;
146
+ const out = new Uint8ClampedArray(4);
147
+ extractBitmapPixels(out, region(source, 1, 1, 1, 1));
148
+ expect(out[0]).toBe(0xff);
149
+ expect(out[3]).toBe(0xff);
150
+ });
151
+
152
+ it('silently skips pixels outside source bounds', () => {
153
+ const source = createBitmap(1, 1, 0xffffffff);
154
+ const out = new Uint8ClampedArray(4 * 4);
155
+ extractBitmapPixels(out, region(source, -1, -1, 2, 2));
156
+ const i = (1 * 2 + 1) * 4;
157
+ expect(out[i]).toBe(0xff);
158
+ expect(out[i + 3]).toBe(0xff);
159
+ expect(out[0]).toBe(0);
160
+ });
161
+
162
+ it('source.bitmap.data can be passed as out for a full-bitmap extraction', () => {
163
+ const source = createBitmap(2, 2, 0xaabbccff);
164
+ extractBitmapPixels(source.data, region(source));
165
+ expect(source.data[0]).toBe(0xaa);
166
+ expect(source.data[3]).toBe(0xff);
167
+ });
168
+ });
169
+
170
+ describe('extractBitmapPixels32', () => {
171
+ it('packs each pixel into one 0xRRGGBBAA entry', () => {
172
+ const source = createBitmap(2, 2);
173
+ const i = (1 * 2 + 1) * 4;
174
+ source.data[i] = 0x11;
175
+ source.data[i + 1] = 0x22;
176
+ source.data[i + 2] = 0x33;
177
+ source.data[i + 3] = 0x44;
178
+ const out = new Uint32Array(1);
179
+ extractBitmapPixels32(out, region(source, 1, 1, 1, 1));
180
+ expect(out[0]).toBe(0x11223344);
181
+ });
182
+
183
+ it('round-trips with writeBitmapPixels32', () => {
184
+ const source = createBitmap(3, 2);
185
+ for (let p = 0; p < 6; p++) {
186
+ const i = p * 4;
187
+ source.data[i] = p * 10 + 1;
188
+ source.data[i + 1] = p * 10 + 2;
189
+ source.data[i + 2] = p * 10 + 3;
190
+ source.data[i + 3] = p * 10 + 4;
191
+ }
192
+ const packed = new Uint32Array(6);
193
+ extractBitmapPixels32(packed, region(source));
194
+ const dest = createBitmap(3, 2);
195
+ writeBitmapPixels32(region(dest), packed);
196
+ expect(Array.from(dest.data)).toEqual(Array.from(source.data));
197
+ });
198
+
199
+ it('silently skips pixels outside source bounds', () => {
200
+ const source = createBitmap(1, 1, 0xffffffff);
201
+ const out = new Uint32Array(4);
202
+ extractBitmapPixels32(out, region(source, -1, -1, 2, 2));
203
+ expect(out[1 * 2 + 1]).toBe(0xffffffff);
204
+ expect(out[0]).toBe(0);
205
+ });
206
+ });
207
+
208
+ describe('writeBitmapPixels', () => {
209
+ it('writes pixels at the given destination region', () => {
210
+ const dest = createBitmap(3, 3);
211
+ const pixels = new Uint8ClampedArray([0x33, 0x66, 0x99, 0xff]);
212
+ writeBitmapPixels(region(dest, 1, 1, 1, 1), pixels);
213
+ const i = (1 * 3 + 1) * 4;
214
+ expect(dest.data[i]).toBe(0x33);
215
+ expect(dest.data[i + 1]).toBe(0x66);
216
+ expect(dest.data[i + 2]).toBe(0x99);
217
+ expect(dest.data[i + 3]).toBe(0xff);
218
+ });
219
+
220
+ it('overwrites existing content', () => {
221
+ const dest = createBitmap(1, 1, 0x0000ffff);
222
+ const pixels = new Uint8ClampedArray([0xff, 0, 0, 0xff]);
223
+ writeBitmapPixels(region(dest), pixels);
224
+ expect(dest.data[2]).toBe(0);
225
+ expect(dest.data[0]).toBe(0xff);
226
+ });
227
+
228
+ it('silently clips writes outside destination bounds', () => {
229
+ const dest = createBitmap(1, 1);
230
+ const pixels = new Uint8ClampedArray([0xff, 0xff, 0xff, 0xff]);
231
+ writeBitmapPixels(region(dest, 5, 5, 1, 1), pixels);
232
+ expect(dest.data[3]).toBe(0);
233
+ });
234
+ });
235
+
236
+ describe('writeBitmapPixels32', () => {
237
+ it('unpacks each 0xRRGGBBAA entry into the destination region', () => {
238
+ const dest = createBitmap(3, 3);
239
+ writeBitmapPixels32(region(dest, 1, 1, 1, 1), new Uint32Array([0x33669900 | 0xff]));
240
+ const i = (1 * 3 + 1) * 4;
241
+ expect(dest.data[i]).toBe(0x33);
242
+ expect(dest.data[i + 1]).toBe(0x66);
243
+ expect(dest.data[i + 2]).toBe(0x99);
244
+ expect(dest.data[i + 3]).toBe(0xff);
245
+ });
246
+
247
+ it('overwrites existing content', () => {
248
+ const dest = createBitmap(1, 1, 0x0000ffff);
249
+ writeBitmapPixels32(region(dest), new Uint32Array([0xff0000ff]));
250
+ expect(dest.data[0]).toBe(0xff);
251
+ expect(dest.data[2]).toBe(0);
252
+ });
253
+
254
+ it('silently clips writes outside destination bounds', () => {
255
+ const dest = createBitmap(1, 1);
256
+ writeBitmapPixels32(region(dest, 5, 5, 1, 1), new Uint32Array([0xffffffff]));
257
+ expect(dest.data[3]).toBe(0);
258
+ });
259
+ });
@@ -0,0 +1,130 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { convolveBitmap } from './bitmapConvolution';
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('convolveBitmap', () => {
9
+ it('applies a convolution matrix to the source region', () => {
10
+ const source = createBitmap(3, 1);
11
+ source.data[0] = 10;
12
+ source.data[4] = 20;
13
+ source.data[8] = 30;
14
+ const out = new Uint8ClampedArray(4);
15
+ convolveBitmap(out, region(source, 1, 0, 1, 1), {
16
+ matrix: [1, 1, 1],
17
+ matrixX: 3,
18
+ matrixY: 1,
19
+ preserveAlpha: false,
20
+ });
21
+ expect(out[0]).toBe(20);
22
+ });
23
+
24
+ it("'clamp' edge mode (default) repeats the nearest edge pixel", () => {
25
+ // 1px source, 3-wide kernel: all three samples hit the single source pixel
26
+ const source = createBitmap(1, 1, 0x606060ff);
27
+ const out = new Uint8ClampedArray(4);
28
+ convolveBitmap(out, region(source), {
29
+ matrix: [1, 1, 1],
30
+ matrixX: 3,
31
+ matrixY: 1,
32
+ divisor: 3,
33
+ preserveAlpha: false,
34
+ });
35
+ expect(out[0]).toBe(0x60);
36
+ });
37
+
38
+ it("'mirror' edge mode reflects at boundaries", () => {
39
+ // 3px source [10, 20, 30]. 5-wide kernel [1, 0, 0, 0, 0] picks 2 positions left.
40
+ // For px=0, offsetX=2: rawSampleX = 0 + 0 + 0 - 2 = -2.
41
+ // mirror(-2, 3) reflects to x=1 (value 20), while clamp(-2) gives x=0 (value 10).
42
+ const source = createBitmap(3, 1);
43
+ source.data[0] = 10;
44
+ source.data[4] = 20;
45
+ source.data[8] = 30;
46
+ const out = new Uint8ClampedArray(3 * 4);
47
+ convolveBitmap(out, region(source), {
48
+ edge: 'mirror',
49
+ matrix: [1, 0, 0, 0, 0],
50
+ matrixX: 5,
51
+ matrixY: 1,
52
+ divisor: 1,
53
+ preserveAlpha: false,
54
+ });
55
+ expect(out[0]).toBe(20);
56
+ });
57
+
58
+ it("'transparent' edge mode treats out-of-bounds as transparent black", () => {
59
+ // 1px source (opaque black), 3-wide kernel [1, 0, 0] — leftmost sample
60
+ // is out of bounds and contributes transparent black (all zeros).
61
+ const source = createBitmap(1, 1, 0x000000ff);
62
+ const out = new Uint8ClampedArray(4);
63
+ convolveBitmap(out, region(source), {
64
+ edge: 'transparent',
65
+ divisor: 1,
66
+ matrix: [1, 0, 0],
67
+ matrixX: 3,
68
+ matrixY: 1,
69
+ preserveAlpha: false,
70
+ });
71
+ expect(out[0]).toBe(0);
72
+ expect(out[3]).toBe(0);
73
+ });
74
+
75
+ it("'wrap' edge mode tiles the source toroidally", () => {
76
+ // 3px source [10, 20, 30]. The leftmost kernel position for px=0 wraps to px=2.
77
+ const source = createBitmap(3, 1);
78
+ source.data[0] = 10;
79
+ source.data[4] = 20;
80
+ source.data[8] = 30;
81
+ const out = new Uint8ClampedArray(3 * 4);
82
+ convolveBitmap(out, region(source), {
83
+ edge: 'wrap',
84
+ matrix: [1, 0, 0], // picks left neighbor only
85
+ matrixX: 3,
86
+ matrixY: 1,
87
+ divisor: 1,
88
+ preserveAlpha: false,
89
+ });
90
+ // px=0 left neighbor wraps to px=2 → value 30
91
+ expect(out[0]).toBe(30);
92
+ });
93
+
94
+ it('preserves source alpha by default', () => {
95
+ const source = createBitmap(1, 1, 0x00000044);
96
+ const out = new Uint8ClampedArray(4);
97
+ convolveBitmap(out, region(source), {
98
+ bias: 255,
99
+ matrix: [1],
100
+ matrixX: 1,
101
+ matrixY: 1,
102
+ });
103
+ expect(out[3]).toBe(0x44);
104
+ });
105
+
106
+ it('throws when matrix dimensions are not positive', () => {
107
+ const source = createBitmap(1, 1);
108
+ const out = new Uint8ClampedArray(4);
109
+ expect(() => convolveBitmap(out, region(source), { matrix: [], matrixX: 0, matrixY: 1 })).toThrow();
110
+ });
111
+
112
+ it('throws when matrix is too short for its declared dimensions', () => {
113
+ const source = createBitmap(1, 1);
114
+ const out = new Uint8ClampedArray(4);
115
+ expect(() => convolveBitmap(out, region(source), { matrix: [1], matrixX: 3, matrixY: 3 })).toThrow();
116
+ });
117
+
118
+ it('treats an explicit divisor of 0 as passthrough (no division by zero)', () => {
119
+ const source = createBitmap(1, 1, 0x804020ff);
120
+ const out = new Uint8ClampedArray(4);
121
+ convolveBitmap(out, region(source), {
122
+ divisor: 0,
123
+ matrix: [1],
124
+ matrixX: 1,
125
+ matrixY: 1,
126
+ preserveAlpha: false,
127
+ });
128
+ expect(out[0]).toBe(0x80);
129
+ });
130
+ });
@@ -0,0 +1,48 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { copyBitmapChannel, copyBitmapPixels } from './bitmapCopy';
3
+ import { ImageChannel } from './bitmapImageChannel';
4
+ import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
5
+
6
+ function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
7
+ return { bitmap, x, y, width, height };
8
+ }
9
+
10
+ describe('copyBitmapChannel', () => {
11
+ it('copies the red channel to the blue channel', () => {
12
+ const src = createBitmap(2, 2);
13
+ src.data[0] = 0xab;
14
+ const dst = createBitmap(2, 2);
15
+ copyBitmapChannel(region(dst), ImageChannel.Blue, region(src), ImageChannel.Red);
16
+ expect(dst.data[2]).toBe(src.data[0]);
17
+ });
18
+
19
+ it('copies the alpha channel independently', () => {
20
+ const src = createBitmap(1, 1);
21
+ setBitmapPixel(src, 0, 0, 0x000000de);
22
+ const dst = createBitmap(1, 1);
23
+ copyBitmapChannel(region(dst), ImageChannel.Alpha, region(src), ImageChannel.Alpha);
24
+ expect(dst.data[3]).toBe(0xde);
25
+ });
26
+ });
27
+
28
+ describe('copyBitmapPixels', () => {
29
+ it('copies a region without alpha blend', () => {
30
+ const src = createBitmap(2, 2, 0xaabbccff);
31
+ const dst = createBitmap(4, 4);
32
+ copyBitmapPixels(region(dst, 1, 1, 2, 2), region(src, 0, 0, 2, 2));
33
+ expect(getBitmapPixel(dst, 1, 1)).toBe(0xaabbccff);
34
+ expect(getBitmapPixel(dst, 0, 0)).toBe(0x00000000);
35
+ });
36
+
37
+ it('alpha-composites when composite is true', () => {
38
+ const src = createBitmap(1, 1);
39
+ setBitmapPixel(src, 0, 0, 0xff000080);
40
+ const dst = createBitmap(1, 1);
41
+ setBitmapPixel(dst, 0, 0, 0x0000ffff);
42
+ copyBitmapPixels(region(dst), region(src), true);
43
+ const result = getBitmapPixel(dst, 0, 0);
44
+ expect(result & 0xff).toBe(0xff); // alpha opaque after composite
45
+ expect((result >>> 24) & 0xff).toBeGreaterThan(0); // red blended in
46
+ expect((result >> 8) & 0xff).toBeGreaterThan(0); // blue retained
47
+ });
48
+ });
@@ -0,0 +1,39 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { getBitmapCoverage } from './bitmapCoverage';
3
+ import { setBitmapPixel } from './bitmapPixel';
4
+
5
+ describe('getBitmapCoverage', () => {
6
+ it('returns 0 for a bitmap still entirely the background colour', () => {
7
+ expect(getBitmapCoverage(createBitmap(4, 4, 0x123456ff), 0x123456ff)).toBe(0);
8
+ });
9
+
10
+ it('returns 0 for a fully transparent bitmap against a transparent background', () => {
11
+ expect(getBitmapCoverage(createBitmap(4, 4, 0x00000000), 0x00000000)).toBe(0);
12
+ });
13
+
14
+ it('counts pixels that differ from the background on any channel', () => {
15
+ const bitmap = createBitmap(2, 2, 0x000000ff);
16
+ setBitmapPixel(bitmap, 0, 0, 0xff0000ff);
17
+ expect(getBitmapCoverage(bitmap, 0x000000ff)).toBe(0.25);
18
+ });
19
+
20
+ it('measures a fully painted bitmap as full coverage', () => {
21
+ const bitmap = createBitmap(2, 2, 0x000000ff);
22
+ setBitmapPixel(bitmap, 0, 0, 0xffffffff);
23
+ setBitmapPixel(bitmap, 1, 0, 0xffffffff);
24
+ setBitmapPixel(bitmap, 0, 1, 0xffffffff);
25
+ setBitmapPixel(bitmap, 1, 1, 0xffffffff);
26
+ expect(getBitmapCoverage(bitmap, 0x000000ff)).toBe(1);
27
+ });
28
+
29
+ it('ignores differences within the channel tolerance', () => {
30
+ const bitmap = createBitmap(2, 1, 0x000000ff);
31
+ setBitmapPixel(bitmap, 0, 0, 0x050505ff); // 5 per channel
32
+ expect(getBitmapCoverage(bitmap, 0x000000ff, 5)).toBe(0);
33
+ expect(getBitmapCoverage(bitmap, 0x000000ff, 4)).toBe(0.5);
34
+ });
35
+
36
+ it('returns 0 for an empty bitmap', () => {
37
+ expect(getBitmapCoverage(createBitmap(0, 0), 0x000000ff)).toBe(0);
38
+ });
39
+ });
@@ -0,0 +1,116 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { cropBitmap, extendBitmap, trimBitmap } from './bitmapCrop';
3
+ import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
4
+
5
+ describe('cropBitmap', () => {
6
+ it('returns a bitmap with the requested dimensions', () => {
7
+ const src = createBitmap(10, 10, 0xff0000ff);
8
+ const out = cropBitmap(src, { x: 2, y: 3, width: 4, height: 5 });
9
+ expect(out.width).toBe(4);
10
+ expect(out.height).toBe(5);
11
+ });
12
+
13
+ it('copies pixels from the source region', () => {
14
+ const src = createBitmap(4, 4);
15
+ setBitmapPixel(src, 2, 1, 0x00ff00ff);
16
+ const out = cropBitmap(src, { x: 2, y: 1, width: 2, height: 2 });
17
+ expect(getBitmapPixel(out, 0, 0)).toBe(0x00ff00ff);
18
+ });
19
+
20
+ it('fills pixels outside the source with transparent black', () => {
21
+ const src = createBitmap(2, 2, 0xffffffff);
22
+ // Crop region extends beyond source.
23
+ const out = cropBitmap(src, { x: 1, y: 1, width: 3, height: 3 });
24
+ // (0,0) is inside source — opaque white.
25
+ expect(getBitmapPixel(out, 0, 0)).toBe(0xffffffff);
26
+ // (2,2) is outside source — transparent black.
27
+ expect(getBitmapPixel(out, 2, 2)).toBe(0x00000000);
28
+ });
29
+
30
+ it('preserves source colorSpace and alphaType', () => {
31
+ const src = createBitmap(2, 2, 0xff0000ff);
32
+ const out = cropBitmap(src, { x: 0, y: 0, width: 2, height: 2 });
33
+ expect(out.colorSpace).toBe(src.colorSpace);
34
+ expect(out.alphaType).toBe(src.alphaType);
35
+ });
36
+
37
+ it('returns a distinct bitmap object', () => {
38
+ const src = createBitmap(2, 2, 0xff0000ff);
39
+ const out = cropBitmap(src, { x: 0, y: 0, width: 2, height: 2 });
40
+ expect(out).not.toBe(src);
41
+ expect(out.data).not.toBe(src.data);
42
+ });
43
+ });
44
+
45
+ describe('extendBitmap', () => {
46
+ it('returns a bitmap padded by the correct number of pixels', () => {
47
+ const src = createBitmap(2, 2, 0x0000ffff);
48
+ const out = extendBitmap(src, 1, 2, 3, 4);
49
+ expect(out.width).toBe(2 + 1 + 3);
50
+ expect(out.height).toBe(2 + 2 + 4);
51
+ });
52
+
53
+ it('copies source pixels into the center region', () => {
54
+ const src = createBitmap(2, 2, 0xabcdefff);
55
+ const out = extendBitmap(src, 1, 1, 1, 1);
56
+ expect(getBitmapPixel(out, 1, 1)).toBe(0xabcdefff);
57
+ });
58
+
59
+ it('transparent edge mode fills padding with transparent black by default', () => {
60
+ const src = createBitmap(2, 2, 0xff0000ff);
61
+ const out = extendBitmap(src, 1, 1, 1, 1);
62
+ expect(getBitmapPixel(out, 0, 0)).toBe(0x00000000);
63
+ });
64
+
65
+ it('transparent edge mode uses fillColor when provided', () => {
66
+ const src = createBitmap(2, 2, 0xff0000ff);
67
+ const out = extendBitmap(src, 1, 1, 1, 1, 'transparent', 0xffffffff);
68
+ expect(getBitmapPixel(out, 0, 0)).toBe(0xffffffff);
69
+ });
70
+
71
+ it('clamp edge mode repeats border pixels in padding', () => {
72
+ const src = createBitmap(2, 2, 0x0000ffff);
73
+ // Set a distinct border pixel.
74
+ setBitmapPixel(src, 0, 0, 0x123456ff);
75
+ const out = extendBitmap(src, 1, 1, 0, 0, 'clamp');
76
+ // Top-left padding comes from source (0,0).
77
+ expect(getBitmapPixel(out, 0, 0)).toBe(0x123456ff);
78
+ });
79
+
80
+ it('wrap edge mode tiles the source in padding', () => {
81
+ const src = createBitmap(2, 2);
82
+ setBitmapPixel(src, 0, 0, 0xff0000ff);
83
+ setBitmapPixel(src, 1, 0, 0x00ff00ff);
84
+ // Extend by 2 on the right → first padding column maps to source col 0.
85
+ const out = extendBitmap(src, 0, 0, 2, 0, 'wrap');
86
+ expect(getBitmapPixel(out, 2, 0)).toBe(0xff0000ff);
87
+ expect(getBitmapPixel(out, 3, 0)).toBe(0x00ff00ff);
88
+ });
89
+ });
90
+
91
+ describe('trimBitmap', () => {
92
+ it('removes transparent border rows and columns', () => {
93
+ const src = createBitmap(5, 5);
94
+ // Only pixel (2,2) is non-transparent.
95
+ setBitmapPixel(src, 2, 2, 0xff0000ff);
96
+ const out = trimBitmap(src);
97
+ expect(out.width).toBe(1);
98
+ expect(out.height).toBe(1);
99
+ expect(getBitmapPixel(out, 0, 0)).toBe(0xff0000ff);
100
+ });
101
+
102
+ it('returns a 1×1 transparent bitmap for a fully transparent input', () => {
103
+ const src = createBitmap(3, 3);
104
+ const out = trimBitmap(src);
105
+ expect(out.width).toBe(1);
106
+ expect(out.height).toBe(1);
107
+ expect(getBitmapPixel(out, 0, 0) & 0xff).toBe(0);
108
+ });
109
+
110
+ it('returns a bitmap equal to the source when there is no transparent border', () => {
111
+ const src = createBitmap(2, 2, 0xff0000ff);
112
+ const out = trimBitmap(src);
113
+ expect(out.width).toBe(2);
114
+ expect(out.height).toBe(2);
115
+ });
116
+ });