@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,113 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { copySurfaceAlpha, multiplySurfaceAlpha, setSurfaceAlpha } from './surfaceAlpha';
|
|
3
|
+
import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
|
|
4
|
+
import { createSurfaceRegion } from './surfaceRegion';
|
|
5
|
+
|
|
6
|
+
describe('copySurfaceAlpha', () => {
|
|
7
|
+
it('copies alpha from source to dest, leaving RGB unchanged', () => {
|
|
8
|
+
const src = createSurface(2, 1);
|
|
9
|
+
setSurfacePixel(src, 0, 0, 0x00000080); // alpha 0x80
|
|
10
|
+
setSurfacePixel(src, 1, 0, 0x000000ff); // alpha 0xff
|
|
11
|
+
const dst = createSurface(2, 1, 0xff00ffff); // red pixels, fully opaque
|
|
12
|
+
copySurfaceAlpha(createSurfaceRegion(dst), createSurfaceRegion(src));
|
|
13
|
+
// RGB unchanged, alpha from source.
|
|
14
|
+
expect((getSurfacePixel(dst, 0, 0) >>> 24) & 0xff).toBe(0xff); // red channel
|
|
15
|
+
expect(getSurfacePixel(dst, 0, 0) & 0xff).toBe(0x80); // alpha
|
|
16
|
+
expect(getSurfacePixel(dst, 1, 0) & 0xff).toBe(0xff);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('same-surface region is a no-op on alpha', () => {
|
|
20
|
+
const surf = createSurface(2, 1, 0x112233aa);
|
|
21
|
+
copySurfaceAlpha(createSurfaceRegion(surf), createSurfaceRegion(surf));
|
|
22
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xaa);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('operates only on the overlap of both regions', () => {
|
|
26
|
+
const src = createSurface(3, 1, 0x00000080);
|
|
27
|
+
const dst = createSurface(3, 1, 0x00000000);
|
|
28
|
+
// Copy only 1 pixel wide.
|
|
29
|
+
copySurfaceAlpha(
|
|
30
|
+
{ surface: dst, x: 0, y: 0, width: 1, height: 1 },
|
|
31
|
+
{ surface: src, x: 0, y: 0, width: 1, height: 1 },
|
|
32
|
+
);
|
|
33
|
+
expect(getSurfacePixel(dst, 0, 0) & 0xff).toBe(0x80);
|
|
34
|
+
expect(getSurfacePixel(dst, 1, 0) & 0xff).toBe(0x00); // untouched
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('multiplySurfaceAlpha', () => {
|
|
39
|
+
it('factor 0 makes region fully transparent', () => {
|
|
40
|
+
const surf = createSurface(2, 2, 0xff0000ff);
|
|
41
|
+
multiplySurfaceAlpha(createSurfaceRegion(surf), 0);
|
|
42
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0);
|
|
43
|
+
expect(getSurfacePixel(surf, 1, 1) & 0xff).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('factor 1 leaves alpha unchanged', () => {
|
|
47
|
+
const surf = createSurface(1, 1, 0xff0000ab);
|
|
48
|
+
multiplySurfaceAlpha(createSurfaceRegion(surf), 1);
|
|
49
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xab);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('factor 0.5 halves the alpha', () => {
|
|
53
|
+
const surf = createSurface(1, 1, 0xff0000fe);
|
|
54
|
+
multiplySurfaceAlpha(createSurfaceRegion(surf), 0.5);
|
|
55
|
+
// 0xfe * 0.5 = 127
|
|
56
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(127);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('clamps factor to [0, 1]', () => {
|
|
60
|
+
const surf = createSurface(1, 1, 0xff0000aa);
|
|
61
|
+
multiplySurfaceAlpha(createSurfaceRegion(surf), 2);
|
|
62
|
+
// Factor clamped to 1, alpha unchanged.
|
|
63
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xaa);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('leaves RGB channels unchanged', () => {
|
|
67
|
+
const surf = createSurface(1, 1, 0x123456ff);
|
|
68
|
+
multiplySurfaceAlpha(createSurfaceRegion(surf), 0.5);
|
|
69
|
+
const p = getSurfacePixel(surf, 0, 0);
|
|
70
|
+
expect((p >>> 24) & 0xff).toBe(0x12);
|
|
71
|
+
expect((p >> 16) & 0xff).toBe(0x34);
|
|
72
|
+
expect((p >> 8) & 0xff).toBe(0x56);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('setSurfaceAlpha', () => {
|
|
77
|
+
it('writes a constant alpha to all pixels in region', () => {
|
|
78
|
+
const surf = createSurface(3, 3, 0xff0000ff);
|
|
79
|
+
setSurfaceAlpha(createSurfaceRegion(surf), 0x40);
|
|
80
|
+
for (let y = 0; y < 3; y++) {
|
|
81
|
+
for (let x = 0; x < 3; x++) {
|
|
82
|
+
expect(getSurfacePixel(surf, x, y) & 0xff).toBe(0x40);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('leaves RGB unchanged', () => {
|
|
88
|
+
const surf = createSurface(1, 1, 0x112233ff);
|
|
89
|
+
setSurfaceAlpha(createSurfaceRegion(surf), 0x80);
|
|
90
|
+
const p = getSurfacePixel(surf, 0, 0);
|
|
91
|
+
expect((p >>> 24) & 0xff).toBe(0x11);
|
|
92
|
+
expect((p >> 16) & 0xff).toBe(0x22);
|
|
93
|
+
expect((p >> 8) & 0xff).toBe(0x33);
|
|
94
|
+
expect(p & 0xff).toBe(0x80);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('clamps alpha to [0, 255]', () => {
|
|
98
|
+
const surf = createSurface(1, 1, 0xff0000ff);
|
|
99
|
+
setSurfaceAlpha(createSurfaceRegion(surf), -10);
|
|
100
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0);
|
|
101
|
+
setSurfaceAlpha(createSurfaceRegion(surf), 300);
|
|
102
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(255);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('only affects the specified sub-region', () => {
|
|
106
|
+
const surf = createSurface(4, 1, 0xff0000ff);
|
|
107
|
+
setSurfaceAlpha({ surface: surf, x: 1, y: 0, width: 2, height: 1 }, 0x00);
|
|
108
|
+
expect(getSurfacePixel(surf, 0, 0) & 0xff).toBe(0xff); // untouched
|
|
109
|
+
expect(getSurfacePixel(surf, 1, 0) & 0xff).toBe(0x00);
|
|
110
|
+
expect(getSurfacePixel(surf, 2, 0) & 0xff).toBe(0x00);
|
|
111
|
+
expect(getSurfacePixel(surf, 3, 0) & 0xff).toBe(0xff); // untouched
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { bevelSurface } from './surfaceBevel';
|
|
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
|
+
// 5x1 strip with a single alpha edge: transparent | transparent | opaque | opaque | opaque.
|
|
15
|
+
function edgeStrip() {
|
|
16
|
+
const s = createSurface(5, 1);
|
|
17
|
+
for (let x = 2; x < 5; x++) s.data[x * 4 + 3] = 255;
|
|
18
|
+
return s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('bevelSurface', () => {
|
|
22
|
+
it('highlights the light-facing edge and shades the opposite edge', () => {
|
|
23
|
+
// Light from the left (angle π). With radiusX=1 the blurred alpha is
|
|
24
|
+
// [0, 85, 170, 255, 255]; the gradient highlights the left edge of the
|
|
25
|
+
// shape and shadows the right edge.
|
|
26
|
+
const source = edgeStrip();
|
|
27
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
28
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
29
|
+
bevelSurface(out, scratch, region(source), {
|
|
30
|
+
angle: Math.PI,
|
|
31
|
+
distance: 1,
|
|
32
|
+
radiusX: 1,
|
|
33
|
+
radiusY: 0,
|
|
34
|
+
type: 'both',
|
|
35
|
+
highlightColor: 0xffffffff,
|
|
36
|
+
shadowColor: 0x000000ff,
|
|
37
|
+
});
|
|
38
|
+
// Left edge: white highlight.
|
|
39
|
+
expect(out[2 * 4 + 0]).toBe(255);
|
|
40
|
+
expect(out[2 * 4 + 1]).toBe(255);
|
|
41
|
+
expect(out[2 * 4 + 2]).toBe(255);
|
|
42
|
+
expect(out[2 * 4 + 3]).toBe(170);
|
|
43
|
+
// Right edge of the strip faces away from the light: black shadow.
|
|
44
|
+
expect(out[4 * 4 + 0]).toBe(0);
|
|
45
|
+
expect(out[4 * 4 + 1]).toBe(0);
|
|
46
|
+
expect(out[4 * 4 + 2]).toBe(0);
|
|
47
|
+
expect(out[4 * 4 + 3]).toBe(255);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("'inner' type clips the bevel to inside the shape", () => {
|
|
51
|
+
const source = edgeStrip();
|
|
52
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
53
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
54
|
+
bevelSurface(out, scratch, region(source), {
|
|
55
|
+
angle: Math.PI,
|
|
56
|
+
distance: 1,
|
|
57
|
+
radiusX: 1,
|
|
58
|
+
radiusY: 0,
|
|
59
|
+
type: 'inner',
|
|
60
|
+
});
|
|
61
|
+
// x0/x1 are outside the shape (source alpha 0) → clipped to 0.
|
|
62
|
+
expect(out[0 * 4 + 3]).toBe(0);
|
|
63
|
+
expect(out[1 * 4 + 3]).toBe(0);
|
|
64
|
+
// x2 is inside the shape → highlight survives.
|
|
65
|
+
expect(out[2 * 4 + 3]).toBe(170);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import {
|
|
3
|
+
blurSurfacePixelsHorizontal,
|
|
4
|
+
blurSurfacePixelsHorizontalWeighted,
|
|
5
|
+
blurSurfacePixelsVertical,
|
|
6
|
+
blurSurfacePixelsVerticalWeighted,
|
|
7
|
+
boxBlurSurface,
|
|
8
|
+
computeGaussianKernel,
|
|
9
|
+
gaussianBlurSurface,
|
|
10
|
+
} from './surfaceBlur';
|
|
11
|
+
import { premultiplySurfacePixels, unpremultiplySurfacePixels } from './surfaceFormat';
|
|
12
|
+
|
|
13
|
+
function region(
|
|
14
|
+
surface: ReturnType<typeof createSurface>,
|
|
15
|
+
x = 0,
|
|
16
|
+
y = 0,
|
|
17
|
+
width = surface.width,
|
|
18
|
+
height = surface.height,
|
|
19
|
+
) {
|
|
20
|
+
return { surface, x, y, width, height };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('blurSurfacePixelsHorizontal', () => {
|
|
24
|
+
it('spreads alpha into horizontal neighbors', () => {
|
|
25
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
26
|
+
const out = new Uint8ClampedArray(12);
|
|
27
|
+
blurSurfacePixelsHorizontal(out, source, 3, 1, 1);
|
|
28
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
29
|
+
expect(out[11]).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('copies source to out without spreading when radius is 0', () => {
|
|
33
|
+
const source = new Uint8ClampedArray([0, 0, 0, 255, 0, 0, 0, 0]);
|
|
34
|
+
const out = new Uint8ClampedArray(8);
|
|
35
|
+
blurSurfacePixelsHorizontal(out, source, 2, 1, 0);
|
|
36
|
+
expect(out[3]).toBe(255);
|
|
37
|
+
expect(out[7]).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('premultiplying before blur avoids the dark-halo artifact at transparent edges', () => {
|
|
41
|
+
// [opaque red, transparent]. Straight-alpha blur bleeds the transparent
|
|
42
|
+
// pixel's hidden black into the red; a premultiplied blur keeps it pure red.
|
|
43
|
+
const buf = new Uint8ClampedArray([255, 0, 0, 255, 0, 0, 0, 0]);
|
|
44
|
+
|
|
45
|
+
const straight = new Uint8ClampedArray(8);
|
|
46
|
+
blurSurfacePixelsHorizontal(straight, buf, 2, 1, 1);
|
|
47
|
+
expect(straight[4]).toBe(128); // transparent pixel's red darkened toward black
|
|
48
|
+
|
|
49
|
+
const premul = new Uint8ClampedArray(8);
|
|
50
|
+
premultiplySurfacePixels(premul, buf, 8);
|
|
51
|
+
const blurred = new Uint8ClampedArray(8);
|
|
52
|
+
blurSurfacePixelsHorizontal(blurred, premul, 2, 1, 1);
|
|
53
|
+
const result = new Uint8ClampedArray(8);
|
|
54
|
+
unpremultiplySurfacePixels(result, blurred, 8);
|
|
55
|
+
expect(result[4]).toBe(255); // red stays pure
|
|
56
|
+
expect(result[7]).toBe(128); // alpha still half
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('produces correct edge average for a single row', () => {
|
|
60
|
+
// pixels: [0, 255, 0] alpha only; radius=1
|
|
61
|
+
// x=0: avg([0,255])/2 = 128 (approx); x=1: avg([0,255,0])/3 = 85; x=2: avg([255,0])/2 = 128
|
|
62
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
63
|
+
const out = new Uint8ClampedArray(12);
|
|
64
|
+
blurSurfacePixelsHorizontal(out, source, 3, 1, 1);
|
|
65
|
+
expect(out[3]).toBe(128); // (0+255)/2
|
|
66
|
+
expect(out[7]).toBe(85); // (0+255+0)/3
|
|
67
|
+
expect(out[11]).toBe(128); // (255+0)/2
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('blurSurfacePixelsHorizontalWeighted', () => {
|
|
72
|
+
it('applies Gaussian weights to horizontal neighbors', () => {
|
|
73
|
+
const kernel = new Float32Array(3);
|
|
74
|
+
computeGaussianKernel(kernel, 1, 0.8);
|
|
75
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
76
|
+
const out = new Uint8ClampedArray(12);
|
|
77
|
+
blurSurfacePixelsHorizontalWeighted(out, source, 3, 1, kernel);
|
|
78
|
+
// center weight >> edge; center alpha should be highest
|
|
79
|
+
expect(out[7]).toBeGreaterThan(out[3]);
|
|
80
|
+
expect(out[7]).toBeGreaterThan(out[11]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('with an identity kernel [1] leaves source unchanged', () => {
|
|
84
|
+
const kernel = new Float32Array([1]);
|
|
85
|
+
const source = new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]);
|
|
86
|
+
const out = new Uint8ClampedArray(4);
|
|
87
|
+
blurSurfacePixelsHorizontalWeighted(out, source, 1, 1, kernel);
|
|
88
|
+
expect(out[0]).toBe(0x11);
|
|
89
|
+
expect(out[3]).toBe(0xff);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('blurSurfacePixelsVertical', () => {
|
|
94
|
+
it('spreads alpha into vertical neighbors', () => {
|
|
95
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
96
|
+
const out = new Uint8ClampedArray(12);
|
|
97
|
+
blurSurfacePixelsVertical(out, source, 1, 3, 1);
|
|
98
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
99
|
+
expect(out[11]).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('blurSurfacePixelsVerticalWeighted', () => {
|
|
104
|
+
it('applies Gaussian weights to vertical neighbors', () => {
|
|
105
|
+
const kernel = new Float32Array(3);
|
|
106
|
+
computeGaussianKernel(kernel, 1, 0.8);
|
|
107
|
+
const source = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0]);
|
|
108
|
+
const out = new Uint8ClampedArray(12);
|
|
109
|
+
blurSurfacePixelsVerticalWeighted(out, source, 1, 3, kernel);
|
|
110
|
+
expect(out[7]).toBeGreaterThan(out[3]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('boxBlurSurface', () => {
|
|
115
|
+
it('spreads alpha into neighboring pixels', () => {
|
|
116
|
+
const source = createSurface(3, 1);
|
|
117
|
+
source.data[7] = 255; // center pixel opaque
|
|
118
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
119
|
+
const scratch = new Uint8ClampedArray(3 * 4);
|
|
120
|
+
boxBlurSurface(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
|
|
121
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
122
|
+
expect(out[11]).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('writes source into out when blur is zero', () => {
|
|
126
|
+
const source = createSurface(1, 1, 0x336699ff);
|
|
127
|
+
const out = new Uint8ClampedArray(4);
|
|
128
|
+
const scratch = new Uint8ClampedArray(4);
|
|
129
|
+
boxBlurSurface(out, scratch, region(source), { radiusX: 0, radiusY: 0 });
|
|
130
|
+
expect(out[0]).toBe(0x33);
|
|
131
|
+
expect(out[3]).toBe(0xff);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('result always lands in out regardless of pass parity', () => {
|
|
135
|
+
const source = createSurface(3, 3, 0xffffff88);
|
|
136
|
+
const out = new Uint8ClampedArray(3 * 3 * 4);
|
|
137
|
+
const scratch = new Uint8ClampedArray(3 * 3 * 4);
|
|
138
|
+
boxBlurSurface(out, scratch, region(source), { radiusX: 2, radiusY: 0 });
|
|
139
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('source.surface.data can be used as out for a full-surface region', () => {
|
|
143
|
+
const surface = createSurface(3, 1);
|
|
144
|
+
surface.data[7] = 255;
|
|
145
|
+
const scratch = new Uint8ClampedArray(3 * 4);
|
|
146
|
+
boxBlurSurface(surface.data, scratch, region(surface), { radiusX: 2, radiusY: 0 });
|
|
147
|
+
expect(surface.data[3]).toBeGreaterThan(0);
|
|
148
|
+
expect(surface.data[11]).toBeGreaterThan(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('blurs only the offset sub-region, reading from the right source pixels', () => {
|
|
152
|
+
// 4 px alpha: [_, 255, 100, _]. Blurring region (1,0,2,1) extracts [255,100]
|
|
153
|
+
// and averages them — proving the source x-offset is honored, not ignored.
|
|
154
|
+
const surface = createSurface(4, 1);
|
|
155
|
+
surface.data[1 * 4 + 3] = 255;
|
|
156
|
+
surface.data[2 * 4 + 3] = 100;
|
|
157
|
+
const out = new Uint8ClampedArray(2 * 4);
|
|
158
|
+
const scratch = new Uint8ClampedArray(2 * 4);
|
|
159
|
+
boxBlurSurface(out, scratch, region(surface, 1, 0, 2, 1), { radiusX: 2, radiusY: 0 });
|
|
160
|
+
expect(out[3]).toBe(178); // round((255 + 100) / 2)
|
|
161
|
+
expect(out[7]).toBe(178);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('computeGaussianKernel', () => {
|
|
166
|
+
it('produces a kernel that sums to 1', () => {
|
|
167
|
+
const kernel = new Float32Array(7);
|
|
168
|
+
computeGaussianKernel(kernel, 3, 1.0);
|
|
169
|
+
const sum = kernel.reduce((acc, v) => acc + v, 0);
|
|
170
|
+
expect(sum).toBeCloseTo(1, 5);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('is symmetric', () => {
|
|
174
|
+
const kernel = new Float32Array(9);
|
|
175
|
+
computeGaussianKernel(kernel, 4, 1.5);
|
|
176
|
+
for (let i = 0; i < 4; i++) {
|
|
177
|
+
expect(kernel[i]).toBeCloseTo(kernel[8 - i], 10);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('center weight is the largest', () => {
|
|
182
|
+
const kernel = new Float32Array(5);
|
|
183
|
+
computeGaussianKernel(kernel, 2, 1.0);
|
|
184
|
+
expect(kernel[2]).toBeGreaterThan(kernel[1]);
|
|
185
|
+
expect(kernel[1]).toBeGreaterThan(kernel[0]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('radius-0 kernel produces a single weight of 1', () => {
|
|
189
|
+
const kernel = new Float32Array(1);
|
|
190
|
+
computeGaussianKernel(kernel, 0, 1.0);
|
|
191
|
+
expect(kernel[0]).toBeCloseTo(1, 10);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('sigma <= 0 produces a unit impulse rather than a NaN kernel', () => {
|
|
195
|
+
const kernel = new Float32Array(5).fill(9);
|
|
196
|
+
computeGaussianKernel(kernel, 2, 0);
|
|
197
|
+
expect(kernel[2]).toBe(1);
|
|
198
|
+
expect(kernel[0]).toBe(0);
|
|
199
|
+
expect(kernel[4]).toBe(0);
|
|
200
|
+
expect(kernel.every((v) => !Number.isNaN(v))).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('gaussianBlurSurface', () => {
|
|
205
|
+
it('spreads alpha into neighboring pixels', () => {
|
|
206
|
+
const source = createSurface(5, 1);
|
|
207
|
+
source.data[2 * 4 + 3] = 255; // center pixel opaque
|
|
208
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
209
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
210
|
+
gaussianBlurSurface(out, scratch, region(source), 1.0);
|
|
211
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
212
|
+
expect(out[4 * 4 + 3]).toBeGreaterThan(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('center pixel retains more weight than edges', () => {
|
|
216
|
+
const source = createSurface(5, 1);
|
|
217
|
+
source.data[2 * 4 + 3] = 255;
|
|
218
|
+
const out = new Uint8ClampedArray(5 * 4);
|
|
219
|
+
const scratch = new Uint8ClampedArray(5 * 4);
|
|
220
|
+
gaussianBlurSurface(out, scratch, region(source), 0.8);
|
|
221
|
+
expect(out[2 * 4 + 3]).toBeGreaterThan(out[3]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('with sigma=0 writes source unchanged', () => {
|
|
225
|
+
const source = createSurface(1, 1, 0x336699ff);
|
|
226
|
+
const out = new Uint8ClampedArray(4);
|
|
227
|
+
const scratch = new Uint8ClampedArray(4);
|
|
228
|
+
gaussianBlurSurface(out, scratch, region(source), 0);
|
|
229
|
+
expect(out[0]).toBe(0x33);
|
|
230
|
+
expect(out[3]).toBe(0xff);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('result always lands in out regardless of pass parity', () => {
|
|
234
|
+
const source = createSurface(3, 3, 0xffffff88);
|
|
235
|
+
const out = new Uint8ClampedArray(3 * 3 * 4);
|
|
236
|
+
const scratch = new Uint8ClampedArray(3 * 3 * 4);
|
|
237
|
+
gaussianBlurSurface(out, scratch, region(source), 1.0);
|
|
238
|
+
expect(out[3]).toBeGreaterThan(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { mergeSurfaceChannels, splitSurfaceChannels } from './surfaceChannel';
|
|
3
|
+
import { getSurfacePixel, setSurfacePixel } from './surfacePixel';
|
|
4
|
+
import { createSurfaceRegion } from './surfaceRegion';
|
|
5
|
+
|
|
6
|
+
describe('mergeSurfaceChannels', () => {
|
|
7
|
+
it('combines one channel from each input into the output', () => {
|
|
8
|
+
const rSurf = createSurface(1, 1, 0xff000000); // R=0xff, G=0, B=0, A=0
|
|
9
|
+
const gSurf = createSurface(1, 1, 0x00ff0000); // R=0, G=0xff, B=0, A=0
|
|
10
|
+
const bSurf = createSurface(1, 1, 0x0000ff00); // R=0, G=0, B=0xff, A=0
|
|
11
|
+
const aSurf = createSurface(1, 1, 0x000000ff); // R=0, G=0, B=0, A=0xff
|
|
12
|
+
const out = createSurface(1, 1);
|
|
13
|
+
mergeSurfaceChannels(
|
|
14
|
+
createSurfaceRegion(out),
|
|
15
|
+
createSurfaceRegion(rSurf),
|
|
16
|
+
createSurfaceRegion(gSurf),
|
|
17
|
+
createSurfaceRegion(bSurf),
|
|
18
|
+
createSurfaceRegion(aSurf),
|
|
19
|
+
);
|
|
20
|
+
expect(getSurfacePixel(out, 0, 0)).toBe(0xff_ff_ff_ff);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('uses the minimum dimension overlap', () => {
|
|
24
|
+
const r = createSurface(3, 1, 0xff000000);
|
|
25
|
+
const g = createSurface(3, 1, 0x00ff0000);
|
|
26
|
+
const b = createSurface(3, 1, 0x0000ff00);
|
|
27
|
+
const a = createSurface(3, 1, 0x000000ff);
|
|
28
|
+
const out = createSurface(3, 1);
|
|
29
|
+
// Restrict r region to width=1 — only pixel 0 should be written.
|
|
30
|
+
mergeSurfaceChannels(
|
|
31
|
+
createSurfaceRegion(out),
|
|
32
|
+
{ surface: r, x: 0, y: 0, width: 1, height: 1 },
|
|
33
|
+
createSurfaceRegion(g),
|
|
34
|
+
createSurfaceRegion(b),
|
|
35
|
+
createSurfaceRegion(a),
|
|
36
|
+
);
|
|
37
|
+
// Pixel 0 is covered; pixel 1 should be untouched (0x00000000).
|
|
38
|
+
expect((getSurfacePixel(out, 0, 0) >>> 24) & 0xff).toBe(0xff);
|
|
39
|
+
expect(getSurfacePixel(out, 1, 0)).toBe(0x00000000);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('splitSurfaceChannels', () => {
|
|
44
|
+
it('returns four surfaces of the same dimensions', () => {
|
|
45
|
+
const src = createSurface(3, 2, 0x112233ff);
|
|
46
|
+
const [r, g, b, a] = splitSurfaceChannels(src);
|
|
47
|
+
expect(r.width).toBe(3);
|
|
48
|
+
expect(r.height).toBe(2);
|
|
49
|
+
expect(g.width).toBe(3);
|
|
50
|
+
expect(b.height).toBe(2);
|
|
51
|
+
expect(a.width).toBe(3);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('each channel surface is a grayscale copy of that channel', () => {
|
|
55
|
+
const src = createSurface(1, 1);
|
|
56
|
+
setSurfacePixel(src, 0, 0, 0x11_22_33_44);
|
|
57
|
+
const [r, g, b, a] = splitSurfaceChannels(src);
|
|
58
|
+
// R surface: R=G=B=0x11, A=0xff.
|
|
59
|
+
expect((getSurfacePixel(r, 0, 0) >>> 24) & 0xff).toBe(0x11);
|
|
60
|
+
expect((getSurfacePixel(r, 0, 0) >> 16) & 0xff).toBe(0x11);
|
|
61
|
+
expect((getSurfacePixel(r, 0, 0) >> 8) & 0xff).toBe(0x11);
|
|
62
|
+
expect(getSurfacePixel(r, 0, 0) & 0xff).toBe(0xff);
|
|
63
|
+
// G surface: R=G=B=0x22, A=0xff.
|
|
64
|
+
expect((getSurfacePixel(g, 0, 0) >>> 24) & 0xff).toBe(0x22);
|
|
65
|
+
// B surface: R=G=B=0x33, A=0xff.
|
|
66
|
+
expect((getSurfacePixel(b, 0, 0) >>> 24) & 0xff).toBe(0x33);
|
|
67
|
+
// A surface: R=G=B=A=0x44 (alpha stored in all channels for round-trip fidelity).
|
|
68
|
+
expect((getSurfacePixel(a, 0, 0) >>> 24) & 0xff).toBe(0x44);
|
|
69
|
+
expect(getSurfacePixel(a, 0, 0) & 0xff).toBe(0x44);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns distinct surface objects', () => {
|
|
73
|
+
const src = createSurface(2, 2, 0xffffffff);
|
|
74
|
+
const [r, g, b, a] = splitSurfaceChannels(src);
|
|
75
|
+
expect(r).not.toBe(src);
|
|
76
|
+
expect(r).not.toBe(g);
|
|
77
|
+
expect(g).not.toBe(b);
|
|
78
|
+
expect(b).not.toBe(a);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('round-trip: split then merge reconstructs the original', () => {
|
|
82
|
+
const src = createSurface(2, 2);
|
|
83
|
+
setSurfacePixel(src, 0, 0, 0xaabbccdd);
|
|
84
|
+
setSurfacePixel(src, 1, 0, 0x11223344);
|
|
85
|
+
const [r, g, b, a] = splitSurfaceChannels(src);
|
|
86
|
+
const out = createSurface(2, 2);
|
|
87
|
+
mergeSurfaceChannels(
|
|
88
|
+
createSurfaceRegion(out),
|
|
89
|
+
createSurfaceRegion(r),
|
|
90
|
+
createSurfaceRegion(g),
|
|
91
|
+
createSurfaceRegion(b),
|
|
92
|
+
createSurfaceRegion(a),
|
|
93
|
+
);
|
|
94
|
+
expect(getSurfacePixel(out, 0, 0)).toBe(0xaabbccdd);
|
|
95
|
+
expect(getSurfacePixel(out, 1, 0)).toBe(0x11223344);
|
|
96
|
+
});
|
|
97
|
+
});
|