@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,181 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { displaceBitmap } from './bitmapDisplacement';
|
|
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
|
+
// A 3x1 strip: red | green | blue.
|
|
9
|
+
function rgbStrip() {
|
|
10
|
+
const s = createBitmap(3, 1);
|
|
11
|
+
s.data.set([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]);
|
|
12
|
+
return s;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('displaceBitmap', () => {
|
|
16
|
+
it('is a no-op copy when scaleX and scaleY are 0', () => {
|
|
17
|
+
const source = rgbStrip();
|
|
18
|
+
const map = createBitmap(3, 1);
|
|
19
|
+
map.data.fill(128);
|
|
20
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
21
|
+
displaceBitmap(out, region(source), { map: region(map), scaleX: 0, scaleY: 0, mode: 'clamp' });
|
|
22
|
+
// With zero scale, every output pixel samples from its exact position.
|
|
23
|
+
expect(out[0]).toBe(255); // px0 = red
|
|
24
|
+
expect(out[4 + 1]).toBe(255); // px1 = green
|
|
25
|
+
expect(out[8 + 2]).toBe(255); // px2 = blue
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('shifts the sample position by the scaled map displacement', () => {
|
|
29
|
+
// map value 255 → dx = (255/255 - 0.5) * 2 = +1.0 exactly; scaleX=2.
|
|
30
|
+
const source = rgbStrip();
|
|
31
|
+
const map = createBitmap(3, 1);
|
|
32
|
+
map.data[0] = 255;
|
|
33
|
+
map.data[4] = 255;
|
|
34
|
+
map.data[8] = 255;
|
|
35
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
36
|
+
displaceBitmap(out, region(source), {
|
|
37
|
+
map: region(map),
|
|
38
|
+
componentX: 0,
|
|
39
|
+
scaleX: 2,
|
|
40
|
+
scaleY: 0,
|
|
41
|
+
mode: 'clamp',
|
|
42
|
+
});
|
|
43
|
+
// px0←src1 (green), px1←src2 (blue), px2←src3 oob clamped to src2 (blue)
|
|
44
|
+
expect(out[1]).toBe(255); // green channel of px0
|
|
45
|
+
expect(out[4 + 2]).toBe(255); // blue channel of px1
|
|
46
|
+
expect(out[8 + 2]).toBe(255); // blue channel of px2 (clamped)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("'color' mode fills out-of-range samples with fillColor", () => {
|
|
50
|
+
// Map value 255 with scaleX=4 → dx=+2; px2 samples at x=4 (oob)
|
|
51
|
+
const source = rgbStrip();
|
|
52
|
+
const map = createBitmap(3, 1);
|
|
53
|
+
map.data.fill(255);
|
|
54
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
55
|
+
displaceBitmap(out, region(source), {
|
|
56
|
+
map: region(map),
|
|
57
|
+
scaleX: 4,
|
|
58
|
+
scaleY: 0,
|
|
59
|
+
mode: 'color',
|
|
60
|
+
fillColor: 0xff00ffff,
|
|
61
|
+
});
|
|
62
|
+
// px2 displaces to x=4 (out of range) → magenta fill
|
|
63
|
+
expect(out[2 * 4 + 0]).toBe(0xff);
|
|
64
|
+
expect(out[2 * 4 + 1]).toBe(0);
|
|
65
|
+
expect(out[2 * 4 + 2]).toBe(0xff);
|
|
66
|
+
expect(out[2 * 4 + 3]).toBe(255);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("'ignore' mode keeps the undisplaced source pixel for out-of-range samples", () => {
|
|
70
|
+
// Map value 255 with scaleX=4 → px2 samples at x=4 (oob) → keep blue
|
|
71
|
+
const source = rgbStrip();
|
|
72
|
+
const map = createBitmap(3, 1);
|
|
73
|
+
map.data.fill(255);
|
|
74
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
75
|
+
displaceBitmap(out, region(source), {
|
|
76
|
+
map: region(map),
|
|
77
|
+
scaleX: 4,
|
|
78
|
+
scaleY: 0,
|
|
79
|
+
mode: 'ignore',
|
|
80
|
+
});
|
|
81
|
+
// px2 displaces out of range → keeps src2 (blue) unchanged
|
|
82
|
+
expect(out[2 * 4 + 2]).toBe(255);
|
|
83
|
+
expect(out[2 * 4 + 3]).toBe(255);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("edgeMode 'clamp' clamps out-of-range samples to the edge", () => {
|
|
87
|
+
const source = rgbStrip();
|
|
88
|
+
const map = createBitmap(3, 1);
|
|
89
|
+
map.data[0] = 255;
|
|
90
|
+
map.data[4] = 255;
|
|
91
|
+
map.data[8] = 255;
|
|
92
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
93
|
+
displaceBitmap(out, region(source), {
|
|
94
|
+
map: region(map),
|
|
95
|
+
componentX: 0,
|
|
96
|
+
scaleX: 2,
|
|
97
|
+
scaleY: 0,
|
|
98
|
+
edgeMode: 'clamp',
|
|
99
|
+
});
|
|
100
|
+
// px2 displaces to x=3 (oob) → clamped to x=2 (blue)
|
|
101
|
+
expect(out[8 + 2]).toBe(255);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("edgeMode 'transparent' fills out-of-range samples with transparent black", () => {
|
|
105
|
+
const source = rgbStrip();
|
|
106
|
+
const map = createBitmap(3, 1);
|
|
107
|
+
map.data.fill(255);
|
|
108
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
109
|
+
displaceBitmap(out, region(source), {
|
|
110
|
+
map: region(map),
|
|
111
|
+
scaleX: 4,
|
|
112
|
+
scaleY: 0,
|
|
113
|
+
edgeMode: 'transparent',
|
|
114
|
+
});
|
|
115
|
+
// px2 displaces to x=4 (oob) → transparent black
|
|
116
|
+
expect(out[2 * 4 + 0]).toBe(0);
|
|
117
|
+
expect(out[2 * 4 + 3]).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("edgeMode 'transparent' treats an out-of-range bilinear tap as transparent", () => {
|
|
121
|
+
const source = rgbStrip();
|
|
122
|
+
const map = createBitmap(3, 1);
|
|
123
|
+
map.data.fill(255);
|
|
124
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
125
|
+
displaceBitmap(out, region(source), {
|
|
126
|
+
map: region(map),
|
|
127
|
+
scaleX: 1,
|
|
128
|
+
scaleY: 0,
|
|
129
|
+
edgeMode: 'transparent',
|
|
130
|
+
});
|
|
131
|
+
// px2 samples halfway between blue and the transparent pixel beyond the edge.
|
|
132
|
+
expect([...out.slice(2 * 4, 3 * 4)]).toEqual([0, 0, 128, 128]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("edgeMode 'wrap' wraps each out-of-range bilinear tap", () => {
|
|
136
|
+
const source = rgbStrip();
|
|
137
|
+
const map = createBitmap(3, 1);
|
|
138
|
+
map.data.fill(255);
|
|
139
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
140
|
+
displaceBitmap(out, region(source), {
|
|
141
|
+
map: region(map),
|
|
142
|
+
scaleX: 1,
|
|
143
|
+
scaleY: 0,
|
|
144
|
+
edgeMode: 'wrap',
|
|
145
|
+
});
|
|
146
|
+
// px2 samples halfway between blue at x=2 and wrapped red at x=0.
|
|
147
|
+
expect([...out.slice(2 * 4, 3 * 4)]).toEqual([128, 0, 128, 255]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("edgeMode 'mirror' reflects out-of-range bilinear taps", () => {
|
|
151
|
+
const source = rgbStrip();
|
|
152
|
+
const map = createBitmap(3, 1);
|
|
153
|
+
map.data.fill(255);
|
|
154
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
155
|
+
displaceBitmap(out, region(source), {
|
|
156
|
+
map: region(map),
|
|
157
|
+
scaleX: 3,
|
|
158
|
+
scaleY: 0,
|
|
159
|
+
edgeMode: 'mirror',
|
|
160
|
+
});
|
|
161
|
+
// px2 samples halfway between reflected blue at x=2 and green at x=1.
|
|
162
|
+
expect([...out.slice(2 * 4, 3 * 4)]).toEqual([0, 128, 128, 255]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('samples through the source region offset', () => {
|
|
166
|
+
// 4px [red, green, blue, white]; zero displacement over region (1,0,2,1) copies
|
|
167
|
+
// source pixels 1 and 2 (green, blue), proving the offset is applied.
|
|
168
|
+
const source = createBitmap(4, 1);
|
|
169
|
+
source.data.set([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255]);
|
|
170
|
+
const map = createBitmap(2, 1);
|
|
171
|
+
map.data.fill(128); // neutral — minimal displacement at scale=0
|
|
172
|
+
const out = new Uint8ClampedArray(2 * 4);
|
|
173
|
+
displaceBitmap(out, region(source, 1, 0, 2, 1), {
|
|
174
|
+
map: region(map),
|
|
175
|
+
scaleX: 0,
|
|
176
|
+
scaleY: 0,
|
|
177
|
+
});
|
|
178
|
+
expect(out[1]).toBe(255); // green from source[1]
|
|
179
|
+
expect(out[4 + 2]).toBe(255); // blue from source[2]
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { dissolveBitmapPixels } from './bitmapDissolve';
|
|
3
|
+
import { getBitmapPixel, setBitmapPixel } from './bitmapPixel';
|
|
4
|
+
import { createBitmapRegion } from './bitmapRegion';
|
|
5
|
+
|
|
6
|
+
function countChangedPixels(data: Readonly<Uint8ClampedArray>, original: Readonly<Uint8ClampedArray>): number {
|
|
7
|
+
let changed = 0;
|
|
8
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
9
|
+
if (
|
|
10
|
+
data[i] !== original[i] ||
|
|
11
|
+
data[i + 1] !== original[i + 1] ||
|
|
12
|
+
data[i + 2] !== original[i + 2] ||
|
|
13
|
+
data[i + 3] !== original[i + 3]
|
|
14
|
+
) {
|
|
15
|
+
changed++;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return changed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('dissolveBitmapPixels', () => {
|
|
22
|
+
it('dissolves at most pixelCount pixels per call', () => {
|
|
23
|
+
const source = createBitmap(4, 4, 0x112233ff);
|
|
24
|
+
const dest = createBitmap(4, 4, 0x00000000);
|
|
25
|
+
const original = dest.data.slice();
|
|
26
|
+
dissolveBitmapPixels(createBitmapRegion(dest), createBitmapRegion(source), 0, 5);
|
|
27
|
+
expect(countChangedPixels(dest.data, original)).toBe(5);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('eventually covers every pixel exactly once so dest matches source', () => {
|
|
31
|
+
const source = createBitmap(5, 3);
|
|
32
|
+
for (let i = 0; i < 5 * 3; i++) setBitmapPixel(source, i % 5, (i / 5) | 0, (0x01010100 * (i + 1)) >>> 0);
|
|
33
|
+
const dest = createBitmap(5, 3, 0x00000000);
|
|
34
|
+
const sourceRegion = createBitmapRegion(source);
|
|
35
|
+
const destRegion = createBitmapRegion(dest);
|
|
36
|
+
|
|
37
|
+
let seed = 0;
|
|
38
|
+
for (let call = 0; call < 8; call++) seed = dissolveBitmapPixels(destRegion, sourceRegion, seed, 2);
|
|
39
|
+
|
|
40
|
+
expect(Array.from(dest.data)).toEqual(Array.from(source.data));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('is deterministic for the same seed sequence', () => {
|
|
44
|
+
const source = createBitmap(6, 6, 0xaabbccff);
|
|
45
|
+
const a = createBitmap(6, 6, 0x00000000);
|
|
46
|
+
const b = createBitmap(6, 6, 0x00000000);
|
|
47
|
+
const sourceRegion = createBitmapRegion(source);
|
|
48
|
+
dissolveBitmapPixels(createBitmapRegion(a), sourceRegion, 3, 7);
|
|
49
|
+
dissolveBitmapPixels(createBitmapRegion(b), sourceRegion, 3, 7);
|
|
50
|
+
expect(Array.from(a.data)).toEqual(Array.from(b.data));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns a terminal seed once fully dissolved that no-ops on reuse', () => {
|
|
54
|
+
const source = createBitmap(4, 4, 0x445566ff);
|
|
55
|
+
const dest = createBitmap(4, 4, 0x00000000);
|
|
56
|
+
const sourceRegion = createBitmapRegion(source);
|
|
57
|
+
const destRegion = createBitmapRegion(dest);
|
|
58
|
+
|
|
59
|
+
let seed = 0;
|
|
60
|
+
for (let call = 0; call < 16; call++) seed = dissolveBitmapPixels(destRegion, sourceRegion, seed, 1);
|
|
61
|
+
expect(Array.from(dest.data)).toEqual(Array.from(source.data));
|
|
62
|
+
|
|
63
|
+
const afterComplete = dissolveBitmapPixels(destRegion, sourceRegion, seed, 4);
|
|
64
|
+
expect(afterComplete).toBe(seed);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('dissolves toward fillColor when source and dest are the same region', () => {
|
|
68
|
+
const bitmap = createBitmap(3, 3, 0x112233ff);
|
|
69
|
+
const region = createBitmapRegion(bitmap);
|
|
70
|
+
let seed = 0;
|
|
71
|
+
for (let call = 0; call < 9; call++) seed = dissolveBitmapPixels(region, region, seed, 1, 0x99887766);
|
|
72
|
+
for (let i = 0; i < 9; i++) expect(getBitmapPixel(bitmap, i % 3, (i / 3) | 0)).toBe(0x99887766);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('ignores fillColor and copies from source when regions differ', () => {
|
|
76
|
+
const source = createBitmap(2, 2, 0x0a0b0c0d);
|
|
77
|
+
const dest = createBitmap(2, 2, 0x00000000);
|
|
78
|
+
let seed = 0;
|
|
79
|
+
for (let call = 0; call < 4; call++) {
|
|
80
|
+
seed = dissolveBitmapPixels(createBitmapRegion(dest), createBitmapRegion(source), seed, 1, 0xffffffff);
|
|
81
|
+
}
|
|
82
|
+
expect(Array.from(dest.data)).toEqual(Array.from(source.data));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('returns the seed unchanged for a non-positive pixelCount without writing', () => {
|
|
86
|
+
const source = createBitmap(4, 4, 0x112233ff);
|
|
87
|
+
const dest = createBitmap(4, 4, 0x00000000);
|
|
88
|
+
const original = dest.data.slice();
|
|
89
|
+
expect(dissolveBitmapPixels(createBitmapRegion(dest), createBitmapRegion(source), 2, 0)).toBe(2);
|
|
90
|
+
expect(countChangedPixels(dest.data, original)).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns the seed for a zero-area region', () => {
|
|
94
|
+
const source = createBitmap(4, 4, 0x112233ff);
|
|
95
|
+
const dest = createBitmap(4, 4, 0x00000000);
|
|
96
|
+
const region = createBitmapRegion(dest, 0, 0, 0, 0);
|
|
97
|
+
expect(dissolveBitmapPixels(region, createBitmapRegion(source), 5, 10)).toBe(5);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { extractBitmapPixels } from './bitmapComposite';
|
|
3
|
+
import { drawBitmap } from './bitmapDraw';
|
|
4
|
+
import { setBitmapPixel } from './bitmapPixel';
|
|
5
|
+
|
|
6
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
7
|
+
return { bitmap, x, y, width, height };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readExtractedPixel(buf: Uint8ClampedArray, regionWidth: number, x: number, y: number): number {
|
|
11
|
+
const i = (y * regionWidth + x) * 4;
|
|
12
|
+
return ((buf[i] << 24) | (buf[i + 1] << 16) | (buf[i + 2] << 8) | buf[i + 3]) >>> 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('drawBitmap', () => {
|
|
16
|
+
it('does not throw when drawing onto a canvas', () => {
|
|
17
|
+
const src = createBitmap(2, 2, 0x112233ff);
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
canvas.width = 4;
|
|
20
|
+
canvas.height = 4;
|
|
21
|
+
expect(() => drawBitmap(canvas, region(src), 0, 0)).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not throw when drawing at an offset', () => {
|
|
25
|
+
const src = createBitmap(2, 2, 0x112233ff);
|
|
26
|
+
const canvas = document.createElement('canvas');
|
|
27
|
+
canvas.width = 8;
|
|
28
|
+
canvas.height = 8;
|
|
29
|
+
expect(() => drawBitmap(canvas, region(src), 2, 2)).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('extracts correct pixels from a filled source', () => {
|
|
33
|
+
const src = createBitmap(4, 4, 0xff0000ff);
|
|
34
|
+
const r = region(src);
|
|
35
|
+
const buf = new Uint8ClampedArray(r.width * r.height * 4);
|
|
36
|
+
extractBitmapPixels(buf, r);
|
|
37
|
+
expect(readExtractedPixel(buf, r.width, 0, 0)).toBe(0xff0000ff);
|
|
38
|
+
expect(readExtractedPixel(buf, r.width, 3, 3)).toBe(0xff0000ff);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('extracts correct pixels from a sub-region', () => {
|
|
42
|
+
const src = createBitmap(4, 4);
|
|
43
|
+
setBitmapPixel(src, 1, 1, 0xaabbccdd);
|
|
44
|
+
setBitmapPixel(src, 2, 2, 0x11223344);
|
|
45
|
+
const r = region(src, 1, 1, 2, 2);
|
|
46
|
+
const buf = new Uint8ClampedArray(r.width * r.height * 4);
|
|
47
|
+
extractBitmapPixels(buf, r);
|
|
48
|
+
expect(readExtractedPixel(buf, r.width, 0, 0)).toBe(0xaabbccdd);
|
|
49
|
+
expect(readExtractedPixel(buf, r.width, 1, 1)).toBe(0x11223344);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('extracts zeroes from an empty bitmap', () => {
|
|
53
|
+
const src = createBitmap(2, 2);
|
|
54
|
+
const r = region(src);
|
|
55
|
+
const buf = new Uint8ClampedArray(r.width * r.height * 4);
|
|
56
|
+
extractBitmapPixels(buf, r);
|
|
57
|
+
expect(readExtractedPixel(buf, r.width, 0, 0)).toBe(0x00000000);
|
|
58
|
+
expect(readExtractedPixel(buf, r.width, 1, 1)).toBe(0x00000000);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('is a no-op for a zero-dimension region', () => {
|
|
62
|
+
const src = createBitmap(2, 2, 0x112233ff);
|
|
63
|
+
const canvas = document.createElement('canvas');
|
|
64
|
+
canvas.width = 4;
|
|
65
|
+
canvas.height = 4;
|
|
66
|
+
expect(() => drawBitmap(canvas, region(src, 0, 0, 0, 0), 0, 0)).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { encodeBitmap } from './bitmapEncode';
|
|
3
|
+
|
|
4
|
+
describe('encodeBitmap', () => {
|
|
5
|
+
it('returns a non-empty Uint8Array for a simple image', () => {
|
|
6
|
+
const img = createBitmap(2, 2, 0x112233ff);
|
|
7
|
+
const bytes = encodeBitmap(img);
|
|
8
|
+
expect(bytes).toBeInstanceOf(Uint8Array);
|
|
9
|
+
expect(bytes.length).toBeGreaterThan(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('accepts jpeg format', () => {
|
|
13
|
+
const img = createBitmap(2, 2, 0x112233ff);
|
|
14
|
+
const bytes = encodeBitmap(img, 'jpeg');
|
|
15
|
+
expect(bytes).toBeInstanceOf(Uint8Array);
|
|
16
|
+
expect(bytes.length).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { fillBitmapRectangle, floodFillBitmap } from './bitmapFill';
|
|
3
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
4
|
+
|
|
5
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
6
|
+
return { bitmap, x, y, width, height };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('fillBitmapRectangle', () => {
|
|
10
|
+
it('fills the specified region', () => {
|
|
11
|
+
const img = createBitmap(4, 4);
|
|
12
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
|
|
13
|
+
expect(getBitmapPixel(img, 1, 1)).toBe(0xaabbccff);
|
|
14
|
+
expect(getBitmapPixel(img, 2, 2)).toBe(0xaabbccff);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('bumps the bitmap version (self-invalidation)', () => {
|
|
18
|
+
const img = createBitmap(4, 4);
|
|
19
|
+
const before = img.version;
|
|
20
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
|
|
21
|
+
expect(img.version).toBe(before + 1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not affect pixels outside the region', () => {
|
|
25
|
+
const img = createBitmap(4, 4);
|
|
26
|
+
fillBitmapRectangle(region(img, 1, 1, 2, 2), 0xaabbccff);
|
|
27
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0x00000000);
|
|
28
|
+
expect(getBitmapPixel(img, 3, 3)).toBe(0x00000000);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('skips pixels outside the bitmap bounds', () => {
|
|
32
|
+
const img = createBitmap(2, 2, 0x000000ff);
|
|
33
|
+
fillBitmapRectangle(region(img, -1, -1, 4, 4), 0xffffffff);
|
|
34
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xffffffff);
|
|
35
|
+
expect(getBitmapPixel(img, 1, 1)).toBe(0xffffffff);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('floodFillBitmap', () => {
|
|
40
|
+
it('fills a connected region', () => {
|
|
41
|
+
const img = createBitmap(3, 3, 0xffffffff);
|
|
42
|
+
floodFillBitmap(img, 0, 0, 0x000000ff);
|
|
43
|
+
for (let py = 0; py < 3; py++) {
|
|
44
|
+
for (let px = 0; px < 3; px++) {
|
|
45
|
+
expect(getBitmapPixel(img, px, py)).toBe(0x000000ff);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not cross a barrier', () => {
|
|
51
|
+
const img = createBitmap(3, 3, 0xffffffff);
|
|
52
|
+
for (let py = 0; py < 3; py++) {
|
|
53
|
+
const i = (py * 3 + 1) * 4;
|
|
54
|
+
img.data[i] = 0;
|
|
55
|
+
img.data[i + 1] = 0;
|
|
56
|
+
img.data[i + 2] = 0;
|
|
57
|
+
img.data[i + 3] = 0xff;
|
|
58
|
+
}
|
|
59
|
+
floodFillBitmap(img, 0, 0, 0x0000ffff);
|
|
60
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0x0000ffff);
|
|
61
|
+
expect(getBitmapPixel(img, 2, 0)).toBe(0xffffffff);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('is a no-op when fill color matches target', () => {
|
|
65
|
+
const img = createBitmap(2, 2, 0x112233ff);
|
|
66
|
+
floodFillBitmap(img, 0, 0, 0x112233ff);
|
|
67
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0x112233ff);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reuses the scratch buffer across calls', () => {
|
|
71
|
+
const img = createBitmap(4, 4, 0xffffffff);
|
|
72
|
+
floodFillBitmap(img, 0, 0, 0x000000ff);
|
|
73
|
+
floodFillBitmap(img, 0, 0, 0xffffffff); // second call reuses buffer
|
|
74
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xffffffff);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('reuses the scratch buffer across calls', () => {
|
|
78
|
+
const img = createBitmap(4, 4, 0xffffffff);
|
|
79
|
+
floodFillBitmap(img, 0, 0, 0x000000ff);
|
|
80
|
+
floodFillBitmap(img, 0, 0, 0xffffffff); // second call reuses buffer
|
|
81
|
+
expect(getBitmapPixel(img, 0, 0)).toBe(0xffffffff);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import {
|
|
3
|
+
compareBitmapFingerprints,
|
|
4
|
+
createBitmapFingerprint,
|
|
5
|
+
formatBitmapFingerprint,
|
|
6
|
+
parseBitmapFingerprint,
|
|
7
|
+
} from './bitmapFingerprint';
|
|
8
|
+
import { setBitmapPixel } from './bitmapPixel';
|
|
9
|
+
|
|
10
|
+
describe('compareBitmapFingerprints', () => {
|
|
11
|
+
it('reports 0 for identical fingerprints', () => {
|
|
12
|
+
const fp = createBitmapFingerprint(createBitmap(8, 8, 0x336699ff), 4);
|
|
13
|
+
expect(compareBitmapFingerprints(fp, fp)).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('reports the mean absolute per-channel difference', () => {
|
|
17
|
+
const a = createBitmapFingerprint(createBitmap(4, 4, 0x000000ff), 1);
|
|
18
|
+
const b = createBitmapFingerprint(createBitmap(4, 4, 0x0c0c0cff), 1);
|
|
19
|
+
// one cell, RGB all differ by 12 → mean abs diff 12
|
|
20
|
+
expect(compareBitmapFingerprints(a, b)).toBe(12);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('throws when gridSizes differ', () => {
|
|
24
|
+
const a = createBitmapFingerprint(createBitmap(4, 4), 2);
|
|
25
|
+
const b = createBitmapFingerprint(createBitmap(4, 4), 4);
|
|
26
|
+
expect(() => compareBitmapFingerprints(a, b)).toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('stays small for antialiasing-scale noise but large for a colour swap', () => {
|
|
30
|
+
const base = createBitmap(16, 16, 0x2040a0ff);
|
|
31
|
+
const noisy = createBitmap(16, 16, 0x2040a0ff);
|
|
32
|
+
setBitmapPixel(noisy, 0, 0, 0x2442a2ff); // a few pixels nudged
|
|
33
|
+
setBitmapPixel(noisy, 5, 5, 0x1e3e9eff);
|
|
34
|
+
const swapped = createBitmap(16, 16, 0xa04020ff); // whole image recoloured
|
|
35
|
+
const fp = createBitmapFingerprint(base, 16);
|
|
36
|
+
expect(compareBitmapFingerprints(fp, createBitmapFingerprint(noisy, 16))).toBeLessThan(2);
|
|
37
|
+
expect(compareBitmapFingerprints(fp, createBitmapFingerprint(swapped, 16))).toBeGreaterThan(50);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createBitmapFingerprint', () => {
|
|
42
|
+
it('averages a solid bitmap to that colour in every cell', () => {
|
|
43
|
+
const fp = createBitmapFingerprint(createBitmap(8, 8, 0x336699ff), 4);
|
|
44
|
+
expect(fp.gridSize).toBe(4);
|
|
45
|
+
expect(fp.cells.length).toBe(4 * 4 * 3);
|
|
46
|
+
expect([fp.cells[0], fp.cells[1], fp.cells[2]]).toEqual([0x33, 0x66, 0x99]);
|
|
47
|
+
expect([fp.cells[fp.cells.length - 3], fp.cells[fp.cells.length - 2], fp.cells[fp.cells.length - 1]]).toEqual([
|
|
48
|
+
0x33, 0x66, 0x99,
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('places a coloured quadrant in the matching grid cell', () => {
|
|
53
|
+
const bitmap = createBitmap(2, 2, 0x000000ff);
|
|
54
|
+
setBitmapPixel(bitmap, 0, 0, 0xff0000ff); // top-left
|
|
55
|
+
const fp = createBitmapFingerprint(bitmap, 2);
|
|
56
|
+
expect([fp.cells[0], fp.cells[1], fp.cells[2]]).toEqual([255, 0, 0]); // cell (0,0)
|
|
57
|
+
expect([fp.cells[3], fp.cells[4], fp.cells[5]]).toEqual([0, 0, 0]); // cell (1,0)
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('throws on a non-positive gridSize', () => {
|
|
61
|
+
expect(() => createBitmapFingerprint(createBitmap(4, 4), 0)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns an all-zero grid for an empty bitmap', () => {
|
|
65
|
+
const fp = createBitmapFingerprint(createBitmap(0, 0), 2);
|
|
66
|
+
expect(fp.cells.every((v) => v === 0)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('formatBitmapFingerprint', () => {
|
|
71
|
+
it('round-trips through parseBitmapFingerprint', () => {
|
|
72
|
+
const bitmap = createBitmap(8, 8, 0x000000ff);
|
|
73
|
+
setBitmapPixel(bitmap, 1, 1, 0x12ab34ff);
|
|
74
|
+
setBitmapPixel(bitmap, 6, 6, 0xfe01dcff);
|
|
75
|
+
const fp = createBitmapFingerprint(bitmap, 4);
|
|
76
|
+
const parsed = parseBitmapFingerprint(formatBitmapFingerprint(fp));
|
|
77
|
+
expect(parsed).not.toBeNull();
|
|
78
|
+
expect(parsed!.gridSize).toBe(4);
|
|
79
|
+
expect(Array.from(parsed!.cells)).toEqual(Array.from(fp.cells));
|
|
80
|
+
expect(compareBitmapFingerprints(fp, parsed!)).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('encodes gridSize and lowercase hex cells', () => {
|
|
84
|
+
const fp = createBitmapFingerprint(createBitmap(4, 4, 0x0a0b0cff), 1);
|
|
85
|
+
expect(formatBitmapFingerprint(fp)).toBe('1:0a0b0c');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('parseBitmapFingerprint', () => {
|
|
90
|
+
it('returns null for malformed text', () => {
|
|
91
|
+
expect(parseBitmapFingerprint('garbage')).toBeNull();
|
|
92
|
+
expect(parseBitmapFingerprint('')).toBeNull();
|
|
93
|
+
expect(parseBitmapFingerprint('2:abcd')).toBeNull(); // wrong cell count for gridSize 2
|
|
94
|
+
expect(parseBitmapFingerprint('1:0a0b0')).toBeNull(); // odd hex length
|
|
95
|
+
expect(parseBitmapFingerprint('1:0a0b0z')).toBeNull(); // non-hex digit
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('parses a valid single-cell fingerprint', () => {
|
|
99
|
+
const parsed = parseBitmapFingerprint('1:0a0b0c');
|
|
100
|
+
expect(parsed).not.toBeNull();
|
|
101
|
+
expect(Array.from(parsed!.cells)).toEqual([0x0a, 0x0b, 0x0c]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createBitmap } from './bitmap';
|
|
2
|
+
import { flipBitmapHorizontal, flipBitmapVertical } from './bitmapFlip';
|
|
3
|
+
import { getBitmapPixel } from './bitmapPixel';
|
|
4
|
+
|
|
5
|
+
function region(bitmap: ReturnType<typeof createBitmap>, x = 0, y = 0, width = bitmap.width, height = bitmap.height) {
|
|
6
|
+
return { bitmap, x, y, width, height };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ramp(width: number, height: number) {
|
|
10
|
+
const bitmap = createBitmap(width, height);
|
|
11
|
+
for (let i = 0; i < width * height; i++) {
|
|
12
|
+
bitmap.data[i * 4] = i;
|
|
13
|
+
bitmap.data[i * 4 + 3] = 255;
|
|
14
|
+
}
|
|
15
|
+
return bitmap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('flipBitmapHorizontal', () => {
|
|
19
|
+
it('mirrors columns left-to-right into a separate dest', () => {
|
|
20
|
+
const source = ramp(3, 1);
|
|
21
|
+
const out = createBitmap(3, 1);
|
|
22
|
+
flipBitmapHorizontal(region(out), region(source));
|
|
23
|
+
expect(out.data[0]).toBe(2);
|
|
24
|
+
expect(out.data[4]).toBe(1);
|
|
25
|
+
expect(out.data[8]).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('mirrors in place when dest and source are the same region', () => {
|
|
29
|
+
const bitmap = ramp(4, 1);
|
|
30
|
+
flipBitmapHorizontal(region(bitmap), region(bitmap));
|
|
31
|
+
expect(bitmap.data[0]).toBe(3);
|
|
32
|
+
expect(bitmap.data[4]).toBe(2);
|
|
33
|
+
expect(bitmap.data[8]).toBe(1);
|
|
34
|
+
expect(bitmap.data[12]).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('flipBitmapVertical', () => {
|
|
39
|
+
it('mirrors rows top-to-bottom into a separate dest', () => {
|
|
40
|
+
const source = ramp(1, 3);
|
|
41
|
+
const out = createBitmap(1, 3);
|
|
42
|
+
flipBitmapVertical(region(out), region(source));
|
|
43
|
+
expect(getBitmapPixel(out, 0, 0)).toBe(getBitmapPixel(source, 0, 2));
|
|
44
|
+
expect(getBitmapPixel(out, 0, 2)).toBe(getBitmapPixel(source, 0, 0));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('mirrors in place when dest and source are the same region', () => {
|
|
48
|
+
const bitmap = ramp(1, 4);
|
|
49
|
+
flipBitmapVertical(region(bitmap), region(bitmap));
|
|
50
|
+
expect(bitmap.data[0]).toBe(3);
|
|
51
|
+
expect(bitmap.data[12]).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
});
|