@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.
- package/dist/bitmap.d.ts +17 -0
- package/dist/bitmap.d.ts.map +1 -0
- package/dist/bitmap.js +87 -0
- package/dist/bitmap.js.map +1 -0
- package/dist/bitmapAffine.d.ts +27 -0
- package/dist/bitmapAffine.d.ts.map +1 -0
- package/dist/bitmapAffine.js +159 -0
- package/dist/bitmapAffine.js.map +1 -0
- package/dist/bitmapAlpha.d.ts +27 -0
- package/dist/bitmapAlpha.d.ts.map +1 -0
- package/dist/bitmapAlpha.js +83 -0
- package/dist/bitmapAlpha.js.map +1 -0
- package/dist/bitmapBevel.d.ts +24 -0
- package/dist/bitmapBevel.d.ts.map +1 -0
- package/dist/bitmapBevel.js +102 -0
- package/dist/bitmapBevel.js.map +1 -0
- package/dist/bitmapBlur.d.ts +57 -0
- package/dist/bitmapBlur.d.ts.map +1 -0
- package/dist/bitmapBlur.js +263 -0
- package/dist/bitmapBlur.js.map +1 -0
- package/dist/bitmapChannel.d.ts +24 -0
- package/dist/bitmapChannel.d.ts.map +1 -0
- package/dist/bitmapChannel.js +127 -0
- package/dist/bitmapChannel.js.map +1 -0
- package/dist/bitmapColorMatrix.d.ts +57 -0
- package/dist/bitmapColorMatrix.d.ts.map +1 -0
- package/dist/bitmapColorMatrix.js +127 -0
- package/dist/bitmapColorMatrix.js.map +1 -0
- package/dist/bitmapCompare.d.ts +18 -0
- package/dist/bitmapCompare.d.ts.map +1 -0
- package/dist/bitmapCompare.js +64 -0
- package/dist/bitmapCompare.js.map +1 -0
- package/dist/bitmapComposite.d.ts +54 -0
- package/dist/bitmapComposite.d.ts.map +1 -0
- package/dist/bitmapComposite.js +262 -0
- package/dist/bitmapComposite.js.map +1 -0
- package/dist/bitmapConvolution.d.ts +17 -0
- package/dist/bitmapConvolution.d.ts.map +1 -0
- package/dist/bitmapConvolution.js +103 -0
- package/dist/bitmapConvolution.js.map +1 -0
- package/dist/bitmapCopy.d.ts +23 -0
- package/dist/bitmapCopy.d.ts.map +1 -0
- package/dist/bitmapCopy.js +78 -0
- package/dist/bitmapCopy.js.map +1 -0
- package/dist/bitmapCoverage.d.ts +11 -0
- package/dist/bitmapCoverage.d.ts.map +1 -0
- package/dist/bitmapCoverage.js +29 -0
- package/dist/bitmapCoverage.js.map +1 -0
- package/dist/bitmapCrop.d.ts +33 -0
- package/dist/bitmapCrop.d.ts.map +1 -0
- package/dist/bitmapCrop.js +175 -0
- package/dist/bitmapCrop.js.map +1 -0
- package/dist/bitmapDisplacement.d.ts +22 -0
- package/dist/bitmapDisplacement.d.ts.map +1 -0
- package/dist/bitmapDisplacement.js +111 -0
- package/dist/bitmapDisplacement.js.map +1 -0
- package/dist/bitmapDissolve.d.ts +21 -0
- package/dist/bitmapDissolve.d.ts.map +1 -0
- package/dist/bitmapDissolve.js +97 -0
- package/dist/bitmapDissolve.js.map +1 -0
- package/dist/bitmapDraw.d.ts +8 -0
- package/dist/bitmapDraw.d.ts.map +1 -0
- package/dist/bitmapDraw.js +14 -0
- package/dist/bitmapDraw.js.map +1 -0
- package/dist/bitmapEncode.d.ts +3 -0
- package/dist/bitmapEncode.d.ts.map +1 -0
- package/dist/bitmapEncode.js +18 -0
- package/dist/bitmapEncode.js.map +1 -0
- package/dist/bitmapFill.d.ts +12 -0
- package/dist/bitmapFill.d.ts.map +1 -0
- package/dist/bitmapFill.js +85 -0
- package/dist/bitmapFill.js.map +1 -0
- package/dist/bitmapFingerprint.d.ts +26 -0
- package/dist/bitmapFingerprint.d.ts.map +1 -0
- package/dist/bitmapFingerprint.js +99 -0
- package/dist/bitmapFingerprint.js.map +1 -0
- package/dist/bitmapFlip.d.ts +16 -0
- package/dist/bitmapFlip.d.ts.map +1 -0
- package/dist/bitmapFlip.js +98 -0
- package/dist/bitmapFlip.js.map +1 -0
- package/dist/bitmapFormat.d.ts +26 -0
- package/dist/bitmapFormat.d.ts.map +1 -0
- package/dist/bitmapFormat.js +80 -0
- package/dist/bitmapFormat.js.map +1 -0
- package/dist/bitmapFrom.d.ts +23 -0
- package/dist/bitmapFrom.d.ts.map +1 -0
- package/dist/bitmapFrom.js +90 -0
- package/dist/bitmapFrom.js.map +1 -0
- package/dist/bitmapGradient.d.ts +36 -0
- package/dist/bitmapGradient.d.ts.map +1 -0
- package/dist/bitmapGradient.js +176 -0
- package/dist/bitmapGradient.js.map +1 -0
- package/dist/bitmapGradientFill.d.ts +32 -0
- package/dist/bitmapGradientFill.d.ts.map +1 -0
- package/dist/bitmapGradientFill.js +120 -0
- package/dist/bitmapGradientFill.js.map +1 -0
- package/dist/bitmapHistogram.d.ts +20 -0
- package/dist/bitmapHistogram.d.ts.map +1 -0
- package/dist/bitmapHistogram.js +61 -0
- package/dist/bitmapHistogram.js.map +1 -0
- package/dist/bitmapImageChannel.d.ts +2 -0
- package/dist/bitmapImageChannel.d.ts.map +1 -0
- package/dist/bitmapImageChannel.js +2 -0
- package/dist/bitmapImageChannel.js.map +1 -0
- package/dist/bitmapMedian.d.ts +14 -0
- package/dist/bitmapMedian.d.ts.map +1 -0
- package/dist/bitmapMedian.js +77 -0
- package/dist/bitmapMedian.js.map +1 -0
- package/dist/bitmapMorphological.d.ts +29 -0
- package/dist/bitmapMorphological.d.ts.map +1 -0
- package/dist/bitmapMorphological.js +81 -0
- package/dist/bitmapMorphological.js.map +1 -0
- package/dist/bitmapNoise.d.ts +48 -0
- package/dist/bitmapNoise.d.ts.map +1 -0
- package/dist/bitmapNoise.js +245 -0
- package/dist/bitmapNoise.js.map +1 -0
- package/dist/bitmapPaletteMap.d.ts +17 -0
- package/dist/bitmapPaletteMap.d.ts.map +1 -0
- package/dist/bitmapPaletteMap.js +45 -0
- package/dist/bitmapPaletteMap.js.map +1 -0
- package/dist/bitmapPixel.d.ts +9 -0
- package/dist/bitmapPixel.d.ts.map +1 -0
- package/dist/bitmapPixel.js +36 -0
- package/dist/bitmapPixel.js.map +1 -0
- package/dist/bitmapPixelate.d.ts +13 -0
- package/dist/bitmapPixelate.d.ts.map +1 -0
- package/dist/bitmapPixelate.js +61 -0
- package/dist/bitmapPixelate.js.map +1 -0
- package/dist/bitmapQuery.d.ts +13 -0
- package/dist/bitmapQuery.d.ts.map +1 -0
- package/dist/bitmapQuery.js +46 -0
- package/dist/bitmapQuery.js.map +1 -0
- package/dist/bitmapRegion.d.ts +20 -0
- package/dist/bitmapRegion.d.ts.map +1 -0
- package/dist/bitmapRegion.js +28 -0
- package/dist/bitmapRegion.js.map +1 -0
- package/dist/bitmapResize.d.ts +17 -0
- package/dist/bitmapResize.d.ts.map +1 -0
- package/dist/bitmapResize.js +191 -0
- package/dist/bitmapResize.js.map +1 -0
- package/dist/bitmapRotate.d.ts +32 -0
- package/dist/bitmapRotate.d.ts.map +1 -0
- package/dist/bitmapRotate.js +143 -0
- package/dist/bitmapRotate.js.map +1 -0
- package/dist/bitmapShadow.d.ts +72 -0
- package/dist/bitmapShadow.d.ts.map +1 -0
- package/dist/bitmapShadow.js +175 -0
- package/dist/bitmapShadow.js.map +1 -0
- package/dist/bitmapSharpen.d.ts +16 -0
- package/dist/bitmapSharpen.d.ts.map +1 -0
- package/dist/bitmapSharpen.js +47 -0
- package/dist/bitmapSharpen.js.map +1 -0
- package/dist/bitmapTone.d.ts +28 -0
- package/dist/bitmapTone.d.ts.map +1 -0
- package/dist/bitmapTone.js +69 -0
- package/dist/bitmapTone.js.map +1 -0
- package/dist/bitmapTransform.d.ts +40 -0
- package/dist/bitmapTransform.d.ts.map +1 -0
- package/dist/bitmapTransform.js +163 -0
- package/dist/bitmapTransform.js.map +1 -0
- package/dist/bitmapWarp.d.ts +48 -0
- package/dist/bitmapWarp.d.ts.map +1 -0
- package/dist/bitmapWarp.js +284 -0
- package/dist/bitmapWarp.js.map +1 -0
- package/dist/contract.d.ts +43 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +43 -0
- package/dist/contract.js.map +1 -0
- package/dist/explainBitmapReadback.d.ts +3 -0
- package/dist/explainBitmapReadback.d.ts.map +1 -0
- package/dist/explainBitmapReadback.js +39 -0
- package/dist/explainBitmapReadback.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/bitmap.test.ts +88 -0
- package/src/bitmapAffine.test.ts +98 -0
- package/src/bitmapAlpha.test.ts +110 -0
- package/src/bitmapBevel.test.ts +61 -0
- package/src/bitmapBlur.test.ts +234 -0
- package/src/bitmapChannel.test.ts +97 -0
- package/src/bitmapColorMatrix.test.ts +214 -0
- package/src/bitmapCompare.test.ts +92 -0
- package/src/bitmapComposite.test.ts +259 -0
- package/src/bitmapConvolution.test.ts +130 -0
- package/src/bitmapCopy.test.ts +48 -0
- package/src/bitmapCoverage.test.ts +39 -0
- package/src/bitmapCrop.test.ts +116 -0
- package/src/bitmapDisplacement.test.ts +181 -0
- package/src/bitmapDissolve.test.ts +99 -0
- package/src/bitmapDraw.test.ts +68 -0
- package/src/bitmapEncode.test.ts +18 -0
- package/src/bitmapFill.test.ts +83 -0
- package/src/bitmapFingerprint.test.ts +103 -0
- package/src/bitmapFlip.test.ts +53 -0
- package/src/bitmapFormat.test.ts +124 -0
- package/src/bitmapFrom.test.ts +86 -0
- package/src/bitmapGradient.test.ts +85 -0
- package/src/bitmapGradientFill.test.ts +107 -0
- package/src/bitmapHistogram.test.ts +64 -0
- package/src/bitmapImageChannel.test.ts +19 -0
- package/src/bitmapMedian.test.ts +39 -0
- package/src/bitmapMorphological.test.ts +69 -0
- package/src/bitmapNoise.test.ts +163 -0
- package/src/bitmapPaletteMap.test.ts +49 -0
- package/src/bitmapPixel.test.ts +117 -0
- package/src/bitmapPixelate.test.ts +40 -0
- package/src/bitmapQuery.test.ts +42 -0
- package/src/bitmapRegion.test.ts +34 -0
- package/src/bitmapResize.test.ts +106 -0
- package/src/bitmapRotate.test.ts +108 -0
- package/src/bitmapShadow.test.ts +159 -0
- package/src/bitmapSharpen.test.ts +44 -0
- package/src/bitmapTone.test.ts +104 -0
- package/src/bitmapTransform.test.ts +128 -0
- package/src/bitmapWarp.test.ts +118 -0
- package/src/explainBitmapReadback.test.ts +84 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import {
|
|
3
|
+
blurBitmapPixelsHorizontal,
|
|
4
|
+
blurBitmapPixelsHorizontalWeighted,
|
|
5
|
+
blurBitmapPixelsVertical,
|
|
6
|
+
blurBitmapPixelsVerticalWeighted,
|
|
7
|
+
boxBlurBitmap,
|
|
8
|
+
computeGaussianKernel,
|
|
9
|
+
gaussianBlurBitmap,
|
|
10
|
+
} from './bitmapBlur';
|
|
11
|
+
import { premultiplyBitmapPixels, unpremultiplyBitmapPixels } from './bitmapFormat';
|
|
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('blurBitmapPixelsHorizontal', () => {
|
|
18
|
+
it('spreads alpha into horizontal neighbors', () => {
|
|
19
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
20
|
+
const out = new Uint8ClampedArray(12);
|
|
21
|
+
blurBitmapPixelsHorizontal(out, source, 3, 1, 1);
|
|
22
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
23
|
+
expect(out[11]).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('copies source to out without spreading when radius is 0', () => {
|
|
27
|
+
const source = new Uint8ClampedArray([0, 0, 0, 255, 0, 0, 0, 0]);
|
|
28
|
+
const out = new Uint8ClampedArray(8);
|
|
29
|
+
blurBitmapPixelsHorizontal(out, source, 2, 1, 0);
|
|
30
|
+
expect(out[3]).toBe(255);
|
|
31
|
+
expect(out[7]).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('premultiplying before blur avoids the dark-halo artifact at transparent edges', () => {
|
|
35
|
+
// [opaque red, transparent]. Straight-alpha blur bleeds the transparent
|
|
36
|
+
// pixel's hidden black into the red; a premultiplied blur keeps it pure red.
|
|
37
|
+
const buf = new Uint8ClampedArray([255, 0, 0, 255, 0, 0, 0, 0]);
|
|
38
|
+
|
|
39
|
+
const straight = new Uint8ClampedArray(8);
|
|
40
|
+
blurBitmapPixelsHorizontal(straight, buf, 2, 1, 1);
|
|
41
|
+
expect(straight[4]).toBe(128); // transparent pixel's red darkened toward black
|
|
42
|
+
|
|
43
|
+
const premul = new Uint8ClampedArray(8);
|
|
44
|
+
premultiplyBitmapPixels(premul, buf, 8);
|
|
45
|
+
const blurred = new Uint8ClampedArray(8);
|
|
46
|
+
blurBitmapPixelsHorizontal(blurred, premul, 2, 1, 1);
|
|
47
|
+
const result = new Uint8ClampedArray(8);
|
|
48
|
+
unpremultiplyBitmapPixels(result, blurred, 8);
|
|
49
|
+
expect(result[4]).toBe(255); // red stays pure
|
|
50
|
+
expect(result[7]).toBe(128); // alpha still half
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('produces correct edge average for a single row', () => {
|
|
54
|
+
// pixels: [0, 255, 0] alpha only; radius=1
|
|
55
|
+
// x=0: avg([0,255])/2 = 128 (approx); x=1: avg([0,255,0])/3 = 85; x=2: avg([255,0])/2 = 128
|
|
56
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
57
|
+
const out = new Uint8ClampedArray(12);
|
|
58
|
+
blurBitmapPixelsHorizontal(out, source, 3, 1, 1);
|
|
59
|
+
expect(out[3]).toBe(128); // (0+255)/2
|
|
60
|
+
expect(out[7]).toBe(85); // (0+255+0)/3
|
|
61
|
+
expect(out[11]).toBe(128); // (255+0)/2
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('blurBitmapPixelsHorizontalWeighted', () => {
|
|
66
|
+
it('applies Gaussian weights to horizontal neighbors', () => {
|
|
67
|
+
const kernel = new Float32Array(3);
|
|
68
|
+
computeGaussianKernel(kernel, 1, 0.8);
|
|
69
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
70
|
+
const out = new Uint8ClampedArray(12);
|
|
71
|
+
blurBitmapPixelsHorizontalWeighted(out, source, 3, 1, kernel);
|
|
72
|
+
// center weight >> edge; center alpha should be highest
|
|
73
|
+
expect(out[7]).toBeGreaterThan(out[3]);
|
|
74
|
+
expect(out[7]).toBeGreaterThan(out[11]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('with an identity kernel [1] leaves source unchanged', () => {
|
|
78
|
+
const kernel = new Float32Array([1]);
|
|
79
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
|
|
80
|
+
const out = new Uint8ClampedArray(4);
|
|
81
|
+
blurBitmapPixelsHorizontalWeighted(out, source, 1, 1, kernel);
|
|
82
|
+
expect(out[0]).toBe(0x11);
|
|
83
|
+
expect(out[3]).toBe(0xff);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('blurBitmapPixelsVertical', () => {
|
|
88
|
+
it('spreads alpha into vertical neighbors', () => {
|
|
89
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
90
|
+
const out = new Uint8ClampedArray(12);
|
|
91
|
+
blurBitmapPixelsVertical(out, source, 1, 3, 1);
|
|
92
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
93
|
+
expect(out[11]).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('blurBitmapPixelsVerticalWeighted', () => {
|
|
98
|
+
it('applies Gaussian weights to vertical neighbors', () => {
|
|
99
|
+
const kernel = new Float32Array(3);
|
|
100
|
+
computeGaussianKernel(kernel, 1, 0.8);
|
|
101
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
102
|
+
const out = new Uint8ClampedArray(12);
|
|
103
|
+
blurBitmapPixelsVerticalWeighted(out, source, 1, 3, kernel);
|
|
104
|
+
expect(out[7]).toBeGreaterThan(out[3]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('boxBlurBitmap', () => {
|
|
109
|
+
it('spreads alpha into neighboring pixels', () => {
|
|
110
|
+
const source = createBitmap(3, 1);
|
|
111
|
+
source.data[7] = 255; // center pixel opaque
|
|
112
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
113
|
+
const scratch = new Uint8ClampedArray(3 * 4);
|
|
114
|
+
boxBlurBitmap(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
|
|
115
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
116
|
+
expect(out[11]).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('writes source into out when blur is zero', () => {
|
|
120
|
+
const source = createBitmap(1, 1, 0x336699ff);
|
|
121
|
+
const out = new Uint8ClampedArray(4);
|
|
122
|
+
const scratch = new Uint8ClampedArray(4);
|
|
123
|
+
boxBlurBitmap(out, scratch, region(source), { radiusX: 0, radiusY: 0 });
|
|
124
|
+
expect(out[0]).toBe(0x33);
|
|
125
|
+
expect(out[3]).toBe(0xff);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('result always lands in out regardless of pass parity', () => {
|
|
129
|
+
const source = createBitmap(3, 3, 0xffffff88);
|
|
130
|
+
const out = new Uint8ClampedArray(3 * 3 * 4);
|
|
131
|
+
const scratch = new Uint8ClampedArray(3 * 3 * 4);
|
|
132
|
+
boxBlurBitmap(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
|
|
133
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('source.bitmap.data can be used as out for a full-bitmap region', () => {
|
|
137
|
+
const bitmap = createBitmap(3, 1);
|
|
138
|
+
bitmap.data[7] = 255;
|
|
139
|
+
const scratch = new Uint8ClampedArray(3 * 4);
|
|
140
|
+
boxBlurBitmap(bitmap.data, scratch, region(bitmap), { radiusX: 2, radiusY: 0 });
|
|
141
|
+
expect(bitmap.data[3]).toBeGreaterThan(0);
|
|
142
|
+
expect(bitmap.data[11]).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('blurs only the offset sub-region, reading from the right source pixels', () => {
|
|
146
|
+
// 4 px alpha: [_, 255, 100, _]. Blurring region (1,0,2,1) extracts [255,100]
|
|
147
|
+
// and averages them — proving the source x-offset is honored, not ignored.
|
|
148
|
+
const bitmap = createBitmap(4, 1);
|
|
149
|
+
bitmap.data[1 * 4 + 3] = 255;
|
|
150
|
+
bitmap.data[2 * 4 + 3] = 100;
|
|
151
|
+
const out = new Uint8ClampedArray(2 * 4);
|
|
152
|
+
const scratch = new Uint8ClampedArray(2 * 4);
|
|
153
|
+
boxBlurBitmap(out, scratch, region(bitmap, 1, 0, 2, 1), { radiusX: 2, radiusY: 0 });
|
|
154
|
+
expect(out[3]).toBe(178); // round((255 + 100) / 2)
|
|
155
|
+
expect(out[7]).toBe(178);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('computeGaussianKernel', () => {
|
|
160
|
+
it('produces a kernel that sums to 1', () => {
|
|
161
|
+
const kernel = new Float32Array(7);
|
|
162
|
+
computeGaussianKernel(kernel, 3, 1.0);
|
|
163
|
+
const sum = kernel.reduce((acc, v) => acc + v, 0);
|
|
164
|
+
expect(sum).toBeCloseTo(1, 5);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('is symmetric', () => {
|
|
168
|
+
const kernel = new Float32Array(9);
|
|
169
|
+
computeGaussianKernel(kernel, 4, 1.5);
|
|
170
|
+
for (let i = 0; i < 4; i++) {
|
|
171
|
+
expect(kernel[i]).toBeCloseTo(kernel[8 - i], 10);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('center weight is the largest', () => {
|
|
176
|
+
const kernel = new Float32Array(5);
|
|
177
|
+
computeGaussianKernel(kernel, 2, 1.0);
|
|
178
|
+
expect(kernel[2]).toBeGreaterThan(kernel[1]);
|
|
179
|
+
expect(kernel[1]).toBeGreaterThan(kernel[0]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('radius-0 kernel produces a single weight of 1', () => {
|
|
183
|
+
const kernel = new Float32Array(1);
|
|
184
|
+
computeGaussianKernel(kernel, 0, 1.0);
|
|
185
|
+
expect(kernel[0]).toBeCloseTo(1, 10);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('sigma <= 0 produces a unit impulse rather than a NaN kernel', () => {
|
|
189
|
+
const kernel = new Float32Array(5).fill(9);
|
|
190
|
+
computeGaussianKernel(kernel, 2, 0);
|
|
191
|
+
expect(kernel[2]).toBe(1);
|
|
192
|
+
expect(kernel[0]).toBe(0);
|
|
193
|
+
expect(kernel[4]).toBe(0);
|
|
194
|
+
expect(kernel.every((v) => !Number.isNaN(v))).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('gaussianBlurBitmap', () => {
|
|
199
|
+
it('spreads alpha into neighboring pixels', () => {
|
|
200
|
+
const source = createBitmap(5, 1);
|
|
201
|
+
source.data[2 * 4 + 3] = 255; // center pixel opaque
|
|
202
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
203
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
204
|
+
gaussianBlurBitmap(out, scratch, region(source), 1.0);
|
|
205
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
206
|
+
expect(out[4 * 4 + 3]).toBeGreaterThan(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('center pixel retains more weight than edges', () => {
|
|
210
|
+
const source = createBitmap(5, 1);
|
|
211
|
+
source.data[2 * 4 + 3] = 255;
|
|
212
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
213
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
214
|
+
gaussianBlurBitmap(out, scratch, region(source), 0.8);
|
|
215
|
+
expect(out[2 * 4 + 3]).toBeGreaterThan(out[3]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('with sigma=0 writes source unchanged', () => {
|
|
219
|
+
const source = createBitmap(1, 1, 0x336699ff);
|
|
220
|
+
const out = new Uint8ClampedArray(4);
|
|
221
|
+
const scratch = new Uint8ClampedArray(4);
|
|
222
|
+
gaussianBlurBitmap(out, scratch, region(source), 0);
|
|
223
|
+
expect(out[0]).toBe(0x33);
|
|
224
|
+
expect(out[3]).toBe(0xff);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('result always lands in out regardless of pass parity', () => {
|
|
228
|
+
const source = createBitmap(3, 3, 0xffffff88);
|
|
229
|
+
const out = new Uint8ClampedArray(3 * 3 * 4);
|
|
230
|
+
const scratch = new Uint8ClampedArray(3 * 3 * 4);
|
|
231
|
+
gaussianBlurBitmap(out, scratch, region(source), 1.0);
|
|
232
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -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
|
+
});
|