@flighthq/surface 0.1.0
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/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/surface.d.ts +16 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +84 -0
- package/dist/surface.js.map +1 -0
- package/dist/surfaceAffine.d.ts +27 -0
- package/dist/surfaceAffine.d.ts.map +1 -0
- package/dist/surfaceAffine.js +159 -0
- package/dist/surfaceAffine.js.map +1 -0
- package/dist/surfaceAlpha.d.ts +27 -0
- package/dist/surfaceAlpha.d.ts.map +1 -0
- package/dist/surfaceAlpha.js +83 -0
- package/dist/surfaceAlpha.js.map +1 -0
- package/dist/surfaceBevel.d.ts +42 -0
- package/dist/surfaceBevel.d.ts.map +1 -0
- package/dist/surfaceBevel.js +102 -0
- package/dist/surfaceBevel.js.map +1 -0
- package/dist/surfaceBlur.d.ts +62 -0
- package/dist/surfaceBlur.d.ts.map +1 -0
- package/dist/surfaceBlur.js +263 -0
- package/dist/surfaceBlur.js.map +1 -0
- package/dist/surfaceChannel.d.ts +24 -0
- package/dist/surfaceChannel.d.ts.map +1 -0
- package/dist/surfaceChannel.js +126 -0
- package/dist/surfaceChannel.js.map +1 -0
- package/dist/surfaceColorMatrix.d.ts +57 -0
- package/dist/surfaceColorMatrix.d.ts.map +1 -0
- package/dist/surfaceColorMatrix.js +127 -0
- package/dist/surfaceColorMatrix.js.map +1 -0
- package/dist/surfaceCompare.d.ts +18 -0
- package/dist/surfaceCompare.d.ts.map +1 -0
- package/dist/surfaceCompare.js +64 -0
- package/dist/surfaceCompare.js.map +1 -0
- package/dist/surfaceComposite.d.ts +53 -0
- package/dist/surfaceComposite.d.ts.map +1 -0
- package/dist/surfaceComposite.js +230 -0
- package/dist/surfaceComposite.js.map +1 -0
- package/dist/surfaceConvolution.d.ts +27 -0
- package/dist/surfaceConvolution.d.ts.map +1 -0
- package/dist/surfaceConvolution.js +103 -0
- package/dist/surfaceConvolution.js.map +1 -0
- package/dist/surfaceCopy.d.ts +23 -0
- package/dist/surfaceCopy.d.ts.map +1 -0
- package/dist/surfaceCopy.js +78 -0
- package/dist/surfaceCopy.js.map +1 -0
- package/dist/surfaceCoverage.d.ts +11 -0
- package/dist/surfaceCoverage.d.ts.map +1 -0
- package/dist/surfaceCoverage.js +29 -0
- package/dist/surfaceCoverage.js.map +1 -0
- package/dist/surfaceCrop.d.ts +33 -0
- package/dist/surfaceCrop.d.ts.map +1 -0
- package/dist/surfaceCrop.js +174 -0
- package/dist/surfaceCrop.js.map +1 -0
- package/dist/surfaceDisplacement.d.ts +48 -0
- package/dist/surfaceDisplacement.d.ts.map +1 -0
- package/dist/surfaceDisplacement.js +131 -0
- package/dist/surfaceDisplacement.js.map +1 -0
- package/dist/surfaceDissolve.d.ts +21 -0
- package/dist/surfaceDissolve.d.ts.map +1 -0
- package/dist/surfaceDissolve.js +97 -0
- package/dist/surfaceDissolve.js.map +1 -0
- package/dist/surfaceDraw.d.ts +8 -0
- package/dist/surfaceDraw.d.ts.map +1 -0
- package/dist/surfaceDraw.js +14 -0
- package/dist/surfaceDraw.js.map +1 -0
- package/dist/surfaceEncode.d.ts +3 -0
- package/dist/surfaceEncode.d.ts.map +1 -0
- package/dist/surfaceEncode.js +18 -0
- package/dist/surfaceEncode.js.map +1 -0
- package/dist/surfaceFill.d.ts +12 -0
- package/dist/surfaceFill.d.ts.map +1 -0
- package/dist/surfaceFill.js +85 -0
- package/dist/surfaceFill.js.map +1 -0
- package/dist/surfaceFingerprint.d.ts +26 -0
- package/dist/surfaceFingerprint.d.ts.map +1 -0
- package/dist/surfaceFingerprint.js +99 -0
- package/dist/surfaceFingerprint.js.map +1 -0
- package/dist/surfaceFlip.d.ts +16 -0
- package/dist/surfaceFlip.d.ts.map +1 -0
- package/dist/surfaceFlip.js +98 -0
- package/dist/surfaceFlip.js.map +1 -0
- package/dist/surfaceFormat.d.ts +26 -0
- package/dist/surfaceFormat.d.ts.map +1 -0
- package/dist/surfaceFormat.js +80 -0
- package/dist/surfaceFormat.js.map +1 -0
- package/dist/surfaceFrom.d.ts +14 -0
- package/dist/surfaceFrom.d.ts.map +1 -0
- package/dist/surfaceFrom.js +87 -0
- package/dist/surfaceFrom.js.map +1 -0
- package/dist/surfaceGradient.d.ts +57 -0
- package/dist/surfaceGradient.d.ts.map +1 -0
- package/dist/surfaceGradient.js +176 -0
- package/dist/surfaceGradient.js.map +1 -0
- package/dist/surfaceGradientFill.d.ts +32 -0
- package/dist/surfaceGradientFill.d.ts.map +1 -0
- package/dist/surfaceGradientFill.js +120 -0
- package/dist/surfaceGradientFill.js.map +1 -0
- package/dist/surfaceHistogram.d.ts +20 -0
- package/dist/surfaceHistogram.d.ts.map +1 -0
- package/dist/surfaceHistogram.js +61 -0
- package/dist/surfaceHistogram.js.map +1 -0
- package/dist/surfaceImageChannel.d.ts +2 -0
- package/dist/surfaceImageChannel.d.ts.map +1 -0
- package/dist/surfaceImageChannel.js +2 -0
- package/dist/surfaceImageChannel.js.map +1 -0
- package/dist/surfaceMedian.d.ts +14 -0
- package/dist/surfaceMedian.d.ts.map +1 -0
- package/dist/surfaceMedian.js +77 -0
- package/dist/surfaceMedian.js.map +1 -0
- package/dist/surfaceMorphological.d.ts +29 -0
- package/dist/surfaceMorphological.d.ts.map +1 -0
- package/dist/surfaceMorphological.js +81 -0
- package/dist/surfaceMorphological.js.map +1 -0
- package/dist/surfaceNoise.d.ts +48 -0
- package/dist/surfaceNoise.d.ts.map +1 -0
- package/dist/surfaceNoise.js +245 -0
- package/dist/surfaceNoise.js.map +1 -0
- package/dist/surfacePaletteMap.d.ts +17 -0
- package/dist/surfacePaletteMap.d.ts.map +1 -0
- package/dist/surfacePaletteMap.js +45 -0
- package/dist/surfacePaletteMap.js.map +1 -0
- package/dist/surfacePixel.d.ts +9 -0
- package/dist/surfacePixel.d.ts.map +1 -0
- package/dist/surfacePixel.js +36 -0
- package/dist/surfacePixel.js.map +1 -0
- package/dist/surfacePixelate.d.ts +13 -0
- package/dist/surfacePixelate.d.ts.map +1 -0
- package/dist/surfacePixelate.js +61 -0
- package/dist/surfacePixelate.js.map +1 -0
- package/dist/surfaceQuery.d.ts +13 -0
- package/dist/surfaceQuery.d.ts.map +1 -0
- package/dist/surfaceQuery.js +46 -0
- package/dist/surfaceQuery.js.map +1 -0
- package/dist/surfaceRegion.d.ts +20 -0
- package/dist/surfaceRegion.d.ts.map +1 -0
- package/dist/surfaceRegion.js +28 -0
- package/dist/surfaceRegion.js.map +1 -0
- package/dist/surfaceResize.d.ts +27 -0
- package/dist/surfaceResize.d.ts.map +1 -0
- package/dist/surfaceResize.js +191 -0
- package/dist/surfaceResize.js.map +1 -0
- package/dist/surfaceRotate.d.ts +32 -0
- package/dist/surfaceRotate.d.ts.map +1 -0
- package/dist/surfaceRotate.js +143 -0
- package/dist/surfaceRotate.js.map +1 -0
- package/dist/surfaceShadow.d.ts +107 -0
- package/dist/surfaceShadow.d.ts.map +1 -0
- package/dist/surfaceShadow.js +173 -0
- package/dist/surfaceShadow.js.map +1 -0
- package/dist/surfaceSharpen.d.ts +25 -0
- package/dist/surfaceSharpen.d.ts.map +1 -0
- package/dist/surfaceSharpen.js +47 -0
- package/dist/surfaceSharpen.js.map +1 -0
- package/dist/surfaceTone.d.ts +28 -0
- package/dist/surfaceTone.d.ts.map +1 -0
- package/dist/surfaceTone.js +69 -0
- package/dist/surfaceTone.js.map +1 -0
- package/dist/surfaceTransform.d.ts +40 -0
- package/dist/surfaceTransform.d.ts.map +1 -0
- package/dist/surfaceTransform.js +163 -0
- package/dist/surfaceTransform.js.map +1 -0
- package/dist/surfaceWarp.d.ts +48 -0
- package/dist/surfaceWarp.d.ts.map +1 -0
- package/dist/surfaceWarp.js +284 -0
- package/dist/surfaceWarp.js.map +1 -0
- package/package.json +39 -0
- package/src/surface.test.ts +78 -0
- package/src/surfaceAffine.test.ts +98 -0
- package/src/surfaceAlpha.test.ts +113 -0
- package/src/surfaceBevel.test.ts +67 -0
- package/src/surfaceBlur.test.ts +240 -0
- package/src/surfaceChannel.test.ts +97 -0
- package/src/surfaceColorMatrix.test.ts +220 -0
- package/src/surfaceCompare.test.ts +92 -0
- package/src/surfaceComposite.test.ts +256 -0
- package/src/surfaceConvolution.test.ts +136 -0
- package/src/surfaceCopy.test.ts +54 -0
- package/src/surfaceCoverage.test.ts +39 -0
- package/src/surfaceCrop.test.ts +116 -0
- package/src/surfaceDisplacement.test.ts +142 -0
- package/src/surfaceDissolve.test.ts +99 -0
- package/src/surfaceDraw.test.ts +38 -0
- package/src/surfaceEncode.test.ts +18 -0
- package/src/surfaceFill.test.ts +89 -0
- package/src/surfaceFingerprint.test.ts +103 -0
- package/src/surfaceFlip.test.ts +59 -0
- package/src/surfaceFormat.test.ts +124 -0
- package/src/surfaceFrom.test.ts +62 -0
- package/src/surfaceGradient.test.ts +91 -0
- package/src/surfaceGradientFill.test.ts +107 -0
- package/src/surfaceHistogram.test.ts +70 -0
- package/src/surfaceImageChannel.test.ts +19 -0
- package/src/surfaceMedian.test.ts +45 -0
- package/src/surfaceMorphological.test.ts +75 -0
- package/src/surfaceNoise.test.ts +169 -0
- package/src/surfacePaletteMap.test.ts +55 -0
- package/src/surfacePixel.test.ts +99 -0
- package/src/surfacePixelate.test.ts +46 -0
- package/src/surfaceQuery.test.ts +48 -0
- package/src/surfaceRegion.test.ts +34 -0
- package/src/surfaceResize.test.ts +112 -0
- package/src/surfaceRotate.test.ts +119 -0
- package/src/surfaceShadow.test.ts +165 -0
- package/src/surfaceSharpen.test.ts +50 -0
- package/src/surfaceTone.test.ts +104 -0
- package/src/surfaceTransform.test.ts +134 -0
- package/src/surfaceWarp.test.ts +124 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import {
|
|
3
|
+
compareSurfaceFingerprints,
|
|
4
|
+
createSurfaceFingerprint,
|
|
5
|
+
formatSurfaceFingerprint,
|
|
6
|
+
parseSurfaceFingerprint,
|
|
7
|
+
} from './surfaceFingerprint';
|
|
8
|
+
import { setSurfacePixel } from './surfacePixel';
|
|
9
|
+
|
|
10
|
+
describe('compareSurfaceFingerprints', () => {
|
|
11
|
+
it('reports 0 for identical fingerprints', () => {
|
|
12
|
+
const fp = createSurfaceFingerprint(createSurface(8, 8, 0x336699ff), 4);
|
|
13
|
+
expect(compareSurfaceFingerprints(fp, fp)).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('reports the mean absolute per-channel difference', () => {
|
|
17
|
+
const a = createSurfaceFingerprint(createSurface(4, 4, 0x000000ff), 1);
|
|
18
|
+
const b = createSurfaceFingerprint(createSurface(4, 4, 0x0c0c0cff), 1);
|
|
19
|
+
// one cell, RGB all differ by 12 → mean abs diff 12
|
|
20
|
+
expect(compareSurfaceFingerprints(a, b)).toBe(12);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('throws when gridSizes differ', () => {
|
|
24
|
+
const a = createSurfaceFingerprint(createSurface(4, 4), 2);
|
|
25
|
+
const b = createSurfaceFingerprint(createSurface(4, 4), 4);
|
|
26
|
+
expect(() => compareSurfaceFingerprints(a, b)).toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('stays small for antialiasing-scale noise but large for a colour swap', () => {
|
|
30
|
+
const base = createSurface(16, 16, 0x2040a0ff);
|
|
31
|
+
const noisy = createSurface(16, 16, 0x2040a0ff);
|
|
32
|
+
setSurfacePixel(noisy, 0, 0, 0x2442a2ff); // a few pixels nudged
|
|
33
|
+
setSurfacePixel(noisy, 5, 5, 0x1e3e9eff);
|
|
34
|
+
const swapped = createSurface(16, 16, 0xa04020ff); // whole image recoloured
|
|
35
|
+
const fp = createSurfaceFingerprint(base, 16);
|
|
36
|
+
expect(compareSurfaceFingerprints(fp, createSurfaceFingerprint(noisy, 16))).toBeLessThan(2);
|
|
37
|
+
expect(compareSurfaceFingerprints(fp, createSurfaceFingerprint(swapped, 16))).toBeGreaterThan(50);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createSurfaceFingerprint', () => {
|
|
42
|
+
it('averages a solid surface to that colour in every cell', () => {
|
|
43
|
+
const fp = createSurfaceFingerprint(createSurface(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 surface = createSurface(2, 2, 0x000000ff);
|
|
54
|
+
setSurfacePixel(surface, 0, 0, 0xff0000ff); // top-left
|
|
55
|
+
const fp = createSurfaceFingerprint(surface, 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(() => createSurfaceFingerprint(createSurface(4, 4), 0)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns an all-zero grid for an empty surface', () => {
|
|
65
|
+
const fp = createSurfaceFingerprint(createSurface(0, 0), 2);
|
|
66
|
+
expect(fp.cells.every((v) => v === 0)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('formatSurfaceFingerprint', () => {
|
|
71
|
+
it('round-trips through parseSurfaceFingerprint', () => {
|
|
72
|
+
const surface = createSurface(8, 8, 0x000000ff);
|
|
73
|
+
setSurfacePixel(surface, 1, 1, 0x12ab34ff);
|
|
74
|
+
setSurfacePixel(surface, 6, 6, 0xfe01dcff);
|
|
75
|
+
const fp = createSurfaceFingerprint(surface, 4);
|
|
76
|
+
const parsed = parseSurfaceFingerprint(formatSurfaceFingerprint(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(compareSurfaceFingerprints(fp, parsed!)).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('encodes gridSize and lowercase hex cells', () => {
|
|
84
|
+
const fp = createSurfaceFingerprint(createSurface(4, 4, 0x0a0b0cff), 1);
|
|
85
|
+
expect(formatSurfaceFingerprint(fp)).toBe('1:0a0b0c');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('parseSurfaceFingerprint', () => {
|
|
90
|
+
it('returns null for malformed text', () => {
|
|
91
|
+
expect(parseSurfaceFingerprint('garbage')).toBeNull();
|
|
92
|
+
expect(parseSurfaceFingerprint('')).toBeNull();
|
|
93
|
+
expect(parseSurfaceFingerprint('2:abcd')).toBeNull(); // wrong cell count for gridSize 2
|
|
94
|
+
expect(parseSurfaceFingerprint('1:0a0b0')).toBeNull(); // odd hex length
|
|
95
|
+
expect(parseSurfaceFingerprint('1:0a0b0z')).toBeNull(); // non-hex digit
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('parses a valid single-cell fingerprint', () => {
|
|
99
|
+
const parsed = parseSurfaceFingerprint('1:0a0b0c');
|
|
100
|
+
expect(parsed).not.toBeNull();
|
|
101
|
+
expect(Array.from(parsed!.cells)).toEqual([0x0a, 0x0b, 0x0c]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { flipSurfaceHorizontal, flipSurfaceVertical } from './surfaceFlip';
|
|
3
|
+
import { getSurfacePixel } from './surfacePixel';
|
|
4
|
+
|
|
5
|
+
function region(
|
|
6
|
+
surface: ReturnType<typeof createSurface>,
|
|
7
|
+
x = 0,
|
|
8
|
+
y = 0,
|
|
9
|
+
width = surface.width,
|
|
10
|
+
height = surface.height,
|
|
11
|
+
) {
|
|
12
|
+
return { surface, x, y, width, height };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ramp(width: number, height: number) {
|
|
16
|
+
const surface = createSurface(width, height);
|
|
17
|
+
for (let i = 0; i < width * height; i++) {
|
|
18
|
+
surface.data[i * 4] = i;
|
|
19
|
+
surface.data[i * 4 + 3] = 255;
|
|
20
|
+
}
|
|
21
|
+
return surface;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('flipSurfaceHorizontal', () => {
|
|
25
|
+
it('mirrors columns left-to-right into a separate dest', () => {
|
|
26
|
+
const source = ramp(3, 1);
|
|
27
|
+
const out = createSurface(3, 1);
|
|
28
|
+
flipSurfaceHorizontal(region(out), region(source));
|
|
29
|
+
expect(out.data[0]).toBe(2);
|
|
30
|
+
expect(out.data[4]).toBe(1);
|
|
31
|
+
expect(out.data[8]).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('mirrors in place when dest and source are the same region', () => {
|
|
35
|
+
const surface = ramp(4, 1);
|
|
36
|
+
flipSurfaceHorizontal(region(surface), region(surface));
|
|
37
|
+
expect(surface.data[0]).toBe(3);
|
|
38
|
+
expect(surface.data[4]).toBe(2);
|
|
39
|
+
expect(surface.data[8]).toBe(1);
|
|
40
|
+
expect(surface.data[12]).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('flipSurfaceVertical', () => {
|
|
45
|
+
it('mirrors rows top-to-bottom into a separate dest', () => {
|
|
46
|
+
const source = ramp(1, 3);
|
|
47
|
+
const out = createSurface(1, 3);
|
|
48
|
+
flipSurfaceVertical(region(out), region(source));
|
|
49
|
+
expect(getSurfacePixel(out, 0, 0)).toBe(getSurfacePixel(source, 0, 2));
|
|
50
|
+
expect(getSurfacePixel(out, 0, 2)).toBe(getSurfacePixel(source, 0, 0));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('mirrors in place when dest and source are the same region', () => {
|
|
54
|
+
const surface = ramp(1, 4);
|
|
55
|
+
flipSurfaceVertical(region(surface), region(surface));
|
|
56
|
+
expect(surface.data[0]).toBe(3);
|
|
57
|
+
expect(surface.data[12]).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { convertSurfacePixelOrder, premultiplySurfacePixels, unpremultiplySurfacePixels } from './surfaceFormat';
|
|
2
|
+
|
|
3
|
+
describe('convertSurfacePixelOrder', () => {
|
|
4
|
+
it('is a no-op when from === to and out !== source', () => {
|
|
5
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
|
|
6
|
+
const out = new Uint8ClampedArray(4);
|
|
7
|
+
convertSurfacePixelOrder(out, source, 4, 'RGBA', 'RGBA');
|
|
8
|
+
expect(out[0]).toBe(0x11);
|
|
9
|
+
expect(out[1]).toBe(0x22);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('converts RGBA to BGRA', () => {
|
|
13
|
+
const source = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xff]);
|
|
14
|
+
const out = new Uint8ClampedArray(4);
|
|
15
|
+
convertSurfacePixelOrder(out, source, 4, 'RGBA', 'BGRA');
|
|
16
|
+
expect(out[0]).toBe(0xcc); // B
|
|
17
|
+
expect(out[1]).toBe(0xbb); // G
|
|
18
|
+
expect(out[2]).toBe(0xaa); // R
|
|
19
|
+
expect(out[3]).toBe(0xff); // A
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('converts RGBA to ARGB', () => {
|
|
23
|
+
const source = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xdd]);
|
|
24
|
+
const out = new Uint8ClampedArray(4);
|
|
25
|
+
convertSurfacePixelOrder(out, source, 4, 'RGBA', 'ARGB');
|
|
26
|
+
expect(out[0]).toBe(0xdd); // A
|
|
27
|
+
expect(out[1]).toBe(0xaa); // R
|
|
28
|
+
expect(out[2]).toBe(0xbb); // G
|
|
29
|
+
expect(out[3]).toBe(0xcc); // B
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('round-trips RGBA → BGRA → RGBA', () => {
|
|
33
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0x44]);
|
|
34
|
+
const tmp = new Uint8ClampedArray(4);
|
|
35
|
+
const out = new Uint8ClampedArray(4);
|
|
36
|
+
convertSurfacePixelOrder(tmp, source, 4, 'RGBA', 'BGRA');
|
|
37
|
+
convertSurfacePixelOrder(out, tmp, 4, 'BGRA', 'RGBA');
|
|
38
|
+
expect(out[0]).toBe(0x11);
|
|
39
|
+
expect(out[1]).toBe(0x22);
|
|
40
|
+
expect(out[2]).toBe(0x33);
|
|
41
|
+
expect(out[3]).toBe(0x44);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is safe in-place for the same format (no-op path)', () => {
|
|
45
|
+
const buf = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xff]);
|
|
46
|
+
convertSurfacePixelOrder(buf, buf, 4, 'RGBA', 'RGBA');
|
|
47
|
+
expect(buf[0]).toBe(0xaa);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('is safe in-place for different formats', () => {
|
|
51
|
+
const buf = new Uint8ClampedArray([0xaa, 0xbb, 0xcc, 0xff]);
|
|
52
|
+
convertSurfacePixelOrder(buf, buf, 4, 'RGBA', 'BGRA');
|
|
53
|
+
expect(buf[0]).toBe(0xcc);
|
|
54
|
+
expect(buf[2]).toBe(0xaa);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('premultiplySurfacePixels', () => {
|
|
59
|
+
it('scales RGB by alpha/255', () => {
|
|
60
|
+
const source = new Uint8ClampedArray([0xff, 0x80, 0x00, 0x80]);
|
|
61
|
+
const out = new Uint8ClampedArray(4);
|
|
62
|
+
premultiplySurfacePixels(out, source, 4);
|
|
63
|
+
expect(out[0]).toBeCloseTo(0x80, 0); // 255 * 128/255 ≈ 128
|
|
64
|
+
expect(out[1]).toBeCloseTo(0x40, 0); // 128 * 128/255 ≈ 64
|
|
65
|
+
expect(out[2]).toBe(0);
|
|
66
|
+
expect(out[3]).toBe(0x80); // alpha unchanged
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('fully transparent pixel becomes all zeros', () => {
|
|
70
|
+
const source = new Uint8ClampedArray([0xff, 0xff, 0xff, 0x00]);
|
|
71
|
+
const out = new Uint8ClampedArray(4);
|
|
72
|
+
premultiplySurfacePixels(out, source, 4);
|
|
73
|
+
expect(out[0]).toBe(0);
|
|
74
|
+
expect(out[1]).toBe(0);
|
|
75
|
+
expect(out[2]).toBe(0);
|
|
76
|
+
expect(out[3]).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('fully opaque pixel is unchanged', () => {
|
|
80
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
|
|
81
|
+
const out = new Uint8ClampedArray(4);
|
|
82
|
+
premultiplySurfacePixels(out, source, 4);
|
|
83
|
+
expect(out[0]).toBe(0x11);
|
|
84
|
+
expect(out[1]).toBe(0x22);
|
|
85
|
+
expect(out[2]).toBe(0x33);
|
|
86
|
+
expect(out[3]).toBe(0xff);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('is safe in-place', () => {
|
|
90
|
+
const buf = new Uint8ClampedArray([0xff, 0x00, 0x00, 0x80]);
|
|
91
|
+
premultiplySurfacePixels(buf, buf, 4);
|
|
92
|
+
expect(buf[0]).toBeCloseTo(0x80, 0);
|
|
93
|
+
expect(buf[3]).toBe(0x80);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('unpremultiplySurfacePixels', () => {
|
|
98
|
+
it('recovers original RGB after premultiply round-trip within ±1', () => {
|
|
99
|
+
// Integer premultiply introduces at most ±1 rounding error per channel.
|
|
100
|
+
const source = new Uint8ClampedArray([0xc0, 0x80, 0x40, 0x80]);
|
|
101
|
+
const premul = new Uint8ClampedArray(4);
|
|
102
|
+
const out = new Uint8ClampedArray(4);
|
|
103
|
+
premultiplySurfacePixels(premul, source, 4);
|
|
104
|
+
unpremultiplySurfacePixels(out, premul, 4);
|
|
105
|
+
expect(Math.abs(out[0] - 0xc0)).toBeLessThanOrEqual(1);
|
|
106
|
+
expect(Math.abs(out[1] - 0x80)).toBeLessThanOrEqual(1);
|
|
107
|
+
expect(out[3]).toBe(0x80);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('fully transparent pixel remains all zeros', () => {
|
|
111
|
+
const source = new Uint8ClampedArray([0x00, 0x00, 0x00, 0x00]);
|
|
112
|
+
const out = new Uint8ClampedArray(4);
|
|
113
|
+
unpremultiplySurfacePixels(out, source, 4);
|
|
114
|
+
expect(out[0]).toBe(0);
|
|
115
|
+
expect(out[3]).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('is safe in-place', () => {
|
|
119
|
+
const buf = new Uint8ClampedArray([0x80, 0x00, 0x00, 0x80]);
|
|
120
|
+
unpremultiplySurfacePixels(buf, buf, 4);
|
|
121
|
+
expect(buf[0]).toBeCloseTo(0xff, 0);
|
|
122
|
+
expect(buf[3]).toBe(0x80);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createImageResource } from '@flighthq/image';
|
|
2
|
+
|
|
3
|
+
import { createSurface } from './surface';
|
|
4
|
+
import {
|
|
5
|
+
createImageResourceFromSurface,
|
|
6
|
+
createSurfaceFromCanvas,
|
|
7
|
+
createSurfaceFromImageResource,
|
|
8
|
+
createSurfaceFromImageSource,
|
|
9
|
+
} from './surfaceFrom';
|
|
10
|
+
|
|
11
|
+
describe('createImageResourceFromSurface', () => {
|
|
12
|
+
it('returns an ImageResource with matching dimensions', () => {
|
|
13
|
+
const img = createSurface(4, 4, 0x112233ff);
|
|
14
|
+
const resource = createImageResourceFromSurface(img);
|
|
15
|
+
expect(resource.width).toBe(4);
|
|
16
|
+
expect(resource.height).toBe(4);
|
|
17
|
+
expect(resource.source).not.toBeNull();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('createSurfaceFromCanvas', () => {
|
|
22
|
+
it('returns Surface matching the canvas size', () => {
|
|
23
|
+
const canvas = document.createElement('canvas');
|
|
24
|
+
canvas.width = 4;
|
|
25
|
+
canvas.height = 4;
|
|
26
|
+
const data = createSurfaceFromCanvas(canvas);
|
|
27
|
+
expect(data.width).toBe(4);
|
|
28
|
+
expect(data.height).toBe(4);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns a Surface with data length matching canvas pixels', () => {
|
|
32
|
+
const canvas = document.createElement('canvas');
|
|
33
|
+
canvas.width = 8;
|
|
34
|
+
canvas.height = 8;
|
|
35
|
+
const data = createSurfaceFromCanvas(canvas);
|
|
36
|
+
expect(data.data.length).toBe(8 * 8 * 4);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('createSurfaceFromImageResource', () => {
|
|
41
|
+
it('returns Surface matching the resource dimensions', () => {
|
|
42
|
+
const resource = createImageResource();
|
|
43
|
+
resource.width = 4;
|
|
44
|
+
resource.height = 4;
|
|
45
|
+
resource.source = null;
|
|
46
|
+
const data = createSurfaceFromImageResource(resource);
|
|
47
|
+
expect(data.width).toBe(4);
|
|
48
|
+
expect(data.height).toBe(4);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('createSurfaceFromImageSource', () => {
|
|
53
|
+
it('captures a canvas image source at the given device size', () => {
|
|
54
|
+
const canvas = document.createElement('canvas');
|
|
55
|
+
canvas.width = 8;
|
|
56
|
+
canvas.height = 4;
|
|
57
|
+
const surface = createSurfaceFromImageSource(canvas, 8, 4);
|
|
58
|
+
expect(surface.width).toBe(8);
|
|
59
|
+
expect(surface.height).toBe(4);
|
|
60
|
+
expect(surface.data.length).toBe(8 * 4 * 4);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { buildSurfaceGradientRamp, gradientBevelSurface, gradientGlowSurface } from './surfaceGradient';
|
|
3
|
+
|
|
4
|
+
function region(
|
|
5
|
+
surface: ReturnType<typeof createSurface>,
|
|
6
|
+
x = 0,
|
|
7
|
+
y = 0,
|
|
8
|
+
width = surface.width,
|
|
9
|
+
height = surface.height,
|
|
10
|
+
) {
|
|
11
|
+
return { surface, x, y, width, height };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('buildSurfaceGradientRamp', () => {
|
|
15
|
+
it('places endpoints exactly and interpolates the middle', () => {
|
|
16
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
17
|
+
buildSurfaceGradientRamp(ramp, [0xff0000, 0x0000ff], [0, 1], [0, 255]);
|
|
18
|
+
expect(Array.from(ramp.subarray(0, 4))).toEqual([255, 0, 0, 0]);
|
|
19
|
+
expect(Array.from(ramp.subarray(255 * 4, 256 * 4))).toEqual([0, 0, 255, 255]);
|
|
20
|
+
expect(Array.from(ramp.subarray(128 * 4, 129 * 4))).toEqual([127, 0, 128, 128]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('zero-fills when there are no stops', () => {
|
|
24
|
+
const ramp = new Uint8ClampedArray(256 * 4).fill(99);
|
|
25
|
+
buildSurfaceGradientRamp(ramp, [], [], []);
|
|
26
|
+
expect(ramp.every((v) => v === 0)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('gradientBevelSurface', () => {
|
|
31
|
+
it('maps the signed edge gradient through the ramp', () => {
|
|
32
|
+
// 5x1 edge: transparent x2 | opaque x3. Shadow ramp at index 0 (black),
|
|
33
|
+
// transparent middle at 128, highlight white at 255.
|
|
34
|
+
const source = createSurface(5, 1);
|
|
35
|
+
for (let x = 2; x < 5; x++) source.data[x * 4 + 3] = 255;
|
|
36
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
37
|
+
buildSurfaceGradientRamp(ramp, [0x000000, 0x808080, 0xffffff], [1, 0, 1], [0, 128, 255]);
|
|
38
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
39
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
40
|
+
gradientBevelSurface(out, scratch, region(source), ramp, {
|
|
41
|
+
angle: Math.PI,
|
|
42
|
+
distance: 1,
|
|
43
|
+
radiusX: 1,
|
|
44
|
+
radiusY: 0,
|
|
45
|
+
type: 'both',
|
|
46
|
+
});
|
|
47
|
+
// Light-facing edge (x2) is on the highlight half of the ramp → bright.
|
|
48
|
+
expect(out[2 * 4 + 0]).toBeGreaterThan(150);
|
|
49
|
+
expect(out[2 * 4 + 3]).toBeGreaterThan(0);
|
|
50
|
+
// Far edge (x4) maps to ramp index 0 → opaque black shadow.
|
|
51
|
+
expect(out[4 * 4 + 0]).toBe(0);
|
|
52
|
+
expect(out[4 * 4 + 3]).toBe(255);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('gradientGlowSurface', () => {
|
|
57
|
+
it('indexes the ramp by the blurred alpha', () => {
|
|
58
|
+
// ramp: index 0 transparent green, 255 opaque green.
|
|
59
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
60
|
+
buildSurfaceGradientRamp(ramp, [0x00ff00, 0x00ff00], [0, 1], [0, 255]);
|
|
61
|
+
const source = createSurface(1, 1, 0xffffffff);
|
|
62
|
+
const out = new Uint8ClampedArray(4);
|
|
63
|
+
const scratch = new Uint8ClampedArray(4);
|
|
64
|
+
gradientGlowSurface(out, scratch, region(source), ramp, { radiusX: 0, radiusY: 0 });
|
|
65
|
+
// Full alpha (255) → ramp[255] = opaque green.
|
|
66
|
+
expect(out[0]).toBe(0);
|
|
67
|
+
expect(out[1]).toBe(0xff);
|
|
68
|
+
expect(out[2]).toBe(0);
|
|
69
|
+
expect(out[3]).toBe(255);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('scales the ramp alpha by intensity', () => {
|
|
73
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
74
|
+
buildSurfaceGradientRamp(ramp, [0x00ff00, 0x00ff00], [1, 1], [0, 255]);
|
|
75
|
+
const source = createSurface(1, 1, 0xffffffff);
|
|
76
|
+
const out = new Uint8ClampedArray(4);
|
|
77
|
+
const scratch = new Uint8ClampedArray(4);
|
|
78
|
+
gradientGlowSurface(out, scratch, region(source), ramp, { radiusX: 0, radiusY: 0, intensity: 0.5 });
|
|
79
|
+
expect(out[3]).toBe(128);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('source.surface.data can be used as out for a full-surface region', () => {
|
|
83
|
+
const ramp = new Uint8ClampedArray(256 * 4);
|
|
84
|
+
buildSurfaceGradientRamp(ramp, [0x00ff00, 0x00ff00], [0, 1], [0, 255]);
|
|
85
|
+
const surface = createSurface(1, 1, 0xffffffff);
|
|
86
|
+
const scratch = new Uint8ClampedArray(4);
|
|
87
|
+
gradientGlowSurface(surface.data, scratch, region(surface), ramp, { radiusX: 0, radiusY: 0 });
|
|
88
|
+
expect(surface.data[1]).toBe(0xff);
|
|
89
|
+
expect(surface.data[3]).toBe(255);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { buildSurfaceGradientRamp } from './surfaceGradient';
|
|
3
|
+
import { fillSurfaceLinearGradient, fillSurfaceRadialGradient } from './surfaceGradientFill';
|
|
4
|
+
import { getSurfacePixel } from './surfacePixel';
|
|
5
|
+
import { createSurfaceRegion } from './surfaceRegion';
|
|
6
|
+
|
|
7
|
+
function buildTwoStopRamp(colorA: number, colorB: number): Uint8ClampedArray {
|
|
8
|
+
const ramp = new Uint8ClampedArray(1024);
|
|
9
|
+
const aR = (colorA >>> 24) & 0xff;
|
|
10
|
+
const aG = (colorA >> 16) & 0xff;
|
|
11
|
+
const aB = (colorA >> 8) & 0xff;
|
|
12
|
+
const aA = colorA & 0xff;
|
|
13
|
+
const bR = (colorB >>> 24) & 0xff;
|
|
14
|
+
const bG = (colorB >> 16) & 0xff;
|
|
15
|
+
const bB = (colorB >> 8) & 0xff;
|
|
16
|
+
const bA = colorB & 0xff;
|
|
17
|
+
buildSurfaceGradientRamp(
|
|
18
|
+
ramp,
|
|
19
|
+
[(aR << 16) | (aG << 8) | aB, (bR << 16) | (bG << 8) | bB],
|
|
20
|
+
[aA / 255, bA / 255],
|
|
21
|
+
[0, 255],
|
|
22
|
+
);
|
|
23
|
+
return ramp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('fillSurfaceLinearGradient', () => {
|
|
27
|
+
it('fills leftmost pixel with the first stop color', () => {
|
|
28
|
+
const surf = createSurface(4, 1);
|
|
29
|
+
const region = createSurfaceRegion(surf);
|
|
30
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
31
|
+
fillSurfaceLinearGradient(region, ramp, 0, 0, 3, 0);
|
|
32
|
+
const pixel = getSurfacePixel(surf, 0, 0);
|
|
33
|
+
expect((pixel >>> 24) & 0xff).toBeCloseTo(0xff, -1);
|
|
34
|
+
expect((pixel >> 8) & 0xff).toBeCloseTo(0x00, -1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('fills rightmost pixel with the last stop color', () => {
|
|
38
|
+
const surf = createSurface(4, 1);
|
|
39
|
+
const region = createSurfaceRegion(surf);
|
|
40
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
41
|
+
fillSurfaceLinearGradient(region, ramp, 0, 0, 3, 0);
|
|
42
|
+
const pixel = getSurfacePixel(surf, 3, 0);
|
|
43
|
+
expect((pixel >>> 24) & 0xff).toBeCloseTo(0x00, -1);
|
|
44
|
+
expect((pixel >> 8) & 0xff).toBeCloseTo(0xff, -1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('middle pixel is a blend of both stops', () => {
|
|
48
|
+
const surf = createSurface(3, 1);
|
|
49
|
+
const region = createSurfaceRegion(surf);
|
|
50
|
+
const ramp = buildTwoStopRamp(0x000000ff, 0xffffffff);
|
|
51
|
+
fillSurfaceLinearGradient(region, ramp, 0, 0, 2, 0);
|
|
52
|
+
const mid = getSurfacePixel(surf, 1, 0);
|
|
53
|
+
const r = (mid >>> 24) & 0xff;
|
|
54
|
+
expect(r).toBeGreaterThan(0);
|
|
55
|
+
expect(r).toBeLessThan(255);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('pad spread clamps colors outside axis', () => {
|
|
59
|
+
const surf = createSurface(5, 1);
|
|
60
|
+
const region = createSurfaceRegion(surf);
|
|
61
|
+
// Gradient from x=1 to x=3; pixels at x=0 and x=4 should get clamped stops.
|
|
62
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
63
|
+
fillSurfaceLinearGradient(region, ramp, 1, 0, 3, 0, 'pad');
|
|
64
|
+
const left = getSurfacePixel(surf, 0, 0);
|
|
65
|
+
const right = getSurfacePixel(surf, 4, 0);
|
|
66
|
+
expect((left >>> 24) & 0xff).toBe((getSurfacePixel(surf, 1, 0) >>> 24) & 0xff);
|
|
67
|
+
expect((right >> 8) & 0xff).toBe((getSurfacePixel(surf, 3, 0) >> 8) & 0xff);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('skips pixels outside surface bounds', () => {
|
|
71
|
+
const surf = createSurface(2, 2);
|
|
72
|
+
// Region that extends outside surface — should not throw.
|
|
73
|
+
const region = { surface: surf, x: -1, y: 0, width: 4, height: 1 };
|
|
74
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
75
|
+
expect(() => fillSurfaceLinearGradient(region, ramp, 0, 0, 3, 0)).not.toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('fillSurfaceRadialGradient', () => {
|
|
80
|
+
it('center pixel gets the first stop color', () => {
|
|
81
|
+
const surf = createSurface(5, 5);
|
|
82
|
+
const region = createSurfaceRegion(surf);
|
|
83
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
84
|
+
fillSurfaceRadialGradient(region, ramp, 2, 2, 2);
|
|
85
|
+
// Center is at distance 0, so t=0, gets first stop.
|
|
86
|
+
const pixel = getSurfacePixel(surf, 2, 2);
|
|
87
|
+
expect((pixel >>> 24) & 0xff).toBeCloseTo(0xff, -1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('pad spread clamps beyond radius to last stop', () => {
|
|
91
|
+
const surf = createSurface(9, 1);
|
|
92
|
+
const region = createSurfaceRegion(surf);
|
|
93
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
94
|
+
// Center at x=4, radius=2; pixels at x=0 and x=8 are far outside.
|
|
95
|
+
fillSurfaceRadialGradient(region, ramp, 4, 0, 2, 4, 0, 'pad');
|
|
96
|
+
const far = getSurfacePixel(surf, 0, 0);
|
|
97
|
+
// Should be clamped to last stop (blue).
|
|
98
|
+
expect((far >> 8) & 0xff).toBeCloseTo(0xff, -1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('skips pixels outside surface bounds', () => {
|
|
102
|
+
const surf = createSurface(2, 2);
|
|
103
|
+
const region = { surface: surf, x: -1, y: 0, width: 4, height: 1 };
|
|
104
|
+
const ramp = buildTwoStopRamp(0xff0000ff, 0x0000ffff);
|
|
105
|
+
expect(() => fillSurfaceRadialGradient(region, ramp, 1, 0, 2)).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { equalizeSurfaceHistogram, getSurfaceHistogram } from './surfaceHistogram';
|
|
3
|
+
import { setSurfacePixel } from './surfacePixel';
|
|
4
|
+
|
|
5
|
+
function region(
|
|
6
|
+
surface: ReturnType<typeof createSurface>,
|
|
7
|
+
x = 0,
|
|
8
|
+
y = 0,
|
|
9
|
+
width = surface.width,
|
|
10
|
+
height = surface.height,
|
|
11
|
+
) {
|
|
12
|
+
return { surface, x, y, width, height };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('equalizeSurfaceHistogram', () => {
|
|
16
|
+
it('spreads a narrow tonal range to fill 0..255', () => {
|
|
17
|
+
// All red values are either 0 or 128 — a compressed range.
|
|
18
|
+
const source = createSurface(2, 1);
|
|
19
|
+
setSurfacePixel(source, 0, 0, 0x000000ff);
|
|
20
|
+
setSurfacePixel(source, 1, 0, 0x808000ff);
|
|
21
|
+
const dest = createSurface(2, 1);
|
|
22
|
+
equalizeSurfaceHistogram(region(dest), region(source));
|
|
23
|
+
// After equalization the darker pixel maps to 0 and brighter to 255
|
|
24
|
+
expect(dest.data[0]).toBe(0);
|
|
25
|
+
expect(dest.data[4]).toBe(255);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('is safe in-place', () => {
|
|
29
|
+
const surface = createSurface(2, 1);
|
|
30
|
+
setSurfacePixel(surface, 0, 0, 0x000000ff);
|
|
31
|
+
setSurfacePixel(surface, 1, 0, 0xff0000ff);
|
|
32
|
+
expect(() => equalizeSurfaceHistogram(region(surface), region(surface))).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('preserves alpha unchanged', () => {
|
|
36
|
+
const source = createSurface(1, 1, 0x40404080);
|
|
37
|
+
const dest = createSurface(1, 1);
|
|
38
|
+
equalizeSurfaceHistogram(region(dest), region(source));
|
|
39
|
+
expect(dest.data[3]).toBe(0x80);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('getSurfaceHistogram', () => {
|
|
44
|
+
it('counts pixel values per channel', () => {
|
|
45
|
+
const surface = createSurface(2, 1);
|
|
46
|
+
setSurfacePixel(surface, 0, 0, 0x0a000000);
|
|
47
|
+
setSurfacePixel(surface, 1, 0, 0x0a0000ff);
|
|
48
|
+
const histogram = getSurfaceHistogram(region(surface));
|
|
49
|
+
expect(histogram.red[0x0a]).toBe(2);
|
|
50
|
+
expect(histogram.alpha[0]).toBe(1);
|
|
51
|
+
expect(histogram.alpha[255]).toBe(1);
|
|
52
|
+
expect(histogram.green[0]).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns all-zero bins for an empty region', () => {
|
|
56
|
+
const surface = createSurface(2, 2, 0xffffffff);
|
|
57
|
+
const histogram = getSurfaceHistogram(region(surface, 0, 0, 0, 0));
|
|
58
|
+
expect(histogram.red.reduce((a, b) => a + b, 0)).toBe(0);
|
|
59
|
+
expect(histogram.red.length).toBe(256);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('counts only a sub-region', () => {
|
|
63
|
+
const surface = createSurface(2, 1);
|
|
64
|
+
setSurfacePixel(surface, 0, 0, 0x10000000);
|
|
65
|
+
setSurfacePixel(surface, 1, 0, 0x20000000);
|
|
66
|
+
const histogram = getSurfaceHistogram(region(surface, 1, 0, 1, 1));
|
|
67
|
+
expect(histogram.red[0x20]).toBe(1);
|
|
68
|
+
expect(histogram.red[0x10]).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ImageChannel } from './surfaceImageChannel';
|
|
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,45 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { medianSurface } from './surfaceMedian';
|
|
3
|
+
|
|
4
|
+
function region(
|
|
5
|
+
surface: ReturnType<typeof createSurface>,
|
|
6
|
+
x = 0,
|
|
7
|
+
y = 0,
|
|
8
|
+
width = surface.width,
|
|
9
|
+
height = surface.height,
|
|
10
|
+
) {
|
|
11
|
+
return { surface, x, y, width, height };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('medianSurface', () => {
|
|
15
|
+
it('removes an isolated salt pixel without blurring', () => {
|
|
16
|
+
// 3x3 of black with a single white (R=255) center; the median is black.
|
|
17
|
+
const source = createSurface(3, 3);
|
|
18
|
+
for (let i = 0; i < 9; i++) source.data[i * 4 + 3] = 255;
|
|
19
|
+
source.data[4 * 4] = 255;
|
|
20
|
+
const out = new Uint8ClampedArray(3 * 3 * 4);
|
|
21
|
+
medianSurface(out, region(source), 1);
|
|
22
|
+
expect(out[4 * 4]).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('preserves a hard edge (median is not an average)', () => {
|
|
26
|
+
// Left half 0, right half 255 across a 4x1 row; the boundary stays crisp.
|
|
27
|
+
const source = createSurface(4, 1);
|
|
28
|
+
source.data[0] = 0;
|
|
29
|
+
source.data[4] = 0;
|
|
30
|
+
source.data[8] = 255;
|
|
31
|
+
source.data[12] = 255;
|
|
32
|
+
const out = new Uint8ClampedArray(4 * 4);
|
|
33
|
+
medianSurface(out, region(source), 1);
|
|
34
|
+
expect(out[4]).toBe(0);
|
|
35
|
+
expect(out[8]).toBe(255);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('radius 0 copies the source', () => {
|
|
39
|
+
const source = createSurface(1, 1, 0x123456ff);
|
|
40
|
+
const out = new Uint8ClampedArray(4);
|
|
41
|
+
medianSurface(out, region(source), 0);
|
|
42
|
+
expect(out[0]).toBe(0x12);
|
|
43
|
+
expect(out[3]).toBe(0xff);
|
|
44
|
+
});
|
|
45
|
+
});
|