@flighthq/bitmap 0.3.0-next.1021.f5f1ee0

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 (214) 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 +131 -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 +17 -0
  86. package/dist/bitmapFrom.d.ts.map +1 -0
  87. package/dist/bitmapFrom.js +67 -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 +42 -0
  166. package/dist/contract.d.ts.map +1 -0
  167. package/dist/contract.js +42 -0
  168. package/dist/contract.js.map +1 -0
  169. package/dist/index.d.ts +2 -0
  170. package/dist/index.d.ts.map +1 -0
  171. package/dist/index.js +2 -0
  172. package/dist/index.js.map +1 -0
  173. package/package.json +48 -0
  174. package/src/bitmap.test.ts +88 -0
  175. package/src/bitmapAffine.test.ts +98 -0
  176. package/src/bitmapAlpha.test.ts +110 -0
  177. package/src/bitmapBevel.test.ts +61 -0
  178. package/src/bitmapBlur.test.ts +234 -0
  179. package/src/bitmapChannel.test.ts +97 -0
  180. package/src/bitmapColorMatrix.test.ts +214 -0
  181. package/src/bitmapCompare.test.ts +92 -0
  182. package/src/bitmapComposite.test.ts +259 -0
  183. package/src/bitmapConvolution.test.ts +130 -0
  184. package/src/bitmapCopy.test.ts +48 -0
  185. package/src/bitmapCoverage.test.ts +39 -0
  186. package/src/bitmapCrop.test.ts +116 -0
  187. package/src/bitmapDisplacement.test.ts +136 -0
  188. package/src/bitmapDissolve.test.ts +99 -0
  189. package/src/bitmapDraw.test.ts +68 -0
  190. package/src/bitmapEncode.test.ts +18 -0
  191. package/src/bitmapFill.test.ts +83 -0
  192. package/src/bitmapFingerprint.test.ts +103 -0
  193. package/src/bitmapFlip.test.ts +53 -0
  194. package/src/bitmapFormat.test.ts +124 -0
  195. package/src/bitmapFrom.test.ts +57 -0
  196. package/src/bitmapGradient.test.ts +85 -0
  197. package/src/bitmapGradientFill.test.ts +107 -0
  198. package/src/bitmapHistogram.test.ts +64 -0
  199. package/src/bitmapImageChannel.test.ts +19 -0
  200. package/src/bitmapMedian.test.ts +39 -0
  201. package/src/bitmapMorphological.test.ts +69 -0
  202. package/src/bitmapNoise.test.ts +163 -0
  203. package/src/bitmapPaletteMap.test.ts +49 -0
  204. package/src/bitmapPixel.test.ts +117 -0
  205. package/src/bitmapPixelate.test.ts +40 -0
  206. package/src/bitmapQuery.test.ts +42 -0
  207. package/src/bitmapRegion.test.ts +34 -0
  208. package/src/bitmapResize.test.ts +106 -0
  209. package/src/bitmapRotate.test.ts +108 -0
  210. package/src/bitmapShadow.test.ts +159 -0
  211. package/src/bitmapSharpen.test.ts +44 -0
  212. package/src/bitmapTone.test.ts +104 -0
  213. package/src/bitmapTransform.test.ts +128 -0
  214. package/src/bitmapWarp.test.ts +118 -0
