@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,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
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import {
|
|
3
|
+
fillBitmapNoise,
|
|
4
|
+
fillBitmapPerlinNoise,
|
|
5
|
+
fillBitmapTurbulence,
|
|
6
|
+
BITMAP_NOISE_CHANNEL_A,
|
|
7
|
+
BITMAP_NOISE_CHANNEL_R,
|
|
8
|
+
} from './bitmapNoise';
|
|
9
|
+
|
|
10
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
11
|
+
return { bitmap, x, y, width, height };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('fillBitmapNoise', () => {
|
|
15
|
+
it('is deterministic for the same seed', () => {
|
|
16
|
+
const a = createBitmap(4, 4);
|
|
17
|
+
const b = createBitmap(4, 4);
|
|
18
|
+
fillBitmapNoise(region(a), 12345);
|
|
19
|
+
fillBitmapNoise(region(b), 12345);
|
|
20
|
+
expect(Array.from(a.data)).toEqual(Array.from(b.data));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('produces different output for different seeds', () => {
|
|
24
|
+
const a = createBitmap(8, 8);
|
|
25
|
+
const b = createBitmap(8, 8);
|
|
26
|
+
fillBitmapNoise(region(a), 1);
|
|
27
|
+
fillBitmapNoise(region(b), 2);
|
|
28
|
+
expect(Array.from(a.data)).not.toEqual(Array.from(b.data));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('grayscale gives every pixel equal R, G, B', () => {
|
|
32
|
+
const bitmap = createBitmap(4, 4);
|
|
33
|
+
fillBitmapNoise(region(bitmap), 99, 0, 255, true);
|
|
34
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
35
|
+
expect(bitmap.data[i]).toBe(bitmap.data[i + 1]);
|
|
36
|
+
expect(bitmap.data[i + 1]).toBe(bitmap.data[i + 2]);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('respects the low/high range and sets alpha opaque', () => {
|
|
41
|
+
const bitmap = createBitmap(8, 8);
|
|
42
|
+
fillBitmapNoise(region(bitmap), 7, 100, 110);
|
|
43
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
44
|
+
expect(bitmap.data[i]).toBeGreaterThanOrEqual(100);
|
|
45
|
+
expect(bitmap.data[i]).toBeLessThanOrEqual(110);
|
|
46
|
+
expect(bitmap.data[i + 3]).toBe(255);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('fills only the given sub-region', () => {
|
|
51
|
+
const bitmap = createBitmap(2, 1, 0x000000ff);
|
|
52
|
+
fillBitmapNoise(region(bitmap, 1, 0, 1, 1), 5, 200, 255);
|
|
53
|
+
expect(bitmap.data[0]).toBe(0); // left pixel untouched
|
|
54
|
+
expect(bitmap.data[4]).toBeGreaterThanOrEqual(200);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('fillBitmapPerlinNoise', () => {
|
|
59
|
+
it('is deterministic for the same seed', () => {
|
|
60
|
+
const a = createBitmap(8, 8);
|
|
61
|
+
const b = createBitmap(8, 8);
|
|
62
|
+
fillBitmapPerlinNoise(region(a), 16, 16, 3, 42);
|
|
63
|
+
fillBitmapPerlinNoise(region(b), 16, 16, 3, 42);
|
|
64
|
+
expect(Array.from(a.data)).toEqual(Array.from(b.data));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('keeps values in range and alpha opaque', () => {
|
|
68
|
+
const bitmap = createBitmap(8, 8);
|
|
69
|
+
fillBitmapPerlinNoise(region(bitmap), 8, 8, 2, 5);
|
|
70
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
71
|
+
expect(bitmap.data[i]).toBeGreaterThanOrEqual(0);
|
|
72
|
+
expect(bitmap.data[i]).toBeLessThanOrEqual(255);
|
|
73
|
+
expect(bitmap.data[i + 3]).toBe(255);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('grayscale gives every pixel equal R, G, B', () => {
|
|
78
|
+
const bitmap = createBitmap(8, 8);
|
|
79
|
+
fillBitmapPerlinNoise(region(bitmap), 8, 8, 2, 5, true);
|
|
80
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
81
|
+
expect(bitmap.data[i]).toBe(bitmap.data[i + 1]);
|
|
82
|
+
expect(bitmap.data[i + 1]).toBe(bitmap.data[i + 2]);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('produces smooth low-frequency fields (neighbors stay close)', () => {
|
|
87
|
+
const bitmap = createBitmap(16, 1);
|
|
88
|
+
fillBitmapPerlinNoise(region(bitmap), 32, 32, 1, 3, true);
|
|
89
|
+
let maxStep = 0;
|
|
90
|
+
for (let x = 1; x < 16; x++) {
|
|
91
|
+
maxStep = Math.max(maxStep, Math.abs(bitmap.data[x * 4] - bitmap.data[(x - 1) * 4]));
|
|
92
|
+
}
|
|
93
|
+
expect(maxStep).toBeLessThan(64);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('fills selected channels only and leaves alpha opaque by default', () => {
|
|
97
|
+
const bitmap = createBitmap(8, 8, 0x000000ff);
|
|
98
|
+
fillBitmapPerlinNoise(region(bitmap), 8, 8, 2, 5, false, false, BITMAP_NOISE_CHANNEL_R);
|
|
99
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
100
|
+
// G and B were not selected, so they stay at their initial 0.
|
|
101
|
+
expect(bitmap.data[i + 1]).toBe(0);
|
|
102
|
+
expect(bitmap.data[i + 2]).toBe(0);
|
|
103
|
+
// Alpha not selected -> forced opaque.
|
|
104
|
+
expect(bitmap.data[i + 3]).toBe(255);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('fillBitmapTurbulence', () => {
|
|
110
|
+
it('is deterministic for the same seed', () => {
|
|
111
|
+
const a = createBitmap(8, 8);
|
|
112
|
+
const b = createBitmap(8, 8);
|
|
113
|
+
fillBitmapTurbulence(region(a), 16, 16, 3, 42);
|
|
114
|
+
fillBitmapTurbulence(region(b), 16, 16, 3, 42);
|
|
115
|
+
expect(Array.from(a.data)).toEqual(Array.from(b.data));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('keeps values in range and alpha opaque', () => {
|
|
119
|
+
const bitmap = createBitmap(8, 8);
|
|
120
|
+
fillBitmapTurbulence(region(bitmap), 8, 8, 2, 5);
|
|
121
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
122
|
+
expect(bitmap.data[i]).toBeGreaterThanOrEqual(0);
|
|
123
|
+
expect(bitmap.data[i]).toBeLessThanOrEqual(255);
|
|
124
|
+
expect(bitmap.data[i + 3]).toBe(255);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('grayscale gives every pixel equal R, G, B', () => {
|
|
129
|
+
const bitmap = createBitmap(8, 8);
|
|
130
|
+
fillBitmapTurbulence(region(bitmap), 8, 8, 2, 5, true);
|
|
131
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
132
|
+
expect(bitmap.data[i]).toBe(bitmap.data[i + 1]);
|
|
133
|
+
expect(bitmap.data[i + 1]).toBe(bitmap.data[i + 2]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('writes noise into alpha when the A channel is selected', () => {
|
|
138
|
+
const bitmap = createBitmap(8, 8, 0x000000ff);
|
|
139
|
+
fillBitmapTurbulence(region(bitmap), 4, 4, 3, 9, false, false, BITMAP_NOISE_CHANNEL_A);
|
|
140
|
+
let sawNonOpaque = false;
|
|
141
|
+
for (let i = 0; i < bitmap.data.length; i += 4) {
|
|
142
|
+
if (bitmap.data[i + 3] !== 255) sawNonOpaque = true;
|
|
143
|
+
}
|
|
144
|
+
expect(sawNonOpaque).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('differs from the smooth fractal sum for the same parameters', () => {
|
|
148
|
+
const turbulent = createBitmap(8, 8);
|
|
149
|
+
const fractal = createBitmap(8, 8);
|
|
150
|
+
fillBitmapTurbulence(region(turbulent), 8, 8, 3, 7, true);
|
|
151
|
+
fillBitmapPerlinNoise(region(fractal), 8, 8, 3, 7, true);
|
|
152
|
+
expect(Array.from(turbulent.data)).not.toEqual(Array.from(fractal.data));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('fills only the given sub-region', () => {
|
|
156
|
+
const bitmap = createBitmap(2, 1, 0x010203ff);
|
|
157
|
+
fillBitmapTurbulence(region(bitmap, 1, 0, 1, 1), 8, 8, 2, 3);
|
|
158
|
+
// Left pixel untouched.
|
|
159
|
+
expect(bitmap.data[0]).toBe(1);
|
|
160
|
+
expect(bitmap.data[1]).toBe(2);
|
|
161
|
+
expect(bitmap.data[2]).toBe(3);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { applyBitmapPaletteMap } from './bitmapPaletteMap';
|
|
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
|
+
function invertTable(): number[] {
|
|
9
|
+
const table = new Array<number>(256);
|
|
10
|
+
for (let i = 0; i < 256; i++) table[i] = 255 - i;
|
|
11
|
+
return table;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('applyBitmapPaletteMap', () => {
|
|
15
|
+
it('remaps a channel through its lookup table', () => {
|
|
16
|
+
const source = createBitmap(1, 1, 0x10203040);
|
|
17
|
+
const out = createBitmap(1, 1);
|
|
18
|
+
applyBitmapPaletteMap(region(out), region(source), invertTable(), null, null, null);
|
|
19
|
+
expect(out.data[0]).toBe(255 - 0x10);
|
|
20
|
+
expect(out.data[1]).toBe(0x20);
|
|
21
|
+
expect(out.data[2]).toBe(0x30);
|
|
22
|
+
expect(out.data[3]).toBe(0x40);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('null maps leave every channel unchanged', () => {
|
|
26
|
+
const source = createBitmap(1, 1, 0x11223344);
|
|
27
|
+
const out = createBitmap(1, 1);
|
|
28
|
+
applyBitmapPaletteMap(region(out), region(source), null, null, null, null);
|
|
29
|
+
expect(out.data[0]).toBe(0x11);
|
|
30
|
+
expect(out.data[3]).toBe(0x44);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('is safe in place', () => {
|
|
34
|
+
const img = createBitmap(1, 1, 0x00ff00ff);
|
|
35
|
+
applyBitmapPaletteMap(region(img), region(img), invertTable(), invertTable(), invertTable(), null);
|
|
36
|
+
expect(img.data[0]).toBe(255);
|
|
37
|
+
expect(img.data[1]).toBe(0);
|
|
38
|
+
expect(img.data[2]).toBe(255);
|
|
39
|
+
expect(img.data[3]).toBe(0xff);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('maps only the given sub-region', () => {
|
|
43
|
+
const source = createBitmap(2, 1, 0x80808080);
|
|
44
|
+
const out = createBitmap(2, 1);
|
|
45
|
+
applyBitmapPaletteMap(region(out, 1, 0, 1, 1), region(source, 1, 0, 1, 1), invertTable(), null, null, null);
|
|
46
|
+
expect(out.data[4]).toBe(255 - 0x80);
|
|
47
|
+
expect(out.data[0]).toBe(0); // untouched left pixel
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { ImageChannel } from './bitmapImageChannel';
|
|
3
|
+
import {
|
|
4
|
+
getBitmapPixel,
|
|
5
|
+
getBitmapPixelChannel,
|
|
6
|
+
getBitmapPixelLuminance,
|
|
7
|
+
getBitmapPixelRgb,
|
|
8
|
+
setBitmapPixel,
|
|
9
|
+
setBitmapPixelRgb,
|
|
10
|
+
} from './bitmapPixel';
|
|
11
|
+
|
|
12
|
+
describe('getBitmapPixel', () => {
|
|
13
|
+
it('reads back a full 0xRRGGBBAA value written by setBitmapPixel', () => {
|
|
14
|
+
const img = createBitmap(2, 2);
|
|
15
|
+
setBitmapPixel(img, 0, 0, 0xaabbcc80);
|
|
16
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xaabbcc80);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('reads the first pixel at (0, 0)', () => {
|
|
20
|
+
const img = createBitmap(8, 8);
|
|
21
|
+
setBitmapPixel(img, 0, 0, 0xdeadbeef);
|
|
22
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xdeadbeef);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reads the last pixel at (width-1, height-1)', () => {
|
|
26
|
+
const img = createBitmap(8, 8);
|
|
27
|
+
setBitmapPixel(img, 7, 7, 0xcafebabe);
|
|
28
|
+
expect(getBitmapPixel(img, 7, 7)).toBe(0xcafebabe);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('round-trips an RGBA value', () => {
|
|
32
|
+
const img = createBitmap(4, 4);
|
|
33
|
+
setBitmapPixel(img, 1, 2, 0x11223380);
|
|
34
|
+
expect(getBitmapPixel(img, 1, 2)).toBe(0x11223380);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('stores alpha in the fourth byte', () => {
|
|
38
|
+
const img = createBitmap(2, 2);
|
|
39
|
+
setBitmapPixel(img, 0, 0, 0x112233de);
|
|
40
|
+
expect(img.data[3]).toBe(0xde);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('getBitmapPixelChannel', () => {
|
|
45
|
+
it('reads a single channel by index', () => {
|
|
46
|
+
const img = createBitmap(2, 2);
|
|
47
|
+
setBitmapPixel(img, 0, 0, 0xaabbccdd);
|
|
48
|
+
expect(getBitmapPixelChannel(img, 0, 0, ImageChannel.Red)).toBe(0xaa);
|
|
49
|
+
expect(getBitmapPixelChannel(img, 0, 0, ImageChannel.Green)).toBe(0xbb);
|
|
50
|
+
expect(getBitmapPixelChannel(img, 0, 0, ImageChannel.Blue)).toBe(0xcc);
|
|
51
|
+
expect(getBitmapPixelChannel(img, 0, 0, ImageChannel.Alpha)).toBe(0xdd);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getBitmapPixelLuminance', () => {
|
|
56
|
+
it('returns 0 for black', () => {
|
|
57
|
+
const img = createBitmap(1, 1, 0x000000ff);
|
|
58
|
+
expect(getBitmapPixelLuminance(img, 0, 0)).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns 255 for white', () => {
|
|
62
|
+
const img = createBitmap(1, 1, 0xffffffff);
|
|
63
|
+
expect(getBitmapPixelLuminance(img, 0, 0)).toBe(255);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uses W3C luma weighting (green is heaviest)', () => {
|
|
67
|
+
const red = createBitmap(1, 1, 0xff0000ff);
|
|
68
|
+
const green = createBitmap(1, 1, 0x00ff00ff);
|
|
69
|
+
const blue = createBitmap(1, 1, 0x0000ffff);
|
|
70
|
+
expect(getBitmapPixelLuminance(green, 0, 0)).toBeGreaterThan(getBitmapPixelLuminance(red, 0, 0));
|
|
71
|
+
expect(getBitmapPixelLuminance(red, 0, 0)).toBeGreaterThan(getBitmapPixelLuminance(blue, 0, 0));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getBitmapPixelRgb', () => {
|
|
76
|
+
it('reads back a 0xRRGGBB value without alpha', () => {
|
|
77
|
+
const img = createBitmap(2, 2);
|
|
78
|
+
setBitmapPixelRgb(img, 0, 0, 0xaabbcc);
|
|
79
|
+
expect(getBitmapPixelRgb(img, 0, 0)).toBe(0xaabbcc);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('round-trips an RGB value', () => {
|
|
83
|
+
const img = createBitmap(4, 4);
|
|
84
|
+
setBitmapPixelRgb(img, 1, 2, 0x112233);
|
|
85
|
+
expect(getBitmapPixelRgb(img, 1, 2)).toBe(0x112233);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('setBitmapPixel', () => {
|
|
90
|
+
it('writes all four RGBA channels', () => {
|
|
91
|
+
const img = createBitmap(2, 2);
|
|
92
|
+
setBitmapPixel(img, 1, 0, 0xadbeefde);
|
|
93
|
+
expect(getBitmapPixel(img, 1, 0)).toBe(0xadbeefde);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('writes to the last pixel at (width-1, height-1)', () => {
|
|
97
|
+
const img = createBitmap(8, 8);
|
|
98
|
+
setBitmapPixel(img, 7, 7, 0xfedcba98);
|
|
99
|
+
expect(getBitmapPixel(img, 7, 7)).toBe(0xfedcba98);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('bumps the bitmap version (self-invalidation)', () => {
|
|
103
|
+
const img = createBitmap(2, 2);
|
|
104
|
+
const before = img.version;
|
|
105
|
+
setBitmapPixel(img, 1, 0, 0xadbeefde);
|
|
106
|
+
expect(img.version).toBe(before + 1);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('setBitmapPixelRgb', () => {
|
|
111
|
+
it('writes RGB channels without touching alpha', () => {
|
|
112
|
+
const img = createBitmap(2, 2, 0x000000ff);
|
|
113
|
+
setBitmapPixelRgb(img, 0, 0, 0x112233);
|
|
114
|
+
expect(img.data[3]).toBe(0xff);
|
|
115
|
+
expect(getBitmapPixelRgb(img, 0, 0)).toBe(0x112233);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { pixelateBitmap } from './bitmapPixelate';
|
|
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('pixelateBitmap', () => {
|
|
9
|
+
it('averages a block to a single uniform color', () => {
|
|
10
|
+
// 2x1 row: R=0 and R=100; a block of 2 averages both to 50.
|
|
11
|
+
const source = createBitmap(2, 1);
|
|
12
|
+
source.data[0] = 0;
|
|
13
|
+
source.data[4] = 100;
|
|
14
|
+
source.data[3] = 255;
|
|
15
|
+
source.data[7] = 255;
|
|
16
|
+
const out = new Uint8ClampedArray(2 * 4);
|
|
17
|
+
pixelateBitmap(out, region(source), 2);
|
|
18
|
+
expect(out[0]).toBe(50);
|
|
19
|
+
expect(out[4]).toBe(50);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps each block independent', () => {
|
|
23
|
+
const source = createBitmap(2, 1);
|
|
24
|
+
source.data[0] = 10;
|
|
25
|
+
source.data[4] = 200;
|
|
26
|
+
const out = new Uint8ClampedArray(2 * 4);
|
|
27
|
+
pixelateBitmap(out, region(source), 1);
|
|
28
|
+
expect(out[0]).toBe(10);
|
|
29
|
+
expect(out[4]).toBe(200);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('can use source.bitmap.data as out for a full-bitmap region', () => {
|
|
33
|
+
const bitmap = createBitmap(2, 1);
|
|
34
|
+
bitmap.data[0] = 0;
|
|
35
|
+
bitmap.data[4] = 80;
|
|
36
|
+
pixelateBitmap(bitmap.data, region(bitmap), 2);
|
|
37
|
+
expect(bitmap.data[0]).toBe(40);
|
|
38
|
+
expect(bitmap.data[4]).toBe(40);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { fillBitmapRectangle } from './bitmapFill';
|
|
3
|
+
import { getBitmapColorBoundsRectangle } from './bitmapQuery';
|
|
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('getBitmapColorBoundsRectangle', () => {
|
|
10
|
+
it('returns null when no pixels match', () => {
|
|
11
|
+
const img = createBitmap(4, 4);
|
|
12
|
+
expect(getBitmapColorBoundsRectangle(region(img), 0xffffffff, 0xffffffff)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('finds the bounding rect of matching pixels', () => {
|
|
16
|
+
const img = createBitmap(8, 8);
|
|
17
|
+
fillBitmapRectangle(region(img, 2, 3, 3, 2), 0xff0000ff);
|
|
18
|
+
const rect = getBitmapColorBoundsRectangle(region(img), 0xffffffff, 0xff0000ff);
|
|
19
|
+
expect(rect).toEqual({ x: 2, y: 3, width: 3, height: 2 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('finds non-matching pixels when findColor is false', () => {
|
|
23
|
+
const img = createBitmap(4, 4, 0xffffffff);
|
|
24
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0x000000ff);
|
|
25
|
+
const rect = getBitmapColorBoundsRectangle(region(img), 0xffffffff, 0xffffffff, false);
|
|
26
|
+
expect(rect).toEqual({ x: 1, y: 1, width: 2, height: 2 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('respects the mask', () => {
|
|
30
|
+
const img = createBitmap(2, 2);
|
|
31
|
+
fillBitmapRectangle(region(img), 0x112233ff);
|
|
32
|
+
const rect = getBitmapColorBoundsRectangle(region(img), 0xff0000ff, 0x110000ff);
|
|
33
|
+
expect(rect).not.toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reports bitmap-absolute coordinates when scanning a sub-region', () => {
|
|
37
|
+
const img = createBitmap(8, 8);
|
|
38
|
+
fillBitmapRectangle(region(img, 5, 5, 2, 2), 0xff0000ff);
|
|
39
|
+
const rect = getBitmapColorBoundsRectangle(region(img, 4, 4, 4, 4), 0xffffffff, 0xff0000ff);
|
|
40
|
+
expect(rect).toEqual({ x: 5, y: 5, width: 2, height: 2 });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { createBitmapRegion, setBitmapRegion } from './bitmapRegion';
|
|
3
|
+
|
|
4
|
+
describe('createBitmapRegion', () => {
|
|
5
|
+
it('covers the whole bitmap when no bounds are given', () => {
|
|
6
|
+
const bitmap = createBitmap(7, 5);
|
|
7
|
+
const r = createBitmapRegion(bitmap);
|
|
8
|
+
expect(r).toEqual({ bitmap, x: 0, y: 0, width: 7, height: 5 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('uses the supplied bounds', () => {
|
|
12
|
+
const bitmap = createBitmap(8, 8);
|
|
13
|
+
const r = createBitmapRegion(bitmap, 1, 2, 3, 4);
|
|
14
|
+
expect(r).toEqual({ bitmap, x: 1, y: 2, width: 3, height: 4 });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('setBitmapRegion', () => {
|
|
19
|
+
it('mutates the existing region without allocating a new object', () => {
|
|
20
|
+
const bitmap = createBitmap(8, 8);
|
|
21
|
+
const r = createBitmapRegion(bitmap);
|
|
22
|
+
const returned = setBitmapRegion(r, bitmap, 2, 3, 4, 5);
|
|
23
|
+
expect(returned).toBe(r);
|
|
24
|
+
expect(r).toEqual({ bitmap, x: 2, y: 3, width: 4, height: 5 });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('covers the whole bitmap when no bounds are given', () => {
|
|
28
|
+
const a = createBitmap(2, 2);
|
|
29
|
+
const b = createBitmap(6, 9);
|
|
30
|
+
const r = createBitmapRegion(a);
|
|
31
|
+
setBitmapRegion(r, b);
|
|
32
|
+
expect(r).toEqual({ bitmap: b, x: 0, y: 0, width: 6, height: 9 });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
3
|
+
import { resizeBitmap } from './bitmapResize';
|
|
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('resizeBitmap', () => {
|
|
10
|
+
it('nearest upscales by pixel duplication', () => {
|
|
11
|
+
const source = createBitmap(2, 1);
|
|
12
|
+
source.data.set([10, 20, 30, 255], 0);
|
|
13
|
+
source.data.set([40, 50, 60, 255], 4);
|
|
14
|
+
const out = createBitmap(4, 1);
|
|
15
|
+
resizeBitmap(region(out), region(source), 'nearest');
|
|
16
|
+
expect(getBitmapPixel(out, 0, 0)).toBe(getBitmapPixel(source, 0, 0));
|
|
17
|
+
expect(getBitmapPixel(out, 1, 0)).toBe(getBitmapPixel(source, 0, 0));
|
|
18
|
+
expect(getBitmapPixel(out, 2, 0)).toBe(getBitmapPixel(source, 1, 0));
|
|
19
|
+
expect(getBitmapPixel(out, 3, 0)).toBe(getBitmapPixel(source, 1, 0));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('bilinear interpolates between source pixels', () => {
|
|
23
|
+
const source = createBitmap(2, 1);
|
|
24
|
+
source.data.set([0, 0, 0, 255], 0);
|
|
25
|
+
source.data.set([100, 100, 100, 255], 4);
|
|
26
|
+
const out = createBitmap(4, 1);
|
|
27
|
+
resizeBitmap(region(out), region(source), 'bilinear');
|
|
28
|
+
expect(out.data[0]).toBe(0);
|
|
29
|
+
expect(out.data[4]).toBeGreaterThanOrEqual(out.data[0]);
|
|
30
|
+
expect(out.data[8]).toBeGreaterThan(out.data[4]);
|
|
31
|
+
expect(out.data[12]).toBe(100);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('bicubic produces a smoother interpolation than bilinear', () => {
|
|
35
|
+
const source = createBitmap(2, 1);
|
|
36
|
+
source.data.set([0, 0, 0, 255], 0);
|
|
37
|
+
source.data.set([200, 200, 200, 255], 4);
|
|
38
|
+
const out = createBitmap(4, 1);
|
|
39
|
+
resizeBitmap(region(out), region(source), 'bicubic');
|
|
40
|
+
// Bicubic should produce a monotonically increasing sequence
|
|
41
|
+
expect(out.data[4]).toBeGreaterThanOrEqual(out.data[0]);
|
|
42
|
+
expect(out.data[8]).toBeGreaterThanOrEqual(out.data[4]);
|
|
43
|
+
expect(out.data[12]).toBeGreaterThanOrEqual(out.data[8]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('premultiplied option prevents dark-halo bleed at transparent edges', () => {
|
|
47
|
+
// Red pixel adjacent to fully transparent. Straight-alpha blend mixes the
|
|
48
|
+
// hidden black of the transparent pixel into the output. Premultiplied
|
|
49
|
+
// blending keeps the red pure on the opaque side.
|
|
50
|
+
const source = createBitmap(2, 1);
|
|
51
|
+
source.data.set([255, 0, 0, 255], 0); // opaque red
|
|
52
|
+
source.data.set([0, 0, 0, 0], 4); // transparent
|
|
53
|
+
const straight = createBitmap(4, 1);
|
|
54
|
+
resizeBitmap(region(straight), region(source), 'bilinear');
|
|
55
|
+
const premul = createBitmap(4, 1);
|
|
56
|
+
resizeBitmap(region(premul), region(source), { mode: 'bilinear', premultiplied: true });
|
|
57
|
+
// At the boundary (pixel 1), premultiplied keeps red higher than straight
|
|
58
|
+
expect(premul.data[4]).toBeGreaterThanOrEqual(straight.data[4]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('downscales by averaging toward the source values', () => {
|
|
62
|
+
const source = createBitmap(4, 1, 0x40608000);
|
|
63
|
+
const out = createBitmap(2, 1);
|
|
64
|
+
resizeBitmap(region(out), region(source), 'bilinear');
|
|
65
|
+
expect(out.data[0]).toBe(0x40);
|
|
66
|
+
expect(out.data[1]).toBe(0x60);
|
|
67
|
+
expect(out.data[2]).toBe(0x80);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('resamples a source sub-region into a dest sub-region', () => {
|
|
71
|
+
const source = createBitmap(4, 1);
|
|
72
|
+
source.data.set([10, 0, 0, 255], 2 * 4); // source pixel (2,0)
|
|
73
|
+
const out = createBitmap(4, 1);
|
|
74
|
+
resizeBitmap(region(out, 0, 0, 2, 1), region(source, 2, 0, 1, 1), 'nearest');
|
|
75
|
+
expect(out.data[0]).toBe(10);
|
|
76
|
+
expect(out.data[4]).toBe(10);
|
|
77
|
+
expect(out.data[8]).toBe(0); // outside the dest sub-region
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("edgeMode 'wrap' wraps boundary interpolation", () => {
|
|
81
|
+
// 2px source [black, white]. With wrap, the right boundary wraps to the left.
|
|
82
|
+
const source = createBitmap(2, 1);
|
|
83
|
+
source.data.set([0, 0, 0, 255], 0);
|
|
84
|
+
source.data.set([200, 200, 200, 255], 4);
|
|
85
|
+
const outClamp = createBitmap(4, 1);
|
|
86
|
+
resizeBitmap(region(outClamp), region(source), { mode: 'bilinear', edgeMode: 'clamp' });
|
|
87
|
+
const outWrap = createBitmap(4, 1);
|
|
88
|
+
resizeBitmap(region(outWrap), region(source), { mode: 'bilinear', edgeMode: 'wrap' });
|
|
89
|
+
// The rightmost pixel should differ: clamp repeats edge, wrap uses the other side.
|
|
90
|
+
expect(outClamp.data[12]).not.toBe(outWrap.data[12]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("edgeMode 'transparent' fades boundary pixels to transparent", () => {
|
|
94
|
+
const source = createBitmap(2, 1, 0xff0000ff);
|
|
95
|
+
const out = createBitmap(4, 1);
|
|
96
|
+
resizeBitmap(region(out), region(source), { mode: 'bilinear', edgeMode: 'transparent' });
|
|
97
|
+
// The rightmost pixel samples partially out-of-bounds → alpha drops
|
|
98
|
+
expect(out.data[12 + 3]).toBeLessThan(255);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('is a no-op when a dimension is zero', () => {
|
|
102
|
+
const source = createBitmap(2, 2, 0xffffffff);
|
|
103
|
+
const out = createBitmap(0, 0);
|
|
104
|
+
expect(() => resizeBitmap(region(out), region(source))).not.toThrow();
|
|
105
|
+
});
|
|
106
|
+
});
|