@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,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,86 @@
|
|
|
1
|
+
import { createImageResource } from '@flighthq/image/contract';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createBitmap } from './bitmap';
|
|
5
|
+
import { createBitmapFromCanvas, captureBitmapFromImageResource, createBitmapFromImageSource } from './bitmapFrom';
|
|
6
|
+
|
|
7
|
+
describe('captureBitmapFromImageResource', () => {
|
|
8
|
+
it('reads a host-backed resource into CPU pixels', () => {
|
|
9
|
+
const canvas = document.createElement('canvas');
|
|
10
|
+
canvas.width = 3;
|
|
11
|
+
canvas.height = 2;
|
|
12
|
+
const bitmap = captureBitmapFromImageResource(createImageResource(canvas));
|
|
13
|
+
expect(bitmap.width).toBe(3);
|
|
14
|
+
expect(bitmap.height).toBe(2);
|
|
15
|
+
expect(bitmap.data).toHaveLength(3 * 2 * 4);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns Bitmap matching the resource dimensions', () => {
|
|
19
|
+
const canvas = document.createElement('canvas');
|
|
20
|
+
canvas.width = 4;
|
|
21
|
+
canvas.height = 4;
|
|
22
|
+
const resource = createImageResource(canvas);
|
|
23
|
+
const data = captureBitmapFromImageResource(resource);
|
|
24
|
+
expect(data.width).toBe(4);
|
|
25
|
+
expect(data.height).toBe(4);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('createBitmapFromCanvas', () => {
|
|
30
|
+
it('returns Bitmap matching the canvas size', () => {
|
|
31
|
+
const canvas = document.createElement('canvas');
|
|
32
|
+
canvas.width = 4;
|
|
33
|
+
canvas.height = 4;
|
|
34
|
+
const data = createBitmapFromCanvas(canvas);
|
|
35
|
+
expect(data.width).toBe(4);
|
|
36
|
+
expect(data.height).toBe(4);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns a Bitmap with data length matching canvas pixels', () => {
|
|
40
|
+
const canvas = document.createElement('canvas');
|
|
41
|
+
canvas.width = 8;
|
|
42
|
+
canvas.height = 8;
|
|
43
|
+
const data = createBitmapFromCanvas(canvas);
|
|
44
|
+
expect(data.data.length).toBe(8 * 8 * 4);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('createBitmapFromImageSource', () => {
|
|
49
|
+
it('captures a canvas image source at the given device size', () => {
|
|
50
|
+
const canvas = document.createElement('canvas');
|
|
51
|
+
canvas.width = 8;
|
|
52
|
+
canvas.height = 4;
|
|
53
|
+
const bitmap = createBitmapFromImageSource(canvas, 8, 4);
|
|
54
|
+
expect(bitmap).not.toBeNull();
|
|
55
|
+
expect(bitmap!.width).toBe(8);
|
|
56
|
+
expect(bitmap!.height).toBe(4);
|
|
57
|
+
expect(bitmap!.data.length).toBe(8 * 4 * 4);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns null rather than letting a tainted source throw', () => {
|
|
61
|
+
// A cross-origin draw taints the scratch canvas and the platform refuses its pixels with a
|
|
62
|
+
// SecurityError from getImageData. jsdom does not model tainting, so the throw is staged on
|
|
63
|
+
// getImageData directly — which is the exact call and the exact exception the sentinel exists to
|
|
64
|
+
// stop escaping to a caller who only asked for a bitmap.
|
|
65
|
+
const canvas = document.createElement('canvas');
|
|
66
|
+
canvas.width = 8;
|
|
67
|
+
canvas.height = 4;
|
|
68
|
+
const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'getImageData').mockImplementation(() => {
|
|
69
|
+
throw new DOMException('Tainted canvases may not be exported.', 'SecurityError');
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
expect(() => createBitmapFromImageSource(canvas, 8, 4)).not.toThrow();
|
|
73
|
+
expect(createBitmapFromImageSource(canvas, 8, 4)).toBeNull();
|
|
74
|
+
} finally {
|
|
75
|
+
spy.mockRestore();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns null for an empty capture rather than allocating a zero-pixel bitmap', () => {
|
|
80
|
+
const canvas = document.createElement('canvas');
|
|
81
|
+
canvas.width = 8;
|
|
82
|
+
canvas.height = 4;
|
|
83
|
+
expect(createBitmapFromImageSource(canvas, 0, 4)).toBeNull();
|
|
84
|
+
expect(createBitmapFromImageSource(canvas, 8, -1)).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ImageChannel } from './bitmapImageChannel';
|
|
2
|
+
|
|
3
|
+
describe('ImageChannel', () => {
|
|
4
|
+
it('has Red = 0', () => {
|
|
5
|
+
expect(ImageChannel.Red).toBe(0);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('has Green = 1', () => {
|
|
9
|
+
expect(ImageChannel.Green).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('has Blue = 2', () => {
|
|
13
|
+
expect(ImageChannel.Blue).toBe(2);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('has Alpha = 3', () => {
|
|
17
|
+
expect(ImageChannel.Alpha).toBe(3);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { medianBitmap } from './bitmapMedian';
|
|
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('medianBitmap', () => {
|
|
9
|
+
it('removes an isolated salt pixel without blurring', () => {
|
|
10
|
+
// 3x3 of black with a single white (R=255) center; the median is black.
|
|
11
|
+
const source = createBitmap(3, 3);
|
|
12
|
+
for (let i = 0; i < 9; i++) source.data[i * 4 + 3] = 255;
|
|
13
|
+
source.data[4 * 4] = 255;
|
|
14
|
+
const out = new Uint8ClampedArray(3 * 3 * 4);
|
|
15
|
+
medianBitmap(out, region(source), 1);
|
|
16
|
+
expect(out[4 * 4]).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('preserves a hard edge (median is not an average)', () => {
|
|
20
|
+
// Left half 0, right half 255 across a 4x1 row; the boundary stays crisp.
|
|
21
|
+
const source = createBitmap(4, 1);
|
|
22
|
+
source.data[0] = 0;
|
|
23
|
+
source.data[4] = 0;
|
|
24
|
+
source.data[8] = 255;
|
|
25
|
+
source.data[12] = 255;
|
|
26
|
+
const out = new Uint8ClampedArray(4 * 4);
|
|
27
|
+
medianBitmap(out, region(source), 1);
|
|
28
|
+
expect(out[4]).toBe(0);
|
|
29
|
+
expect(out[8]).toBe(255);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('radius 0 copies the source', () => {
|
|
33
|
+
const source = createBitmap(1, 1, 0x123456ff);
|
|
34
|
+
const out = new Uint8ClampedArray(4);
|
|
35
|
+
medianBitmap(out, region(source), 0);
|
|
36
|
+
expect(out[0]).toBe(0x12);
|
|
37
|
+
expect(out[3]).toBe(0xff);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { dilateBitmap, erodeBitmap } from './bitmapMorphological';
|
|
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('dilateBitmap', () => {
|
|
9
|
+
it('expands a single bright pixel into its neighborhood', () => {
|
|
10
|
+
const source = createBitmap(3, 1);
|
|
11
|
+
source.data[1 * 4 + 3] = 255; // center pixel fully opaque
|
|
12
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
13
|
+
dilateBitmap(out, region(source), 1);
|
|
14
|
+
expect(out[0 * 4 + 3]).toBe(255);
|
|
15
|
+
expect(out[1 * 4 + 3]).toBe(255);
|
|
16
|
+
expect(out[2 * 4 + 3]).toBe(255);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('with radius 0 is an identity copy', () => {
|
|
20
|
+
const source = createBitmap(3, 1);
|
|
21
|
+
source.data.set([10, 20, 30, 100], 0);
|
|
22
|
+
source.data.set([50, 60, 70, 200], 4);
|
|
23
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
24
|
+
dilateBitmap(out, region(source), 0);
|
|
25
|
+
expect(out[0]).toBe(10);
|
|
26
|
+
expect(out[4]).toBe(50);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is dual to erodeBitmap (dilate after erode has no-smaller effect)', () => {
|
|
30
|
+
const source = createBitmap(5, 1);
|
|
31
|
+
for (let i = 1; i < 4; i++) source.data[i * 4 + 3] = 255; // pixels 1-3 opaque
|
|
32
|
+
const eroded = new Uint8ClampedArray(5 * 4);
|
|
33
|
+
const dilated = new Uint8ClampedArray(5 * 4);
|
|
34
|
+
erodeBitmap(eroded, region(source), 1);
|
|
35
|
+
dilateBitmap(dilated, region(source), 1);
|
|
36
|
+
// Dilation always >= source, erosion always <= source for alpha
|
|
37
|
+
expect(dilated[1 * 4 + 3]).toBeGreaterThanOrEqual(source.data[1 * 4 + 3]);
|
|
38
|
+
expect(eroded[1 * 4 + 3]).toBeLessThanOrEqual(source.data[1 * 4 + 3]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('erodeBitmap', () => {
|
|
43
|
+
it('shrinks an isolated bright pixel to dark when radius covers neighbors', () => {
|
|
44
|
+
const source = createBitmap(3, 1);
|
|
45
|
+
source.data[1 * 4 + 3] = 255; // only center opaque
|
|
46
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
47
|
+
erodeBitmap(out, region(source), 1);
|
|
48
|
+
// Center eroded to 0 because transparent neighbors dominate the min
|
|
49
|
+
expect(out[1 * 4 + 3]).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('with radius 0 is an identity copy', () => {
|
|
53
|
+
const source = createBitmap(3, 1);
|
|
54
|
+
source.data.set([10, 20, 30, 100], 4);
|
|
55
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
56
|
+
erodeBitmap(out, region(source), 0);
|
|
57
|
+
expect(out[4]).toBe(10);
|
|
58
|
+
expect(out[7]).toBe(100);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('preserves a fully opaque field', () => {
|
|
62
|
+
const source = createBitmap(5, 1, 0xffffffff);
|
|
63
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
64
|
+
erodeBitmap(out, region(source), 2);
|
|
65
|
+
for (let i = 0; i < 5; i++) {
|
|
66
|
+
expect(out[i * 4 + 3]).toBe(255);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|