@@ -0,0 +1,97 @@
1
+ import { createBitmap } from './bitmap';
2
+ import { mergeBitmapChannels, splitBitmapChannels } from './bitmapChannel';
3
+ import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
4
+ import { createBitmapRegion } from './bitmapRegion';
5
+
6
+ describe('mergeBitmapChannels', () => {
7
+ it('combines one channel from each input into the output', () => {
8
+ const rSurf = createBitmap(1, 1, 0xff000000); // R=0xff, G=0, B=0, A=0
9
+ const gSurf = createBitmap(1, 1, 0x00ff0000); // R=0, G=0xff, B=0, A=0
10
+ const bSurf = createBitmap(1, 1, 0x0000ff00); // R=0, G=0, B=0xff, A=0
11
+ const aSurf = createBitmap(1, 1, 0x000000ff); // R=0, G=0, B=0, A=0xff
12
+ const out = createBitmap(1, 1);
13
+ mergeBitmapChannels(
14
+ createBitmapRegion(out),
15
+ createBitmapRegion(rSurf),
16
+ createBitmapRegion(gSurf),
17
+ createBitmapRegion(bSurf),
18
+ createBitmapRegion(aSurf),
19
+ );
20
+ expect(getBitmapPixel(out, 0, 0)).toBe(0xff_ff_ff_ff);
21
+ });
22
+
23
+ it('uses the minimum dimension overlap', () => {
24
+ const r = createBitmap(3, 1, 0xff000000);
25
+ const g = createBitmap(3, 1, 0x00ff0000);
26
+ const b = createBitmap(3, 1, 0x0000ff00);
27
+ const a = createBitmap(3, 1, 0x000000ff);
28
+ const out = createBitmap(3, 1);
29
+ // Restrict r region to width=1 — only pixel 0 should be written.
30
+ mergeBitmapChannels(
31
+ createBitmapRegion(out),
32
+ { bitmap: r, x: 0, y: 0, width: 1, height: 1 },
33
+ createBitmapRegion(g),
34
+ createBitmapRegion(b),
35
+ createBitmapRegion(a),
36
+ );
37
+ // Pixel 0 is covered; pixel 1 should be untouched (0x00000000).
38
+ expect((getBitmapPixel(out, 0, 0) >>> 24) & 0xff).toBe(0xff);
39
+ expect(getBitmapPixel(out, 1, 0)).toBe(0x00000000);
40
+ });
41
+ });
42
+
43
+ describe('splitBitmapChannels', () => {
44
+ it('returns four bitmaps of the same dimensions', () => {
45
+ const src = createBitmap(3, 2, 0x112233ff);
46
+ const [r, g, b, a] = splitBitmapChannels(src);
47
+ expect(r.width).toBe(3);
48
+ expect(r.height).toBe(2);
49
+ expect(g.width).toBe(3);
50
+ expect(b.height).toBe(2);
51
+ expect(a.width).toBe(3);
52
+ });
53
+
54
+ it('each channel bitmap is a grayscale copy of that channel', () => {
55
+ const src = createBitmap(1, 1);
56
+ setBitmapPixel(src, 0, 0, 0x11_22_33_44);
57
+ const [r, g, b, a] = splitBitmapChannels(src);
58
+ // R bitmap: R=G=B=0x11, A=0xff.
59
+ expect((getBitmapPixel(r, 0, 0) >>> 24) & 0xff).toBe(0x11);
60
+ expect((getBitmapPixel(r, 0, 0) >> 16) & 0xff).toBe(0x11);
61
+ expect((getBitmapPixel(r, 0, 0) >> 8) & 0xff).toBe(0x11);
62
+ expect(getBitmapPixel(r, 0, 0) & 0xff).toBe(0xff);
63
+ // G bitmap: R=G=B=0x22, A=0xff.
64
+ expect((getBitmapPixel(g, 0, 0) >>> 24) & 0xff).toBe(0x22);
65
+ // B bitmap: R=G=B=0x33, A=0xff.
66
+ expect((getBitmapPixel(b, 0, 0) >>> 24) & 0xff).toBe(0x33);
67
+ // A bitmap: R=G=B=A=0x44 (alpha stored in all channels for round-trip fidelity).
68
+ expect((getBitmapPixel(a, 0, 0) >>> 24) & 0xff).toBe(0x44);
69
+ expect(getBitmapPixel(a, 0, 0) & 0xff).toBe(0x44);
70
+ });
71
+
72
+ it('returns distinct bitmap objects', () => {
73
+ const src = createBitmap(2, 2, 0xffffffff);
74
+ const [r, g, b, a] = splitBitmapChannels(src);
75
+ expect(r).not.toBe(src);
76
+ expect(r).not.toBe(g);
77
+ expect(g).not.toBe(b);
78
+ expect(b).not.toBe(a);
79
+ });
80
+
81
+ it('round-trip: split then merge reconstructs the original', () => {
82
+ const src = createBitmap(2, 2);
83
+ setBitmapPixel(src, 0, 0, 0xaabbccdd);
84
+ setBitmapPixel(src, 1, 0, 0x11223344);
85
+ const [r, g, b, a] = splitBitmapChannels(src);
86
+ const out = createBitmap(2, 2);
87
+ mergeBitmapChannels(
88
+ createBitmapRegion(out),
89
+ createBitmapRegion(r),
90
+ createBitmapRegion(g),
91
+ createBitmapRegion(b),
92
+ createBitmapRegion(a),
93
+ );
94
+ expect(getBitmapPixel(out, 0, 0)).toBe(0xaabbccdd);
95
+ expect(getBitmapPixel(out, 1, 0)).toBe(0x11223344);
96
+ });
97
+ });
@@ -0,0 +1,214 @@
1
+ import { createBitmap } from './bitmap';
2
+ import {
3
+ buildBitmapBrightnessColorMatrix,
4
+ buildBitmapContrastColorMatrix,
5
+ buildBitmapGrayscaleColorMatrix,
6
+ buildBitmapHueRotationColorMatrix,
7
+ buildBitmapInvertColorMatrix,
8
+ buildBitmapSaturationColorMatrix,
9
+ buildBitmapSepiaColorMatrix,
10
+ colorMatrixBitmap,
11
+ concatBitmapColorMatrix,
12
+ setBitmapColorMatrixIdentity,
13
+ } from './bitmapColorMatrix';
14
+
15
+ function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
16
+ return { bitmap, x, y, width, height };
17
+ }
18
+
19
+ // Build a 1x1 bitmap, apply `matrix`, and return its RGBA bytes.
20
+ function applyTo(rgba: number, matrix: ReadonlyArray<number>): Uint8ClampedArray {
21
+ const bitmap = createBitmap(1, 1, rgba);
22
+ const out = new Uint8ClampedArray(4);
23
+ colorMatrixBitmap(out, region(bitmap), matrix);
24
+ return out;
25
+ }
26
+
27
+ describe('buildBitmapBrightnessColorMatrix', () => {
28
+ it('multiplies RGB by the amount', () => {
29
+ const m = new Array<number>(20);
30
+ buildBitmapBrightnessColorMatrix(m, 0.5);
31
+ const out = applyTo(0xc86432ff, m); // R=200 G=100 B=50
32
+ expect(out[0]).toBe(100);
33
+ expect(out[1]).toBe(50);
34
+ expect(out[2]).toBe(25);
35
+ expect(out[3]).toBe(0xff);
36
+ });
37
+ });
38
+
39
+ describe('buildBitmapContrastColorMatrix', () => {
40
+ it('amount 0 flattens every channel to mid grey', () => {
41
+ const m = new Array<number>(20);
42
+ buildBitmapContrastColorMatrix(m, 0);
43
+ const out = applyTo(0xc80000ff, m); // R=200
44
+ expect(out[0]).toBe(128); // round(127.5)
45
+ expect(out[1]).toBe(128);
46
+ });
47
+ });
48
+
49
+ describe('buildBitmapGrayscaleColorMatrix', () => {
50
+ it('maps pure red to its luma in every channel', () => {
51
+ const m = new Array<number>(20);
52
+ buildBitmapGrayscaleColorMatrix(m);
53
+ const out = applyTo(0xff0000ff, m);
54
+ expect(out[0]).toBe(54); // round(0.213 * 255)
55
+ expect(out[1]).toBe(54);
56
+ expect(out[2]).toBe(54);
57
+ });
58
+ });
59
+
60
+ describe('buildBitmapHueRotationColorMatrix', () => {
61
+ it('leaves grey unchanged at any angle (rotation around the luma axis)', () => {
62
+ const m = new Array<number>(20);
63
+ // 70.7° ≈ 1.234 radians — same geometric rotation, now expressed in degrees
64
+ buildBitmapHueRotationColorMatrix(m, 70.7);
65
+ const out = applyTo(0x808080ff, m);
66
+ expect(out[0]).toBe(128);
67
+ expect(out[1]).toBe(128);
68
+ expect(out[2]).toBe(128);
69
+ });
70
+
71
+ it('is identity at 0 degrees', () => {
72
+ const m = new Array<number>(20);
73
+ buildBitmapHueRotationColorMatrix(m, 0);
74
+ const out = applyTo(0xc86432ff, m);
75
+ expect(out[0]).toBe(200);
76
+ expect(out[1]).toBe(100);
77
+ expect(out[2]).toBe(50);
78
+ });
79
+
80
+ it('is identity at 360 degrees', () => {
81
+ const m = new Array<number>(20);
82
+ buildBitmapHueRotationColorMatrix(m, 360);
83
+ const out = applyTo(0xc86432ff, m);
84
+ expect(out[0]).toBeCloseTo(200, 0);
85
+ expect(out[1]).toBeCloseTo(100, 0);
86
+ expect(out[2]).toBeCloseTo(50, 0);
87
+ });
88
+ });
89
+
90
+ describe('buildBitmapInvertColorMatrix', () => {
91
+ it('inverts RGB and preserves alpha', () => {
92
+ const m = new Array<number>(20);
93
+ buildBitmapInvertColorMatrix(m);
94
+ const out = applyTo(0xc83200ff, m); // R=200 G=50 B=0
95
+ expect(out[0]).toBe(55);
96
+ expect(out[1]).toBe(205);
97
+ expect(out[2]).toBe(255);
98
+ expect(out[3]).toBe(0xff);
99
+ });
100
+ });
101
+
102
+ describe('buildBitmapSaturationColorMatrix', () => {
103
+ it('amount 1 is identity', () => {
104
+ const m = new Array<number>(20);
105
+ buildBitmapSaturationColorMatrix(m, 1);
106
+ const out = applyTo(0xc86432ff, m);
107
+ expect(out[0]).toBe(200);
108
+ expect(out[1]).toBe(100);
109
+ expect(out[2]).toBe(50);
110
+ });
111
+
112
+ it('amount 0 equals grayscale', () => {
113
+ const m = new Array<number>(20);
114
+ buildBitmapSaturationColorMatrix(m, 0);
115
+ const out = applyTo(0xff0000ff, m);
116
+ expect(out[0]).toBe(54);
117
+ expect(out[1]).toBe(54);
118
+ });
119
+ });
120
+
121
+ describe('buildBitmapSepiaColorMatrix', () => {
122
+ it('maps pure red to the sepia tone', () => {
123
+ const m = new Array<number>(20);
124
+ buildBitmapSepiaColorMatrix(m);
125
+ const out = applyTo(0xff0000ff, m);
126
+ expect(out[0]).toBe(100); // round(0.393 * 255)
127
+ expect(out[1]).toBe(89); // round(0.349 * 255)
128
+ expect(out[2]).toBe(69); // round(0.272 * 255)
129
+ });
130
+ });
131
+
132
+ describe('colorMatrixBitmap', () => {
133
+ it('applies a 4x5 color matrix to the source region', () => {
134
+ const source = createBitmap(1, 1, 0x204060ff);
135
+ const out = new Uint8ClampedArray(4);
136
+ colorMatrixBitmap(out, region(source), [0, 0, 0, 0, 10, 0, 0, 0, 0, 20, 0, 0, 0, 0, 30, 0, 0, 0, 1, 0]);
137
+ expect(out[0]).toBe(10);
138
+ expect(out[1]).toBe(20);
139
+ expect(out[2]).toBe(30);
140
+ expect(out[3]).toBe(0xff);
141
+ });
142
+
143
+ it('is safe when out aliases source.bitmap.data for a full-bitmap region', () => {
144
+ const bitmap = createBitmap(1, 1, 0x010203ff);
145
+ colorMatrixBitmap(bitmap.data, region(bitmap), [1, 0, 0, 0, 10, 0, 1, 0, 0, 20, 0, 0, 1, 0, 30, 0, 0, 0, 1, 0]);
146
+ expect(bitmap.data[0]).toBe(11);
147
+ expect(bitmap.data[1]).toBe(22);
148
+ expect(bitmap.data[2]).toBe(33);
149
+ expect(bitmap.data[3]).toBe(0xff);
150
+ });
151
+
152
+ it('clamps output to [0, 255]', () => {
153
+ const source = createBitmap(1, 1, 0xff0000ff);
154
+ const out = new Uint8ClampedArray(4);
155
+ colorMatrixBitmap(out, region(source), [10, 0, 0, 0, 100, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]);
156
+ expect(out[0]).toBe(255);
157
+ expect(out[1]).toBe(0);
158
+ });
159
+
160
+ it('throws when the matrix is too short', () => {
161
+ const source = createBitmap(1, 1);
162
+ const out = new Uint8ClampedArray(4);
163
+ expect(() => colorMatrixBitmap(out, region(source), [])).toThrow('Color matrix filter requires 20 values');
164
+ });
165
+
166
+ it('silently skips pixels outside source bounds', () => {
167
+ const source = createBitmap(1, 1, 0xff0000ff);
168
+ const out = new Uint8ClampedArray(4 * 4);
169
+ const identity = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
170
+ colorMatrixBitmap(out, region(source, -1, -1, 2, 2), identity);
171
+ // only bottom-right pixel of the 2x2 region is in-bounds
172
+ const i = (1 * 2 + 1) * 4;
173
+ expect(out[i]).toBe(0xff);
174
+ expect(out[0]).toBe(0);
175
+ });
176
+ });
177
+
178
+ describe('concatBitmapColorMatrix', () => {
179
+ it('composes two matrices (apply first, then second)', () => {
180
+ const bright2 = new Array<number>(20);
181
+ const half = new Array<number>(20);
182
+ buildBitmapBrightnessColorMatrix(bright2, 2);
183
+ buildBitmapBrightnessColorMatrix(half, 0.5);
184
+ const out = new Array<number>(20);
185
+ concatBitmapColorMatrix(out, bright2, half); // *2 then *0.5 = identity
186
+ const pixel = applyTo(0xc86432ff, out);
187
+ expect(pixel[0]).toBe(200);
188
+ expect(pixel[1]).toBe(100);
189
+ expect(pixel[2]).toBe(50);
190
+ });
191
+
192
+ it('carries the offset through composition', () => {
193
+ const identity = new Array<number>(20);
194
+ const invert = new Array<number>(20);
195
+ setBitmapColorMatrixIdentity(identity);
196
+ buildBitmapInvertColorMatrix(invert);
197
+ const out = new Array<number>(20);
198
+ concatBitmapColorMatrix(out, identity, invert);
199
+ const pixel = applyTo(0xc80000ff, out); // R=200 -> 55
200
+ expect(pixel[0]).toBe(55);
201
+ });
202
+ });
203
+
204
+ describe('setBitmapColorMatrixIdentity', () => {
205
+ it('leaves the pixel unchanged', () => {
206
+ const m = new Array<number>(20);
207
+ setBitmapColorMatrixIdentity(m);
208
+ const out = applyTo(0xc86432ff, m);
209
+ expect(out[0]).toBe(200);
210
+ expect(out[1]).toBe(100);
211
+ expect(out[2]).toBe(50);
212
+ expect(out[3]).toBe(0xff);
213
+ });
214
+ });
@@ -0,0 +1,92 @@
1
+ import type { Bitmap } from '@flighthq/types/contract';
2
+
3
+ import { cloneBitmap, createBitmap } from './bitmap';
4
+ import { compareBitmap, getBitmapMismatch } from './bitmapCompare';
5
+ import { setBitmapPixel } from './bitmapPixel';
6
+
7
+ describe('compareBitmap', () => {
8
+ it('throws when widths differ', () => {
9
+ const a = createBitmap(4, 4);
10
+ const b = createBitmap(8, 4);
11
+ expect(() => compareBitmap(a, b)).toThrow();
12
+ });
13
+
14
+ it('throws when heights differ', () => {
15
+ const a = createBitmap(4, 4);
16
+ const b = createBitmap(4, 8);
17
+ expect(() => compareBitmap(a, b)).toThrow();
18
+ });
19
+
20
+ it('returns null for identical images', () => {
21
+ const a = createBitmap(4, 4, 0x0000ffff);
22
+ const b = cloneBitmap(a);
23
+ expect(compareBitmap(a, b)).toBeNull();
24
+ });
25
+
26
+ it('returns diff Bitmap for different pixels', () => {
27
+ const a = createBitmap(2, 1, 0x000000ff);
28
+ const b = createBitmap(2, 1, 0x000000ff);
29
+ setBitmapPixel(b, 0, 0, 0x102030ff);
30
+ const result = compareBitmap(a, b) as Bitmap;
31
+ expect(result.width).toBe(2);
32
+ expect(result.height).toBe(1);
33
+ expect(result.data[0]).toBe(0x10);
34
+ expect(result.data[1]).toBe(0x20);
35
+ expect(result.data[2]).toBe(0x30);
36
+ expect(result.data[3]).toBe(255);
37
+ expect(result.data[4]).toBe(0);
38
+ expect(result.data[5]).toBe(0);
39
+ expect(result.data[6]).toBe(0);
40
+ expect(result.data[7]).toBe(0);
41
+ });
42
+
43
+ it('diff pixel alpha is 255 when any channel differs', () => {
44
+ const a = createBitmap(1, 1, 0x000000ff);
45
+ const b = createBitmap(1, 1, 0x00000080);
46
+ const result = compareBitmap(a, b) as Bitmap;
47
+ expect(result.data[3]).toBe(255);
48
+ });
49
+
50
+ it('unchanged pixels in diff have zero alpha', () => {
51
+ const a = createBitmap(2, 1, 0x000000ff);
52
+ const b = cloneBitmap(a);
53
+ setBitmapPixel(b, 1, 0, 0xff0000ff);
54
+ const result = compareBitmap(a, b) as Bitmap;
55
+ expect(result.data[3]).toBe(0);
56
+ expect(result.data[7]).toBe(255);
57
+ });
58
+ });
59
+
60
+ describe('getBitmapMismatch', () => {
61
+ it('throws when dimensions differ', () => {
62
+ expect(() => getBitmapMismatch(createBitmap(4, 4), createBitmap(4, 8))).toThrow();
63
+ });
64
+
65
+ it('reports zero mismatch for identical bitmaps', () => {
66
+ const a = createBitmap(4, 4, 0x0000ffff);
67
+ const result = getBitmapMismatch(a, cloneBitmap(a));
68
+ expect(result.mismatchedPixels).toBe(0);
69
+ expect(result.totalPixels).toBe(16);
70
+ expect(result.fraction).toBe(0);
71
+ expect(result.maxChannelDelta).toBe(0);
72
+ });
73
+
74
+ it('counts pixels whose max channel delta exceeds the tolerance', () => {
75
+ const a = createBitmap(2, 1, 0x000000ff);
76
+ const b = createBitmap(2, 1, 0x000000ff);
77
+ setBitmapPixel(b, 0, 0, 0x0a0000ff); // delta 10 on one channel
78
+ expect(getBitmapMismatch(a, b, 0).mismatchedPixels).toBe(1);
79
+ expect(getBitmapMismatch(a, b, 9).mismatchedPixels).toBe(1);
80
+ expect(getBitmapMismatch(a, b, 10).mismatchedPixels).toBe(0); // within tolerance
81
+ });
82
+
83
+ it('reports the largest channel delta and the mismatch fraction', () => {
84
+ const a = createBitmap(2, 1, 0x000000ff);
85
+ const b = createBitmap(2, 1, 0x000000ff);
86
+ setBitmapPixel(b, 1, 0, 0x804020ff); // max delta 0x80 = 128
87
+ const result = getBitmapMismatch(a, b);
88
+ expect(result.maxChannelDelta).toBe(128);
89
+ expect(result.mismatchedPixels).toBe(1);
90
+ expect(result.fraction).toBe(0.5);
91
+ });
92
+ });
@@ -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
+ });