@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.
- 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 +131 -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 +17 -0
- package/dist/bitmapFrom.d.ts.map +1 -0
- package/dist/bitmapFrom.js +67 -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 +42 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +42 -0
- package/dist/contract.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 +136 -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 +57 -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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { fillBitmapRectangle, floodFillBitmap } from './bitmapFill';
|
|
3
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
4
|
+
|
|
5
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
6
|
+
return { bitmap, x, y, width, height };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('fillBitmapRectangle', () => {
|
|
10
|
+
it('fills the specified region', () => {
|
|
11
|
+
const img = createBitmap(4, 4);
|
|
12
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
|
|
13
|
+
expect(getBitmapPixel(img, 1, 1)).toBe(0xaabbccff);
|
|
14
|
+
expect(getBitmapPixel(img, 2, 2)).toBe(0xaabbccff);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('bumps the bitmap version (self-invalidation)', () => {
|
|
18
|
+
const img = createBitmap(4, 4);
|
|
19
|
+
const before = img.version;
|
|
20
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
|
|
21
|
+
expect(img.version).toBe(before + 1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not affect pixels outside the region', () => {
|
|
25
|
+
const img = createBitmap(4, 4);
|
|
26
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
|
|
27
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0x00000000);
|
|
28
|
+
expect(getBitmapPixel(img, 3, 3)).toBe(0x00000000);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('skips pixels outside the bitmap bounds', () => {
|
|
32
|
+
const img = createBitmap(2, 2, 0x000000ff);
|
|
33
|
+
fillBitmapRectangle(region(img, -1, -1, 4, 4), 0xffffffff);
|
|
34
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xffffffff);
|
|
35
|
+
expect(getBitmapPixel(img, 1, 1)).toBe(0xffffffff);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('floodFillBitmap', () => {
|
|
40
|
+
it('fills a connected region', () => {
|
|
41
|
+
const img = createBitmap(3, 3, 0xffffffff);
|
|
42
|
+
floodFillBitmap(img, 0, 0, 0x000000ff);
|
|
43
|
+
for (let py = 0; py < 3; py++) {
|
|
44
|
+
for (let px = 0; px < 3; px++) {
|
|
45
|
+
expect(getBitmapPixel(img, px, py)).toBe(0x000000ff);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not cross a barrier', () => {
|
|
51
|
+
const img = createBitmap(3, 3, 0xffffffff);
|
|
52
|
+
for (let py = 0; py < 3; py++) {
|
|
53
|
+
const i = (py * 3 + 1) * 4;
|
|
54
|
+
img.data[i] = 0;
|
|
55
|
+
img.data[i + 1] = 0;
|
|
56
|
+
img.data[i + 2] = 0;
|
|
57
|
+
img.data[i + 3] = 0xff;
|
|
58
|
+
}
|
|
59
|
+
floodFillBitmap(img, 0, 0, 0x0000ffff);
|
|
60
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0x0000ffff);
|
|
61
|
+
expect(getBitmapPixel(img, 2, 0)).toBe(0xffffffff);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('is a no-op when fill color matches target', () => {
|
|
65
|
+
const img = createBitmap(2, 2, 0x112233ff);
|
|
66
|
+
floodFillBitmap(img, 0, 0, 0x112233ff);
|
|
67
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0x112233ff);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reuses the scratch buffer across calls', () => {
|
|
71
|
+
const img = createBitmap(4, 4, 0xffffffff);
|
|
72
|
+
floodFillBitmap(img, 0, 0, 0x000000ff);
|
|
73
|
+
floodFillBitmap(img, 0, 0, 0xffffffff); // second call reuses buffer
|
|
74
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xffffffff);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('reuses the scratch buffer across calls', () => {
|
|
78
|
+
const img = createBitmap(4, 4, 0xffffffff);
|
|
79
|
+
floodFillBitmap(img, 0, 0, 0x000000ff);
|
|
80
|
+
floodFillBitmap(img, 0, 0, 0xffffffff); // second call reuses buffer
|
|
81
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xffffffff);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import {
|
|
3
|
+
compareBitmapFingerprints,
|
|
4
|
+
createBitmapFingerprint,
|
|
5
|
+
formatBitmapFingerprint,
|
|
6
|
+
parseBitmapFingerprint,
|
|
7
|
+
} from './bitmapFingerprint';
|
|
8
|
+
import { setBitmapPixel } from './bitmapPixel';
|
|
9
|
+
|
|
10
|
+
describe('compareBitmapFingerprints', () => {
|
|
11
|
+
it('reports 0 for identical fingerprints', () => {
|
|
12
|
+
const fp = createBitmapFingerprint(createBitmap(8, 8, 0x336699ff), 4);
|
|
13
|
+
expect(compareBitmapFingerprints(fp, fp)).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('reports the mean absolute per-channel difference', () => {
|
|
17
|
+
const a = createBitmapFingerprint(createBitmap(4, 4, 0x000000ff), 1);
|
|
18
|
+
const b = createBitmapFingerprint(createBitmap(4, 4, 0x0c0c0cff), 1);
|
|
19
|
+
// one cell, RGB all differ by 12 → mean abs diff 12
|
|
20
|
+
expect(compareBitmapFingerprints(a, b)).toBe(12);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('throws when gridSizes differ', () => {
|
|
24
|
+
const a = createBitmapFingerprint(createBitmap(4, 4), 2);
|
|
25
|
+
const b = createBitmapFingerprint(createBitmap(4, 4), 4);
|
|
26
|
+
expect(() => compareBitmapFingerprints(a, b)).toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('stays small for antialiasing-scale noise but large for a colour swap', () => {
|
|
30
|
+
const base = createBitmap(16, 16, 0x2040a0ff);
|
|
31
|
+
const noisy = createBitmap(16, 16, 0x2040a0ff);
|
|
32
|
+
setBitmapPixel(noisy, 0, 0, 0x2442a2ff); // a few pixels nudged
|
|
33
|
+
setBitmapPixel(noisy, 5, 5, 0x1e3e9eff);
|
|
34
|
+
const swapped = createBitmap(16, 16, 0xa04020ff); // whole image recoloured
|
|
35
|
+
const fp = createBitmapFingerprint(base, 16);
|
|
36
|
+
expect(compareBitmapFingerprints(fp, createBitmapFingerprint(noisy, 16))).toBeLessThan(2);
|
|
37
|
+
expect(compareBitmapFingerprints(fp, createBitmapFingerprint(swapped, 16))).toBeGreaterThan(50);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createBitmapFingerprint', () => {
|
|
42
|
+
it('averages a solid bitmap to that colour in every cell', () => {
|
|
43
|
+
const fp = createBitmapFingerprint(createBitmap(8, 8, 0x336699ff), 4);
|
|
44
|
+
expect(fp.gridSize).toBe(4);
|
|
45
|
+
expect(fp.cells.length).toBe(4 * 4 * 3);
|
|
46
|
+
expect([fp.cells[0], fp.cells[1], fp.cells[2]]).toEqual([0x33, 0x66, 0x99]);
|
|
47
|
+
expect([fp.cells[fp.cells.length - 3], fp.cells[fp.cells.length - 2], fp.cells[fp.cells.length - 1]]).toEqual([
|
|
48
|
+
0x33, 0x66, 0x99,
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('places a coloured quadrant in the matching grid cell', () => {
|
|
53
|
+
const bitmap = createBitmap(2, 2, 0x000000ff);
|
|
54
|
+
setBitmapPixel(bitmap, 0, 0, 0xff0000ff); // top-left
|
|
55
|
+
const fp = createBitmapFingerprint(bitmap, 2);
|
|
56
|
+
expect([fp.cells[0], fp.cells[1], fp.cells[2]]).toEqual([255, 0, 0]); // cell (0,0)
|
|
57
|
+
expect([fp.cells[3], fp.cells[4], fp.cells[5]]).toEqual([0, 0, 0]); // cell (1,0)
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('throws on a non-positive gridSize', () => {
|
|
61
|
+
expect(() => createBitmapFingerprint(createBitmap(4, 4), 0)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns an all-zero grid for an empty bitmap', () => {
|
|
65
|
+
const fp = createBitmapFingerprint(createBitmap(0, 0), 2);
|
|
66
|
+
expect(fp.cells.every((v) => v === 0)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('formatBitmapFingerprint', () => {
|
|
71
|
+
it('round-trips through parseBitmapFingerprint', () => {
|
|
72
|
+
const bitmap = createBitmap(8, 8, 0x000000ff);
|
|
73
|
+
setBitmapPixel(bitmap, 1, 1, 0x12ab34ff);
|
|
74
|
+
setBitmapPixel(bitmap, 6, 6, 0xfe01dcff);
|
|
75
|
+
const fp = createBitmapFingerprint(bitmap, 4);
|
|
76
|
+
const parsed = parseBitmapFingerprint(formatBitmapFingerprint(fp));
|
|
77
|
+
expect(parsed).not.toBeNull();
|
|
78
|
+
expect(parsed!.gridSize).toBe(4);
|
|
79
|
+
expect(Array.from(parsed!.cells)).toEqual(Array.from(fp.cells));
|
|
80
|
+
expect(compareBitmapFingerprints(fp, parsed!)).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('encodes gridSize and lowercase hex cells', () => {
|
|
84
|
+
const fp = createBitmapFingerprint(createBitmap(4, 4, 0x0a0b0cff), 1);
|
|
85
|
+
expect(formatBitmapFingerprint(fp)).toBe('1:0a0b0c');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('parseBitmapFingerprint', () => {
|
|
90
|
+
it('returns null for malformed text', () => {
|
|
91
|
+
expect(parseBitmapFingerprint('garbage')).toBeNull();
|
|
92
|
+
expect(parseBitmapFingerprint('')).toBeNull();
|
|
93
|
+
expect(parseBitmapFingerprint('2:abcd')).toBeNull(); // wrong cell count for gridSize 2
|
|
94
|
+
expect(parseBitmapFingerprint('1:0a0b0')).toBeNull(); // odd hex length
|
|
95
|
+
expect(parseBitmapFingerprint('1:0a0b0z')).toBeNull(); // non-hex digit
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('parses a valid single-cell fingerprint', () => {
|
|
99
|
+
const parsed = parseBitmapFingerprint('1:0a0b0c');
|
|
100
|
+
expect(parsed).not.toBeNull();
|
|
101
|
+
expect(Array.from(parsed!.cells)).toEqual([0x0a, 0x0b, 0x0c]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { flipBitmapHorizontal, flipBitmapVertical } from './bitmapFlip';
|
|
3
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
4
|
+
|
|
5
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
6
|
+
return { bitmap, x, y, width, height };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ramp(width: number, height: number) {
|
|
10
|
+
const bitmap = createBitmap(width, height);
|
|
11
|
+
for (let i = 0; i < width * height; i++) {
|
|
12
|
+
bitmap.data[i * 4] = i;
|
|
13
|
+
bitmap.data[i * 4 + 3] = 255;
|
|
14
|
+
}
|
|
15
|
+
return bitmap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('flipBitmapHorizontal', () => {
|
|
19
|
+
it('mirrors columns left-to-right into a separate dest', () => {
|
|
20
|
+
const source = ramp(3, 1);
|
|
21
|
+
const out = createBitmap(3, 1);
|
|
22
|
+
flipBitmapHorizontal(region(out), region(source));
|
|
23
|
+
expect(out.data[0]).toBe(2);
|
|
24
|
+
expect(out.data[4]).toBe(1);
|
|
25
|
+
expect(out.data[8]).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('mirrors in place when dest and source are the same region', () => {
|
|
29
|
+
const bitmap = ramp(4, 1);
|
|
30
|
+
flipBitmapHorizontal(region(bitmap), region(bitmap));
|
|
31
|
+
expect(bitmap.data[0]).toBe(3);
|
|
32
|
+
expect(bitmap.data[4]).toBe(2);
|
|
33
|
+
expect(bitmap.data[8]).toBe(1);
|
|
34
|
+
expect(bitmap.data[12]).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('flipBitmapVertical', () => {
|
|
39
|
+
it('mirrors rows top-to-bottom into a separate dest', () => {
|
|
40
|
+
const source = ramp(1, 3);
|
|
41
|
+
const out = createBitmap(1, 3);
|
|
42
|
+
flipBitmapVertical(region(out), region(source));
|
|
43
|
+
expect(getBitmapPixel(out, 0, 0)).toBe(getBitmapPixel(source, 0, 2));
|
|
44
|
+
expect(getBitmapPixel(out, 0, 2)).toBe(getBitmapPixel(source, 0, 0));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('mirrors in place when dest and source are the same region', () => {
|
|
48
|
+
const bitmap = ramp(1, 4);
|
|
49
|
+
flipBitmapVertical(region(bitmap), region(bitmap));
|
|
50
|
+
expect(bitmap.data[0]).toBe(3);
|
|
51
|
+
expect(bitmap.data[12]).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { convertBitmapPixelOrder, premultiplyBitmapPixels, unpremultiplyBitmapPixels } from './bitmapFormat';
|
|
2
|
+
|
|
3
|
+
describe('convertBitmapPixelOrder', () => {
|
|
4
|
+
it('is a no-op when from === to and out !== source', () => {
|
|
5
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
|
|
6
|
+
const out = new Uint8ClampedArray(4);
|
|
7
|
+
convertBitmapPixelOrder(out, source, 4, 'RGBA', 'RGBA');
|
|
8
|
+
expect(out[0]).toBe(0x11);
|
|
9
|
+
expect(out[1]).toBe(0x22);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('converts RGBA to BGRA', () => {
|
|
13
|
+
const source = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xff]);
|
|
14
|
+
const out = new Uint8ClampedArray(4);
|
|
15
|
+
convertBitmapPixelOrder(out, source, 4, 'RGBA', 'BGRA');
|
|
16
|
+
expect(out[0]).toBe(0xcc); // B
|
|
17
|
+
expect(out[1]).toBe(0xbb); // G
|
|
18
|
+
expect(out[2]).toBe(0xaa); // R
|
|
19
|
+
expect(out[3]).toBe(0xff); // A
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('converts RGBA to ARGB', () => {
|
|
23
|
+
const source = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xdd]);
|
|
24
|
+
const out = new Uint8ClampedArray(4);
|
|
25
|
+
convertBitmapPixelOrder(out, source, 4, 'RGBA', 'ARGB');
|
|
26
|
+
expect(out[0]).toBe(0xdd); // A
|
|
27
|
+
expect(out[1]).toBe(0xaa); // R
|
|
28
|
+
expect(out[2]).toBe(0xbb); // G
|
|
29
|
+
expect(out[3]).toBe(0xcc); // B
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('round-trips RGBA → BGRA → RGBA', () => {
|
|
33
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0x44]);
|
|
34
|
+
const tmp = new Uint8ClampedArray(4);
|
|
35
|
+
const out = new Uint8ClampedArray(4);
|
|
36
|
+
convertBitmapPixelOrder(tmp, source, 4, 'RGBA', 'BGRA');
|
|
37
|
+
convertBitmapPixelOrder(out, tmp, 4, 'BGRA', 'RGBA');
|
|
38
|
+
expect(out[0]).toBe(0x11);
|
|
39
|
+
expect(out[1]).toBe(0x22);
|
|
40
|
+
expect(out[2]).toBe(0x33);
|
|
41
|
+
expect(out[3]).toBe(0x44);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is safe in-place for the same format (no-op path)', () => {
|
|
45
|
+
const buf = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xff]);
|
|
46
|
+
convertBitmapPixelOrder(buf, buf, 4, 'RGBA', 'RGBA');
|
|
47
|
+
expect(buf[0]).toBe(0xaa);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('is safe in-place for different formats', () => {
|
|
51
|
+
const buf = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xff]);
|
|
52
|
+
convertBitmapPixelOrder(buf, buf, 4, 'RGBA', 'BGRA');
|
|
53
|
+
expect(buf[0]).toBe(0xcc);
|
|
54
|
+
expect(buf[2]).toBe(0xaa);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('premultiplyBitmapPixels', () => {
|
|
59
|
+
it('scales RGB by alpha/255', () => {
|
|
60
|
+
const source = new Uint8ClampedArray([0xff, 0x80, 0x00, 0x80]);
|
|
61
|
+
const out = new Uint8ClampedArray(4);
|
|
62
|
+
premultiplyBitmapPixels(out, source, 4);
|
|
63
|
+
expect(out[0]).toBeCloseTo(0x80, 0); // 255 * 128/255 ≈ 128
|
|
64
|
+
expect(out[1]).toBeCloseTo(0x40, 0); // 128 * 128/255 ≈ 64
|
|
65
|
+
expect(out[2]).toBe(0);
|
|
66
|
+
expect(out[3]).toBe(0x80); // alpha unchanged
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('fully transparent pixel becomes all zeros', () => {
|
|
70
|
+
const source = new Uint8ClampedArray([0xff, 0xff, 0xff, 0x00]);
|
|
71
|
+
const out = new Uint8ClampedArray(4);
|
|
72
|
+
premultiplyBitmapPixels(out, source, 4);
|
|
73
|
+
expect(out[0]).toBe(0);
|
|
74
|
+
expect(out[1]).toBe(0);
|
|
75
|
+
expect(out[2]).toBe(0);
|
|
76
|
+
expect(out[3]).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('fully opaque pixel is unchanged', () => {
|
|
80
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
|
|
81
|
+
const out = new Uint8ClampedArray(4);
|
|
82
|
+
premultiplyBitmapPixels(out, source, 4);
|
|
83
|
+
expect(out[0]).toBe(0x11);
|
|
84
|
+
expect(out[1]).toBe(0x22);
|
|
85
|
+
expect(out[2]).toBe(0x33);
|
|
86
|
+
expect(out[3]).toBe(0xff);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('is safe in-place', () => {
|
|
90
|
+
const buf = new Uint8ClampedArray([0xff, 0x00, 0x00, 0x80]);
|
|
91
|
+
premultiplyBitmapPixels(buf, buf, 4);
|
|
92
|
+
expect(buf[0]).toBeCloseTo(0x80, 0);
|
|
93
|
+
expect(buf[3]).toBe(0x80);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('unpremultiplyBitmapPixels', () => {
|
|
98
|
+
it('recovers original RGB after premultiply round-trip within ±1', () => {
|
|
99
|
+
// Integer premultiply introduces at most ±1 rounding error per channel.
|
|
100
|
+
const source = new Uint8ClampedArray([0xc0, 0x80, 0x40, 0x80]);
|
|
101
|
+
const premul = new Uint8ClampedArray(4);
|
|
102
|
+
const out = new Uint8ClampedArray(4);
|
|
103
|
+
premultiplyBitmapPixels(premul, source, 4);
|
|
104
|
+
unpremultiplyBitmapPixels(out, premul, 4);
|
|
105
|
+
expect(Math.abs(out[0] - 0xc0)).toBeLessThanOrEqual(1);
|
|
106
|
+
expect(Math.abs(out[1] - 0x80)).toBeLessThanOrEqual(1);
|
|
107
|
+
expect(out[3]).toBe(0x80);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('fully transparent pixel remains all zeros', () => {
|
|
111
|
+
const source = new Uint8ClampedArray([0x00, 0x00, 0x00, 0x00]);
|
|
112
|
+
const out = new Uint8ClampedArray(4);
|
|
113
|
+
unpremultiplyBitmapPixels(out, source, 4);
|
|
114
|
+
expect(out[0]).toBe(0);
|
|
115
|
+
expect(out[3]).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('is safe in-place', () => {
|
|
119
|
+
const buf = new Uint8ClampedArray([0x80, 0x00, 0x00, 0x80]);
|
|
120
|
+
unpremultiplyBitmapPixels(buf, buf, 4);
|
|
121
|
+
expect(buf[0]).toBeCloseTo(0xff, 0);
|
|
122
|
+
expect(buf[3]).toBe(0x80);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createImageResource } from '@flighthq/image/contract';
|
|
2
|
+
|
|
3
|
+
import { createBitmap } from './bitmap';
|
|
4
|
+
import { createBitmapFromCanvas, captureBitmapFromImageResource, createBitmapFromImageSource } from './bitmapFrom';
|
|
5
|
+
|
|
6
|
+
describe('captureBitmapFromImageResource', () => {
|
|
7
|
+
it('reads a host-backed resource into CPU pixels', () => {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.width = 3;
|
|
10
|
+
canvas.height = 2;
|
|
11
|
+
const bitmap = captureBitmapFromImageResource(createImageResource(canvas));
|
|
12
|
+
expect(bitmap.width).toBe(3);
|
|
13
|
+
expect(bitmap.height).toBe(2);
|
|
14
|
+
expect(bitmap.data).toHaveLength(3 * 2 * 4);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns Bitmap matching the resource dimensions', () => {
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
canvas.width = 4;
|
|
20
|
+
canvas.height = 4;
|
|
21
|
+
const resource = createImageResource(canvas);
|
|
22
|
+
const data = captureBitmapFromImageResource(resource);
|
|
23
|
+
expect(data.width).toBe(4);
|
|
24
|
+
expect(data.height).toBe(4);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('createBitmapFromCanvas', () => {
|
|
29
|
+
it('returns Bitmap matching the canvas size', () => {
|
|
30
|
+
const canvas = document.createElement('canvas');
|
|
31
|
+
canvas.width = 4;
|
|
32
|
+
canvas.height = 4;
|
|
33
|
+
const data = createBitmapFromCanvas(canvas);
|
|
34
|
+
expect(data.width).toBe(4);
|
|
35
|
+
expect(data.height).toBe(4);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns a Bitmap with data length matching canvas pixels', () => {
|
|
39
|
+
const canvas = document.createElement('canvas');
|
|
40
|
+
canvas.width = 8;
|
|
41
|
+
canvas.height = 8;
|
|
42
|
+
const data = createBitmapFromCanvas(canvas);
|
|
43
|
+
expect(data.data.length).toBe(8 * 8 * 4);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('createBitmapFromImageSource', () => {
|
|
48
|
+
it('captures a canvas image source at the given device size', () => {
|
|
49
|
+
const canvas = document.createElement('canvas');
|
|
50
|
+
canvas.width = 8;
|
|
51
|
+
canvas.height = 4;
|
|
52
|
+
const bitmap = createBitmapFromImageSource(canvas, 8, 4);
|
|
53
|
+
expect(bitmap.width).toBe(8);
|
|
54
|
+
expect(bitmap.height).toBe(4);
|
|
55
|
+
expect(bitmap.data.length).toBe(8 * 4 * 4);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { buildBitmapGradientRamp, gradientBevelBitmap, gradientGlowBitmap } from './bitmapGradient';
|
|
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('buildBitmapGradientRamp', () => {
|
|
9
|
+
it('places endpoints exactly and interpolates the middle', () => {
|
|
10
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
11
|
+
buildBitmapGradientRamp(ramp, [0xff0000, 0x0000ff], [0, 1], [0, 255]);
|
|
12
|
+
expect(Array.from(ramp.subarray(0, 4))).toEqual([255, 0, 0, 0]);
|
|
13
|
+
expect(Array.from(ramp.subarray(255 * 4, 256 * 4))).toEqual([0, 0, 255, 255]);
|
|
14
|
+
expect(Array.from(ramp.subarray(128 * 4, 129 * 4))).toEqual([127, 0, 128, 128]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('zero-fills when there are no stops', () => {
|
|
18
|
+
const ramp = new Uint8ClampedArray(256 * 4).fill(99);
|
|
19
|
+
buildBitmapGradientRamp(ramp, [], [], []);
|
|
20
|
+
expect(ramp.every((v) => v === 0)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('gradientBevelBitmap', () => {
|
|
25
|
+
it('maps the signed edge gradient through the ramp', () => {
|
|
26
|
+
// 5x1 edge: transparent x2 | opaque x3. Shadow ramp at index 0 (black),
|
|
27
|
+
// transparent middle at 128, highlight white at 255.
|
|
28
|
+
const source = createBitmap(5, 1);
|
|
29
|
+
for (let x = 2; x < 5; x++) source.data[x * 4 + 3] = 255;
|
|
30
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
31
|
+
buildBitmapGradientRamp(ramp, [0x000000, 0x808080, 0xffffff], [1, 0, 1], [0, 128, 255]);
|
|
32
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
33
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
34
|
+
gradientBevelBitmap(out, scratch, region(source), ramp, {
|
|
35
|
+
angle: Math.PI,
|
|
36
|
+
distance: 1,
|
|
37
|
+
radiusX: 1,
|
|
38
|
+
radiusY: 0,
|
|
39
|
+
type: 'both',
|
|
40
|
+
});
|
|
41
|
+
// Light-facing edge (x2) is on the highlight half of the ramp → bright.
|
|
42
|
+
expect(out[2 * 4 + 0]).toBeGreaterThan(150);
|
|
43
|
+
expect(out[2 * 4 + 3]).toBeGreaterThan(0);
|
|
44
|
+
// Far edge (x4) maps to ramp index 0 → opaque black shadow.
|
|
45
|
+
expect(out[4 * 4 + 0]).toBe(0);
|
|
46
|
+
expect(out[4 * 4 + 3]).toBe(255);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('gradientGlowBitmap', () => {
|
|
51
|
+
it('indexes the ramp by the blurred alpha', () => {
|
|
52
|
+
// ramp: index 0 transparent green, 255 opaque green.
|
|
53
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
54
|
+
buildBitmapGradientRamp(ramp, [0x00ff00, 0x00ff00], [0, 1], [0, 255]);
|
|
55
|
+
const source = createBitmap(1, 1, 0xffffffff);
|
|
56
|
+
const out = new Uint8ClampedArray(4);
|
|
57
|
+
const scratch = new Uint8ClampedArray(4);
|
|
58
|
+
gradientGlowBitmap(out, scratch, region(source), ramp, { radiusX: 0, radiusY: 0 });
|
|
59
|
+
// Full alpha (255) → ramp[255] = opaque green.
|
|
60
|
+
expect(out[0]).toBe(0);
|
|
61
|
+
expect(out[1]).toBe(0xff);
|
|
62
|
+
expect(out[2]).toBe(0);
|
|
63
|
+
expect(out[3]).toBe(255);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('scales the ramp alpha by intensity', () => {
|
|
67
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
68
|
+
buildBitmapGradientRamp(ramp, [0x00ff00, 0x00ff00], [1, 1], [0, 255]);
|
|
69
|
+
const source = createBitmap(1, 1, 0xffffffff);
|
|
70
|
+
const out = new Uint8ClampedArray(4);
|
|
71
|
+
const scratch = new Uint8ClampedArray(4);
|
|
72
|
+
gradientGlowBitmap(out, scratch, region(source), ramp, { radiusX: 0, radiusY: 0, intensity: 0.5 });
|
|
73
|
+
expect(out[3]).toBe(128);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('source.bitmap.data can be used as out for a full-bitmap region', () => {
|
|
77
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
78
|
+
buildBitmapGradientRamp(ramp, [0x00ff00, 0x00ff00], [0, 1], [0, 255]);
|
|
79
|
+
const bitmap = createBitmap(1, 1, 0xffffffff);
|
|
80
|
+
const scratch = new Uint8ClampedArray(4);
|
|
81
|
+
gradientGlowBitmap(bitmap.data, scratch, region(bitmap), ramp, { radiusX: 0, radiusY: 0 });
|
|
82
|
+
expect(bitmap.data[1]).toBe(0xff);
|
|
83
|
+
expect(bitmap.data[3]).toBe(255);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { buildBitmapGradientRamp } from './bitmapGradient';
|
|
3
|
+
import { fillBitmapLinearGradient, fillBitmapRadialGradient } from './bitmapGradientFill';
|
|
4
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
5
|
+
import { createBitmapRegion } from './bitmapRegion';
|
|
6
|
+
|
|
7
|
+
function buildTwoStopRamp(colorA: number, colorB: number): Uint8ClampedArray {
|
|
8
|
+
const ramp = new Uint8ClampedArray(1024);
|
|
9
|
+
const aR = (colorA >>> 24) & 0xff;
|
|
10
|
+
const aG = (colorA >> 16) & 0xff;
|
|
11
|
+
const aB = (colorA >> 8) & 0xff;
|
|
12
|
+
const aA = colorA & 0xff;
|
|
13
|
+
const bR = (colorB >>> 24) & 0xff;
|
|
14
|
+
const bG = (colorB >> 16) & 0xff;
|
|
15
|
+
const bB = (colorB >> 8) & 0xff;
|
|
16
|
+
const bA = colorB & 0xff;
|
|
17
|
+
buildBitmapGradientRamp(
|
|
18
|
+
ramp,
|
|
19
|
+
[(aR << 16) | (aG << 8) | aB, (bR << 16) | (bG << 8) | bB],
|
|
20
|
+
[aA / 255, bA / 255],
|
|
21
|
+
[0, 255],
|
|
22
|
+
);
|
|
23
|
+
return ramp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('fillBitmapLinearGradient', () => {
|
|
27
|
+
it('fills leftmost pixel with the first stop color', () => {
|
|
28
|
+
const surf = createBitmap(4, 1);
|
|
29
|
+
const region = createBitmapRegion(surf);
|
|
30
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
31
|
+
fillBitmapLinearGradient(region, ramp, 0, 0, 3, 0);
|
|
32
|
+
const pixel = getBitmapPixel(surf, 0, 0);
|
|
33
|
+
expect((pixel >>> 24) & 0xff).toBeCloseTo(0xff, -1);
|
|
34
|
+
expect((pixel >> 8) & 0xff).toBeCloseTo(0x00, -1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('fills rightmost pixel with the last stop color', () => {
|
|
38
|
+
const surf = createBitmap(4, 1);
|
|
39
|
+
const region = createBitmapRegion(surf);
|
|
40
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
41
|
+
fillBitmapLinearGradient(region, ramp, 0, 0, 3, 0);
|
|
42
|
+
const pixel = getBitmapPixel(surf, 3, 0);
|
|
43
|
+
expect((pixel >>> 24) & 0xff).toBeCloseTo(0x00, -1);
|
|
44
|
+
expect((pixel >> 8) & 0xff).toBeCloseTo(0xff, -1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('middle pixel is a blend of both stops', () => {
|
|
48
|
+
const surf = createBitmap(3, 1);
|
|
49
|
+
const region = createBitmapRegion(surf);
|
|
50
|
+
const ramp = buildTwoStopRamp(0x000000ff, 0xffffffff);
|
|
51
|
+
fillBitmapLinearGradient(region, ramp, 0, 0, 2, 0);
|
|
52
|
+
const mid = getBitmapPixel(surf, 1, 0);
|
|
53
|
+
const r = (mid >>> 24) & 0xff;
|
|
54
|
+
expect(r).toBeGreaterThan(0);
|
|
55
|
+
expect(r).toBeLessThan(255);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('pad spread clamps colors outside axis', () => {
|
|
59
|
+
const surf = createBitmap(5, 1);
|
|
60
|
+
const region = createBitmapRegion(surf);
|
|
61
|
+
// Gradient from x=1 to x=3; pixels at x=0 and x=4 should get clamped stops.
|
|
62
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
63
|
+
fillBitmapLinearGradient(region, ramp, 1, 0, 3, 0, 'pad');
|
|
64
|
+
const left = getBitmapPixel(surf, 0, 0);
|
|
65
|
+
const right = getBitmapPixel(surf, 4, 0);
|
|
66
|
+
expect((left >>> 24) & 0xff).toBe((getBitmapPixel(surf, 1, 0) >>> 24) & 0xff);
|
|
67
|
+
expect((right >> 8) & 0xff).toBe((getBitmapPixel(surf, 3, 0) >> 8) & 0xff);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('skips pixels outside bitmap bounds', () => {
|
|
71
|
+
const surf = createBitmap(2, 2);
|
|
72
|
+
// Region that extends outside bitmap — should not throw.
|
|
73
|
+
const region = { bitmap: surf, x: -1, y: 0, width: 4, height: 1 };
|
|
74
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
75
|
+
expect(() => fillBitmapLinearGradient(region, ramp, 0, 0, 3, 0)).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('fillBitmapRadialGradient', () => {
|
|
80
|
+
it('center pixel gets the first stop color', () => {
|
|
81
|
+
const surf = createBitmap(5, 5);
|
|
82
|
+
const region = createBitmapRegion(surf);
|
|
83
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
84
|
+
fillBitmapRadialGradient(region, ramp, 2, 2, 2);
|
|
85
|
+
// Center is at distance 0, so t=0, gets first stop.
|
|
86
|
+
const pixel = getBitmapPixel(surf, 2, 2);
|
|
87
|
+
expect((pixel >>> 24) & 0xff).toBeCloseTo(0xff, -1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('pad spread clamps beyond radius to last stop', () => {
|
|
91
|
+
const surf = createBitmap(9, 1);
|
|
92
|
+
const region = createBitmapRegion(surf);
|
|
93
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
94
|
+
// Center at x=4, radius=2; pixels at x=0 and x=8 are far outside.
|
|
95
|
+
fillBitmapRadialGradient(region, ramp, 4, 0, 2, 4, 0, 'pad');
|
|
96
|
+
const far = getBitmapPixel(surf, 0, 0);
|
|
97
|
+
// Should be clamped to last stop (blue).
|
|
98
|
+
expect((far >> 8) & 0xff).toBeCloseTo(0xff, -1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('skips pixels outside bitmap bounds', () => {
|
|
102
|
+
const surf = createBitmap(2, 2);
|
|
103
|
+
const region = { bitmap: surf, x: -1, y: 0, width: 4, height: 1 };
|
|
104
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
105
|
+
expect(() => fillBitmapRadialGradient(region, ramp, 1, 0, 2)).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { equalizeBitmapHistogram, getBitmapHistogram } from './bitmapHistogram';
|
|
3
|
+
import { setBitmapPixel } from './bitmapPixel';
|
|
4
|
+
|
|
5
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
6
|
+
return { bitmap, x, y, width, height };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('equalizeBitmapHistogram', () => {
|
|
10
|
+
it('spreads a narrow tonal range to fill 0..255', () => {
|
|
11
|
+
// All red values are either 0 or 128 — a compressed range.
|
|
12
|
+
const source = createBitmap(2, 1);
|
|
13
|
+
setBitmapPixel(source, 0, 0, 0x000000ff);
|
|
14
|
+
setBitmapPixel(source, 1, 0, 0x808000ff);
|
|
15
|
+
const dest = createBitmap(2, 1);
|
|
16
|
+
equalizeBitmapHistogram(region(dest), region(source));
|
|
17
|
+
// After equalization the darker pixel maps to 0 and brighter to 255
|
|
18
|
+
expect(dest.data[0]).toBe(0);
|
|
19
|
+
expect(dest.data[4]).toBe(255);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('is safe in-place', () => {
|
|
23
|
+
const bitmap = createBitmap(2, 1);
|
|
24
|
+
setBitmapPixel(bitmap, 0, 0, 0x000000ff);
|
|
25
|
+
setBitmapPixel(bitmap, 1, 0, 0xff0000ff);
|
|
26
|
+
expect(() => equalizeBitmapHistogram(region(bitmap), region(bitmap))).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('preserves alpha unchanged', () => {
|
|
30
|
+
const source = createBitmap(1, 1, 0x40404080);
|
|
31
|
+
const dest = createBitmap(1, 1);
|
|
32
|
+
equalizeBitmapHistogram(region(dest), region(source));
|
|
33
|
+
expect(dest.data[3]).toBe(0x80);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getBitmapHistogram', () => {
|
|
38
|
+
it('counts pixel values per channel', () => {
|
|
39
|
+
const bitmap = createBitmap(2, 1);
|
|
40
|
+
setBitmapPixel(bitmap, 0, 0, 0x0a000000);
|
|
41
|
+
setBitmapPixel(bitmap, 1, 0, 0x0a0000ff);
|
|
42
|
+
const histogram = getBitmapHistogram(region(bitmap));
|
|
43
|
+
expect(histogram.red[0x0a]).toBe(2);
|
|
44
|
+
expect(histogram.alpha[0]).toBe(1);
|
|
45
|
+
expect(histogram.alpha[255]).toBe(1);
|
|
46
|
+
expect(histogram.green[0]).toBe(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns all-zero bins for an empty region', () => {
|
|
50
|
+
const bitmap = createBitmap(2, 2, 0xffffffff);
|
|
51
|
+
const histogram = getBitmapHistogram(region(bitmap, 0, 0, 0, 0));
|
|
52
|
+
expect(histogram.red.reduce((a, b) => a + b, 0)).toBe(0);
|
|
53
|
+
expect(histogram.red.length).toBe(256);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('counts only a sub-region', () => {
|
|
57
|
+
const bitmap = createBitmap(2, 1);
|
|
58
|
+
setBitmapPixel(bitmap, 0, 0, 0x10000000);
|
|
59
|
+
setBitmapPixel(bitmap, 1, 0, 0x20000000);
|
|
60
|
+
const histogram = getBitmapHistogram(region(bitmap, 1, 0, 1, 1));
|
|
61
|
+
expect(histogram.red[0x20]).toBe(1);
|
|
62
|
+
expect(histogram.red[0x10]).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
});
|