@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,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
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
3
|
+
import { rotateBitmap, rotateBitmap180, rotateBitmapClockwise, rotateBitmapCounterClockwise } from './bitmapRotate';
|
|
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
|
+
// 2x1 bitmap: pixel (0,0) = A, pixel (1,0) = B.
|
|
10
|
+
function ab() {
|
|
11
|
+
const bitmap = createBitmap(2, 1);
|
|
12
|
+
bitmap.data.set([0xa0, 0, 0, 255], 0);
|
|
13
|
+
bitmap.data.set([0xb0, 0, 0, 255], 4);
|
|
14
|
+
return bitmap;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('rotateBitmap', () => {
|
|
18
|
+
it('with angle 0 copies source into dest', () => {
|
|
19
|
+
const source = createBitmap(3, 3, 0xff0000ff);
|
|
20
|
+
const out = createBitmap(3, 3);
|
|
21
|
+
rotateBitmap(region(out), region(source), 0);
|
|
22
|
+
expect(out.data[0]).toBe(0xff);
|
|
23
|
+
expect(out.data[3]).toBe(0xff);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('with angle π reverses source similar to rotateBitmap180', () => {
|
|
27
|
+
const source = ab();
|
|
28
|
+
const out = createBitmap(2, 1);
|
|
29
|
+
rotateBitmap(region(out), region(source), Math.PI);
|
|
30
|
+
// Bilinear introduces slight blending, so just check direction
|
|
31
|
+
expect(out.data[0]).toBeGreaterThan(out.data[4]); // B-side (brighter) on left
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('clamp edge mode (default) repeats edge pixels for out-of-bounds', () => {
|
|
35
|
+
const source = createBitmap(1, 1, 0xff0000ff);
|
|
36
|
+
const out = createBitmap(5, 5);
|
|
37
|
+
rotateBitmap(region(out), region(source), 0);
|
|
38
|
+
// With clamp, all pixels map to the single source pixel
|
|
39
|
+
expect(out.data[(0 * 5 + 0) * 4 + 3]).toBe(0xff);
|
|
40
|
+
expect(out.data[(2 * 5 + 2) * 4 + 3]).toBe(0xff);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('transparent edge mode writes transparent black for out-of-bounds', () => {
|
|
44
|
+
const source = createBitmap(1, 1, 0xff0000ff);
|
|
45
|
+
const out = createBitmap(5, 5);
|
|
46
|
+
rotateBitmap(region(out), region(source), 0, undefined, undefined, 'transparent');
|
|
47
|
+
// Only center pixel maps back to source; corners are out of bounds
|
|
48
|
+
expect(out.data[(0 * 5 + 0) * 4 + 3]).toBe(0);
|
|
49
|
+
expect(out.data[(2 * 5 + 2) * 4 + 3]).toBe(0xff);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('wrap edge mode tiles the source', () => {
|
|
53
|
+
const source = createBitmap(2, 2, 0xff0000ff);
|
|
54
|
+
const out = createBitmap(4, 4);
|
|
55
|
+
rotateBitmap(region(out), region(source), 0, undefined, undefined, 'wrap');
|
|
56
|
+
// All pixels should be opaque red (wraps the 2x2 source)
|
|
57
|
+
expect(out.data[3]).toBe(0xff);
|
|
58
|
+
expect(out.data[(3 * 4 + 3) * 4 + 3]).toBe(0xff);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('nearest sample mode uses nearest-neighbor interpolation', () => {
|
|
62
|
+
const source = createBitmap(2, 1);
|
|
63
|
+
source.data.set([0, 0, 0, 255], 0);
|
|
64
|
+
source.data.set([200, 200, 200, 255], 4);
|
|
65
|
+
const out = createBitmap(2, 1);
|
|
66
|
+
rotateBitmap(region(out), region(source), 0, undefined, undefined, 'clamp', 'nearest');
|
|
67
|
+
// Nearest should produce exact pixel values, no blending
|
|
68
|
+
expect(out.data[0]).toBe(0);
|
|
69
|
+
expect(out.data[4]).toBe(200);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('rotateBitmap180', () => {
|
|
74
|
+
it('reverses pixel order with matching dimensions', () => {
|
|
75
|
+
const source = ab();
|
|
76
|
+
const out = createBitmap(2, 1);
|
|
77
|
+
rotateBitmap180(region(out), region(source));
|
|
78
|
+
expect(out.data[0]).toBe(0xb0);
|
|
79
|
+
expect(out.data[4]).toBe(0xa0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rotates in place when dest and source are the same region', () => {
|
|
83
|
+
const bitmap = ab();
|
|
84
|
+
rotateBitmap180(region(bitmap), region(bitmap));
|
|
85
|
+
expect(bitmap.data[0]).toBe(0xb0);
|
|
86
|
+
expect(bitmap.data[4]).toBe(0xa0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('rotateBitmapClockwise', () => {
|
|
91
|
+
it('rotates a row into a column, left element on top', () => {
|
|
92
|
+
const source = ab();
|
|
93
|
+
const out = createBitmap(1, 2);
|
|
94
|
+
rotateBitmapClockwise(region(out), region(source));
|
|
95
|
+
expect(getBitmapPixel(out, 0, 0)).toBe(getBitmapPixel(source, 0, 0));
|
|
96
|
+
expect(getBitmapPixel(out, 0, 1)).toBe(getBitmapPixel(source, 1, 0));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('rotateBitmapCounterClockwise', () => {
|
|
101
|
+
it('rotates a row into a column, right element on top', () => {
|
|
102
|
+
const source = ab();
|
|
103
|
+
const out = createBitmap(1, 2);
|
|
104
|
+
rotateBitmapCounterClockwise(region(out), region(source));
|
|
105
|
+
expect(getBitmapPixel(out, 0, 0)).toBe(getBitmapPixel(source, 1, 0));
|
|
106
|
+
expect(getBitmapPixel(out, 0, 1)).toBe(getBitmapPixel(source, 0, 0));
|
|
107
|
+
});
|
|
108
|
+
});
|