@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,220 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import {
|
|
3
|
+
buildSurfaceBrightnessColorMatrix,
|
|
4
|
+
buildSurfaceContrastColorMatrix,
|
|
5
|
+
buildSurfaceGrayscaleColorMatrix,
|
|
6
|
+
buildSurfaceHueRotationColorMatrix,
|
|
7
|
+
buildSurfaceInvertColorMatrix,
|
|
8
|
+
buildSurfaceSaturationColorMatrix,
|
|
9
|
+
buildSurfaceSepiaColorMatrix,
|
|
10
|
+
colorMatrixSurface,
|
|
11
|
+
concatSurfaceColorMatrix,
|
|
12
|
+
setSurfaceColorMatrixIdentity,
|
|
13
|
+
} from './surfaceColorMatrix';
|
|
14
|
+
|
|
15
|
+
function region(
|
|
16
|
+
surface: ReturnType<typeof createSurface>,
|
|
17
|
+
x = 0,
|
|
18
|
+
y = 0,
|
|
19
|
+
width = surface.width,
|
|
20
|
+
height = surface.height,
|
|
21
|
+
) {
|
|
22
|
+
return { surface, x, y, width, height };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build a 1x1 surface, apply `matrix`, and return its RGBA bytes.
|
|
26
|
+
function applyTo(rgba: number, matrix: ReadonlyArray<number>): Uint8ClampedArray {
|
|
27
|
+
const surface = createSurface(1, 1, rgba);
|
|
28
|
+
const out = new Uint8ClampedArray(4);
|
|
29
|
+
colorMatrixSurface(out, region(surface), matrix);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('buildSurfaceBrightnessColorMatrix', () => {
|
|
34
|
+
it('multiplies RGB by the amount', () => {
|
|
35
|
+
const m = new Array<number>(20);
|
|
36
|
+
buildSurfaceBrightnessColorMatrix(m, 0.5);
|
|
37
|
+
const out = applyTo(0xc86432ff, m); // R=200 G=100 B=50
|
|
38
|
+
expect(out[0]).toBe(100);
|
|
39
|
+
expect(out[1]).toBe(50);
|
|
40
|
+
expect(out[2]).toBe(25);
|
|
41
|
+
expect(out[3]).toBe(0xff);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('buildSurfaceContrastColorMatrix', () => {
|
|
46
|
+
it('amount 0 flattens every channel to mid grey', () => {
|
|
47
|
+
const m = new Array<number>(20);
|
|
48
|
+
buildSurfaceContrastColorMatrix(m, 0);
|
|
49
|
+
const out = applyTo(0xc80000ff, m); // R=200
|
|
50
|
+
expect(out[0]).toBe(128); // round(127.5)
|
|
51
|
+
expect(out[1]).toBe(128);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('buildSurfaceGrayscaleColorMatrix', () => {
|
|
56
|
+
it('maps pure red to its luma in every channel', () => {
|
|
57
|
+
const m = new Array<number>(20);
|
|
58
|
+
buildSurfaceGrayscaleColorMatrix(m);
|
|
59
|
+
const out = applyTo(0xff0000ff, m);
|
|
60
|
+
expect(out[0]).toBe(54); // round(0.213 * 255)
|
|
61
|
+
expect(out[1]).toBe(54);
|
|
62
|
+
expect(out[2]).toBe(54);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('buildSurfaceHueRotationColorMatrix', () => {
|
|
67
|
+
it('leaves grey unchanged at any angle (rotation around the luma axis)', () => {
|
|
68
|
+
const m = new Array<number>(20);
|
|
69
|
+
// 70.7° ≈ 1.234 radians — same geometric rotation, now expressed in degrees
|
|
70
|
+
buildSurfaceHueRotationColorMatrix(m, 70.7);
|
|
71
|
+
const out = applyTo(0x808080ff, m);
|
|
72
|
+
expect(out[0]).toBe(128);
|
|
73
|
+
expect(out[1]).toBe(128);
|
|
74
|
+
expect(out[2]).toBe(128);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('is identity at 0 degrees', () => {
|
|
78
|
+
const m = new Array<number>(20);
|
|
79
|
+
buildSurfaceHueRotationColorMatrix(m, 0);
|
|
80
|
+
const out = applyTo(0xc86432ff, m);
|
|
81
|
+
expect(out[0]).toBe(200);
|
|
82
|
+
expect(out[1]).toBe(100);
|
|
83
|
+
expect(out[2]).toBe(50);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('is identity at 360 degrees', () => {
|
|
87
|
+
const m = new Array<number>(20);
|
|
88
|
+
buildSurfaceHueRotationColorMatrix(m, 360);
|
|
89
|
+
const out = applyTo(0xc86432ff, m);
|
|
90
|
+
expect(out[0]).toBeCloseTo(200, 0);
|
|
91
|
+
expect(out[1]).toBeCloseTo(100, 0);
|
|
92
|
+
expect(out[2]).toBeCloseTo(50, 0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('buildSurfaceInvertColorMatrix', () => {
|
|
97
|
+
it('inverts RGB and preserves alpha', () => {
|
|
98
|
+
const m = new Array<number>(20);
|
|
99
|
+
buildSurfaceInvertColorMatrix(m);
|
|
100
|
+
const out = applyTo(0xc83200ff, m); // R=200 G=50 B=0
|
|
101
|
+
expect(out[0]).toBe(55);
|
|
102
|
+
expect(out[1]).toBe(205);
|
|
103
|
+
expect(out[2]).toBe(255);
|
|
104
|
+
expect(out[3]).toBe(0xff);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('buildSurfaceSaturationColorMatrix', () => {
|
|
109
|
+
it('amount 1 is identity', () => {
|
|
110
|
+
const m = new Array<number>(20);
|
|
111
|
+
buildSurfaceSaturationColorMatrix(m, 1);
|
|
112
|
+
const out = applyTo(0xc86432ff, m);
|
|
113
|
+
expect(out[0]).toBe(200);
|
|
114
|
+
expect(out[1]).toBe(100);
|
|
115
|
+
expect(out[2]).toBe(50);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('amount 0 equals grayscale', () => {
|
|
119
|
+
const m = new Array<number>(20);
|
|
120
|
+
buildSurfaceSaturationColorMatrix(m, 0);
|
|
121
|
+
const out = applyTo(0xff0000ff, m);
|
|
122
|
+
expect(out[0]).toBe(54);
|
|
123
|
+
expect(out[1]).toBe(54);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('buildSurfaceSepiaColorMatrix', () => {
|
|
128
|
+
it('maps pure red to the sepia tone', () => {
|
|
129
|
+
const m = new Array<number>(20);
|
|
130
|
+
buildSurfaceSepiaColorMatrix(m);
|
|
131
|
+
const out = applyTo(0xff0000ff, m);
|
|
132
|
+
expect(out[0]).toBe(100); // round(0.393 * 255)
|
|
133
|
+
expect(out[1]).toBe(89); // round(0.349 * 255)
|
|
134
|
+
expect(out[2]).toBe(69); // round(0.272 * 255)
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('colorMatrixSurface', () => {
|
|
139
|
+
it('applies a 4x5 color matrix to the source region', () => {
|
|
140
|
+
const source = createSurface(1, 1, 0x204060ff);
|
|
141
|
+
const out = new Uint8ClampedArray(4);
|
|
142
|
+
colorMatrixSurface(out, region(source), [0, 0, 0, 0, 10, 0, 0, 0, 0, 20, 0, 0, 0, 0, 30, 0, 0, 0, 1, 0]);
|
|
143
|
+
expect(out[0]).toBe(10);
|
|
144
|
+
expect(out[1]).toBe(20);
|
|
145
|
+
expect(out[2]).toBe(30);
|
|
146
|
+
expect(out[3]).toBe(0xff);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('is safe when out aliases source.surface.data for a full-surface region', () => {
|
|
150
|
+
const surface = createSurface(1, 1, 0x010203ff);
|
|
151
|
+
colorMatrixSurface(surface.data, region(surface), [1, 0, 0, 0, 10, 0, 1, 0, 0, 20, 0, 0, 1, 0, 30, 0, 0, 0, 1, 0]);
|
|
152
|
+
expect(surface.data[0]).toBe(11);
|
|
153
|
+
expect(surface.data[1]).toBe(22);
|
|
154
|
+
expect(surface.data[2]).toBe(33);
|
|
155
|
+
expect(surface.data[3]).toBe(0xff);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('clamps output to [0, 255]', () => {
|
|
159
|
+
const source = createSurface(1, 1, 0xff0000ff);
|
|
160
|
+
const out = new Uint8ClampedArray(4);
|
|
161
|
+
colorMatrixSurface(out, region(source), [10, 0, 0, 0, 100, 0, 0, 0, 0, -50, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]);
|
|
162
|
+
expect(out[0]).toBe(255);
|
|
163
|
+
expect(out[1]).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('throws when the matrix is too short', () => {
|
|
167
|
+
const source = createSurface(1, 1);
|
|
168
|
+
const out = new Uint8ClampedArray(4);
|
|
169
|
+
expect(() => colorMatrixSurface(out, region(source), [])).toThrow('Color matrix filter requires 20 values');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('silently skips pixels outside source bounds', () => {
|
|
173
|
+
const source = createSurface(1, 1, 0xff0000ff);
|
|
174
|
+
const out = new Uint8ClampedArray(4 * 4);
|
|
175
|
+
const identity = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0];
|
|
176
|
+
colorMatrixSurface(out, region(source, -1, -1, 2, 2), identity);
|
|
177
|
+
// only bottom-right pixel of the 2x2 region is in-bounds
|
|
178
|
+
const i = (1 * 2 + 1) * 4;
|
|
179
|
+
expect(out[i]).toBe(0xff);
|
|
180
|
+
expect(out[0]).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('concatSurfaceColorMatrix', () => {
|
|
185
|
+
it('composes two matrices (apply first, then second)', () => {
|
|
186
|
+
const bright2 = new Array<number>(20);
|
|
187
|
+
const half = new Array<number>(20);
|
|
188
|
+
buildSurfaceBrightnessColorMatrix(bright2, 2);
|
|
189
|
+
buildSurfaceBrightnessColorMatrix(half, 0.5);
|
|
190
|
+
const out = new Array<number>(20);
|
|
191
|
+
concatSurfaceColorMatrix(out, bright2, half); // *2 then *0.5 = identity
|
|
192
|
+
const pixel = applyTo(0xc86432ff, out);
|
|
193
|
+
expect(pixel[0]).toBe(200);
|
|
194
|
+
expect(pixel[1]).toBe(100);
|
|
195
|
+
expect(pixel[2]).toBe(50);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('carries the offset through composition', () => {
|
|
199
|
+
const identity = new Array<number>(20);
|
|
200
|
+
const invert = new Array<number>(20);
|
|
201
|
+
setSurfaceColorMatrixIdentity(identity);
|
|
202
|
+
buildSurfaceInvertColorMatrix(invert);
|
|
203
|
+
const out = new Array<number>(20);
|
|
204
|
+
concatSurfaceColorMatrix(out, identity, invert);
|
|
205
|
+
const pixel = applyTo(0xc80000ff, out); // R=200 -> 55
|
|
206
|
+
expect(pixel[0]).toBe(55);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('setSurfaceColorMatrixIdentity', () => {
|
|
211
|
+
it('leaves the pixel unchanged', () => {
|
|
212
|
+
const m = new Array<number>(20);
|
|
213
|
+
setSurfaceColorMatrixIdentity(m);
|
|
214
|
+
const out = applyTo(0xc86432ff, m);
|
|
215
|
+
expect(out[0]).toBe(200);
|
|
216
|
+
expect(out[1]).toBe(100);
|
|
217
|
+
expect(out[2]).toBe(50);
|
|
218
|
+
expect(out[3]).toBe(0xff);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Surface } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { cloneSurface, createSurface } from './surface';
|
|
4
|
+
import { compareSurface, getSurfaceMismatch } from './surfaceCompare';
|
|
5
|
+
import { setSurfacePixel } from './surfacePixel';
|
|
6
|
+
|
|
7
|
+
describe('compareSurface', () => {
|
|
8
|
+
it('throws when widths differ', () => {
|
|
9
|
+
const a = createSurface(4, 4);
|
|
10
|
+
const b = createSurface(8, 4);
|
|
11
|
+
expect(() => compareSurface(a, b)).toThrow();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('throws when heights differ', () => {
|
|
15
|
+
const a = createSurface(4, 4);
|
|
16
|
+
const b = createSurface(4, 8);
|
|
17
|
+
expect(() => compareSurface(a, b)).toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns null for identical images', () => {
|
|
21
|
+
const a = createSurface(4, 4, 0x0000ffff);
|
|
22
|
+
const b = cloneSurface(a);
|
|
23
|
+
expect(compareSurface(a, b)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns diff Surface for different pixels', () => {
|
|
27
|
+
const a = createSurface(2, 1, 0x000000ff);
|
|
28
|
+
const b = createSurface(2, 1, 0x000000ff);
|
|
29
|
+
setSurfacePixel(b, 0, 0, 0x102030ff);
|
|
30
|
+
const result = compareSurface(a, b) as Surface;
|
|
31
|
+
expect(result.width).toBe(2);
|
|
32
|
+
expect(result.height).toBe(1);
|
|
33
|
+
expect(result.data[0]).toBe(0x10);
|
|
34
|
+
expect(result.data[1]).toBe(0x20);
|
|
35
|
+
expect(result.data[2]).toBe(0x30);
|
|
36
|
+
expect(result.data[3]).toBe(255);
|
|
37
|
+
expect(result.data[4]).toBe(0);
|
|
38
|
+
expect(result.data[5]).toBe(0);
|
|
39
|
+
expect(result.data[6]).toBe(0);
|
|
40
|
+
expect(result.data[7]).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('diff pixel alpha is 255 when any channel differs', () => {
|
|
44
|
+
const a = createSurface(1, 1, 0x000000ff);
|
|
45
|
+
const b = createSurface(1, 1, 0x00000080);
|
|
46
|
+
const result = compareSurface(a, b) as Surface;
|
|
47
|
+
expect(result.data[3]).toBe(255);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('unchanged pixels in diff have zero alpha', () => {
|
|
51
|
+
const a = createSurface(2, 1, 0x000000ff);
|
|
52
|
+
const b = cloneSurface(a);
|
|
53
|
+
setSurfacePixel(b, 1, 0, 0xff0000ff);
|
|
54
|
+
const result = compareSurface(a, b) as Surface;
|
|
55
|
+
expect(result.data[3]).toBe(0);
|
|
56
|
+
expect(result.data[7]).toBe(255);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('getSurfaceMismatch', () => {
|
|
61
|
+
it('throws when dimensions differ', () => {
|
|
62
|
+
expect(() => getSurfaceMismatch(createSurface(4, 4), createSurface(4, 8))).toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('reports zero mismatch for identical surfaces', () => {
|
|
66
|
+
const a = createSurface(4, 4, 0x0000ffff);
|
|
67
|
+
const result = getSurfaceMismatch(a, cloneSurface(a));
|
|
68
|
+
expect(result.mismatchedPixels).toBe(0);
|
|
69
|
+
expect(result.totalPixels).toBe(16);
|
|
70
|
+
expect(result.fraction).toBe(0);
|
|
71
|
+
expect(result.maxChannelDelta).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('counts pixels whose max channel delta exceeds the tolerance', () => {
|
|
75
|
+
const a = createSurface(2, 1, 0x000000ff);
|
|
76
|
+
const b = createSurface(2, 1, 0x000000ff);
|
|
77
|
+
setSurfacePixel(b, 0, 0, 0x0a0000ff); // delta 10 on one channel
|
|
78
|
+
expect(getSurfaceMismatch(a, b, 0).mismatchedPixels).toBe(1);
|
|
79
|
+
expect(getSurfaceMismatch(a, b, 9).mismatchedPixels).toBe(1);
|
|
80
|
+
expect(getSurfaceMismatch(a, b, 10).mismatchedPixels).toBe(0); // within tolerance
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('reports the largest channel delta and the mismatch fraction', () => {
|
|
84
|
+
const a = createSurface(2, 1, 0x000000ff);
|
|
85
|
+
const b = createSurface(2, 1, 0x000000ff);
|
|
86
|
+
setSurfacePixel(b, 1, 0, 0x804020ff); // max delta 0x80 = 128
|
|
87
|
+
const result = getSurfaceMismatch(a, b);
|
|
88
|
+
expect(result.maxChannelDelta).toBe(128);
|
|
89
|
+
expect(result.mismatchedPixels).toBe(1);
|
|
90
|
+
expect(result.fraction).toBe(0.5);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { BlendMode } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createSurface } from './surface';
|
|
4
|
+
import {
|
|
5
|
+
compositeSurfacePixels,
|
|
6
|
+
compositeSurfaceRegion,
|
|
7
|
+
extractSurfacePixels,
|
|
8
|
+
extractSurfacePixels32,
|
|
9
|
+
writeSurfacePixels,
|
|
10
|
+
writeSurfacePixels32,
|
|
11
|
+
} from './surfaceComposite';
|
|
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('compositeSurfacePixels', () => {
|
|
24
|
+
it('alpha-composites pixels over the destination', () => {
|
|
25
|
+
const dest = createSurface(1, 1, 0x0000ffff);
|
|
26
|
+
const pixels = new Uint8ClampedArray([0xff, 0, 0, 0xff]);
|
|
27
|
+
compositeSurfacePixels(region(dest), pixels);
|
|
28
|
+
expect(dest.data[0]).toBe(0xff);
|
|
29
|
+
expect(dest.data[2]).toBe(0);
|
|
30
|
+
expect(dest.data[3]).toBe(0xff);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('blends semi-transparent source over destination', () => {
|
|
34
|
+
const dest = createSurface(1, 1, 0x000000ff);
|
|
35
|
+
const pixels = new Uint8ClampedArray([0xff, 0xff, 0xff, 0x80]);
|
|
36
|
+
compositeSurfacePixels(region(dest), pixels);
|
|
37
|
+
expect(dest.data[0]).toBeGreaterThan(0);
|
|
38
|
+
expect(dest.data[0]).toBeLessThan(0xff);
|
|
39
|
+
expect(dest.data[3]).toBe(0xff);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('silently skips pixels outside destination bounds', () => {
|
|
43
|
+
const dest = createSurface(1, 1);
|
|
44
|
+
const pixels = new Uint8ClampedArray([0xff, 0xff, 0xff, 0xff]);
|
|
45
|
+
compositeSurfacePixels(region(dest, 5, 5, 1, 1), pixels);
|
|
46
|
+
expect(dest.data[3]).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('multiply blend mode multiplies source and destination channels', () => {
|
|
50
|
+
const dest = createSurface(1, 1, 0xff0000ff); // RGBA: opaque red
|
|
51
|
+
const pixels = new Uint8ClampedArray([128, 128, 128, 255]);
|
|
52
|
+
compositeSurfacePixels(region(dest), pixels, BlendMode.Multiply);
|
|
53
|
+
expect(dest.data[0]).toBe(128); // 255 * 128 / 255
|
|
54
|
+
expect(dest.data[1]).toBe(0); // 0 * 128 / 255
|
|
55
|
+
expect(dest.data[2]).toBe(0);
|
|
56
|
+
expect(dest.data[3]).toBe(255);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('add blend mode clamps the sum of channels', () => {
|
|
60
|
+
const dest = createSurface(1, 1, 0x640000ff); // RGBA: opaque dark red (R=100)
|
|
61
|
+
const pixels = new Uint8ClampedArray([200, 0, 0, 255]);
|
|
62
|
+
compositeSurfacePixels(region(dest), pixels, BlendMode.Add);
|
|
63
|
+
expect(dest.data[0]).toBe(255); // min(255, 100 + 200)
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('defaults to source-over (Normal) when no blend mode is given', () => {
|
|
67
|
+
const dest = createSurface(1, 1, 0xff0000ff); // RGBA: opaque red
|
|
68
|
+
const pixels = new Uint8ClampedArray([0, 0, 255, 255]);
|
|
69
|
+
compositeSurfacePixels(region(dest), pixels);
|
|
70
|
+
expect(dest.data[0]).toBe(0);
|
|
71
|
+
expect(dest.data[2]).toBe(255);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('overlay blend mode darkens on dark backdrops', () => {
|
|
75
|
+
const dest = createSurface(1, 1, 0x400000ff); // RGBA: R=64
|
|
76
|
+
const pixels = new Uint8ClampedArray([200, 0, 0, 255]);
|
|
77
|
+
compositeSurfacePixels(region(dest), pixels, BlendMode.Overlay);
|
|
78
|
+
expect(dest.data[0]).toBe(100); // 2 * 64 * 200 / 255
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('hardlight blend mode is overlay with operands swapped', () => {
|
|
82
|
+
const dest = createSurface(1, 1, 0xc80000ff); // RGBA: R=200
|
|
83
|
+
const pixels = new Uint8ClampedArray([64, 0, 0, 255]);
|
|
84
|
+
compositeSurfacePixels(region(dest), pixels, BlendMode.Hardlight);
|
|
85
|
+
expect(dest.data[0]).toBe(100); // 2 * 200 * 64 / 255
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('invert blend mode inverts the backdrop, ignoring source color', () => {
|
|
89
|
+
const dest = createSurface(1, 1, 0xc80000ff); // RGBA: R=200
|
|
90
|
+
const pixels = new Uint8ClampedArray([0, 0, 0, 255]);
|
|
91
|
+
compositeSurfacePixels(region(dest), pixels, BlendMode.Invert);
|
|
92
|
+
expect(dest.data[0]).toBe(55); // 255 - 200
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('erase blend mode knocks alpha out of the backdrop, keeping color', () => {
|
|
96
|
+
const dest = createSurface(1, 1, 0xff0000ff); // RGBA: opaque red
|
|
97
|
+
const pixels = new Uint8ClampedArray([0, 0, 0, 128]);
|
|
98
|
+
compositeSurfacePixels(region(dest), pixels, BlendMode.Erase);
|
|
99
|
+
expect(dest.data[0]).toBe(255); // color untouched
|
|
100
|
+
expect(dest.data[3]).toBe(127); // 255 * (1 - 128/255)
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('throws for blend modes with no surface meaning', () => {
|
|
104
|
+
const dest = createSurface(1, 1);
|
|
105
|
+
const pixels = new Uint8ClampedArray([0, 0, 0, 255]);
|
|
106
|
+
expect(() => compositeSurfacePixels(region(dest), pixels, BlendMode.Shader)).toThrow(/not supported/);
|
|
107
|
+
expect(() => compositeSurfacePixels(region(dest), pixels, BlendMode.Alpha)).toThrow(/not supported/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('compositeSurfaceRegion', () => {
|
|
112
|
+
it('alpha-composites a region of one surface over another', () => {
|
|
113
|
+
const source = createSurface(1, 1, 0xff0000ff);
|
|
114
|
+
const dest = createSurface(1, 1, 0x0000ffff);
|
|
115
|
+
compositeSurfaceRegion(region(dest), region(source));
|
|
116
|
+
expect(dest.data[0]).toBe(0xff);
|
|
117
|
+
expect(dest.data[2]).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('clips to the smaller of source and dest dimensions', () => {
|
|
121
|
+
const source = createSurface(1, 1, 0xff0000ff);
|
|
122
|
+
const dest = createSurface(3, 3);
|
|
123
|
+
compositeSurfaceRegion(region(dest, 1, 1, 2, 2), region(source));
|
|
124
|
+
expect(dest.data[(1 * 3 + 1) * 4]).toBe(0xff);
|
|
125
|
+
expect(dest.data[(1 * 3 + 2) * 4]).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('applies the blend mode to the source region', () => {
|
|
129
|
+
const source = createSurface(1, 1, 0x808080ff); // RGBA: opaque gray
|
|
130
|
+
const dest = createSurface(1, 1, 0xff0000ff); // RGBA: opaque red
|
|
131
|
+
compositeSurfaceRegion(region(dest), region(source), BlendMode.Multiply);
|
|
132
|
+
expect(dest.data[0]).toBe(128); // 255 * 128 / 255
|
|
133
|
+
expect(dest.data[1]).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('extractSurfacePixels', () => {
|
|
138
|
+
it('copies a surface region into a tightly-packed buffer', () => {
|
|
139
|
+
const source = createSurface(2, 2);
|
|
140
|
+
const i = (1 * 2 + 1) * 4;
|
|
141
|
+
source.data[i] = 0xff;
|
|
142
|
+
source.data[i + 3] = 0xff;
|
|
143
|
+
const out = new Uint8ClampedArray(4);
|
|
144
|
+
extractSurfacePixels(out, region(source, 1, 1, 1, 1));
|
|
145
|
+
expect(out[0]).toBe(0xff);
|
|
146
|
+
expect(out[3]).toBe(0xff);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('silently skips pixels outside source bounds', () => {
|
|
150
|
+
const source = createSurface(1, 1, 0xffffffff);
|
|
151
|
+
const out = new Uint8ClampedArray(4 * 4);
|
|
152
|
+
extractSurfacePixels(out, region(source, -1, -1, 2, 2));
|
|
153
|
+
const i = (1 * 2 + 1) * 4;
|
|
154
|
+
expect(out[i]).toBe(0xff);
|
|
155
|
+
expect(out[i + 3]).toBe(0xff);
|
|
156
|
+
expect(out[0]).toBe(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('source.surface.data can be passed as out for a full-surface extraction', () => {
|
|
160
|
+
const source = createSurface(2, 2, 0xaabbccff);
|
|
161
|
+
extractSurfacePixels(source.data, region(source));
|
|
162
|
+
expect(source.data[0]).toBe(0xaa);
|
|
163
|
+
expect(source.data[3]).toBe(0xff);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('extractSurfacePixels32', () => {
|
|
168
|
+
it('packs each pixel into one 0xRRGGBBAA entry', () => {
|
|
169
|
+
const source = createSurface(2, 2);
|
|
170
|
+
const i = (1 * 2 + 1) * 4;
|
|
171
|
+
source.data[i] = 0x11;
|
|
172
|
+
source.data[i + 1] = 0x22;
|
|
173
|
+
source.data[i + 2] = 0x33;
|
|
174
|
+
source.data[i + 3] = 0x44;
|
|
175
|
+
const out = new Uint32Array(1);
|
|
176
|
+
extractSurfacePixels32(out, region(source, 1, 1, 1, 1));
|
|
177
|
+
expect(out[0]).toBe(0x11223344);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('round-trips with writeSurfacePixels32', () => {
|
|
181
|
+
const source = createSurface(3, 2);
|
|
182
|
+
for (let p = 0; p < 6; p++) {
|
|
183
|
+
const i = p * 4;
|
|
184
|
+
source.data[i] = p * 10 + 1;
|
|
185
|
+
source.data[i + 1] = p * 10 + 2;
|
|
186
|
+
source.data[i + 2] = p * 10 + 3;
|
|
187
|
+
source.data[i + 3] = p * 10 + 4;
|
|
188
|
+
}
|
|
189
|
+
const packed = new Uint32Array(6);
|
|
190
|
+
extractSurfacePixels32(packed, region(source));
|
|
191
|
+
const dest = createSurface(3, 2);
|
|
192
|
+
writeSurfacePixels32(region(dest), packed);
|
|
193
|
+
expect(Array.from(dest.data)).toEqual(Array.from(source.data));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('silently skips pixels outside source bounds', () => {
|
|
197
|
+
const source = createSurface(1, 1, 0xffffffff);
|
|
198
|
+
const out = new Uint32Array(4);
|
|
199
|
+
extractSurfacePixels32(out, region(source, -1, -1, 2, 2));
|
|
200
|
+
expect(out[1 * 2 + 1]).toBe(0xffffffff);
|
|
201
|
+
expect(out[0]).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('writeSurfacePixels', () => {
|
|
206
|
+
it('writes pixels at the given destination region', () => {
|
|
207
|
+
const dest = createSurface(3, 3);
|
|
208
|
+
const pixels = new Uint8ClampedArray([0x33, 0x66, 0x99, 0xff]);
|
|
209
|
+
writeSurfacePixels(region(dest, 1, 1, 1, 1), pixels);
|
|
210
|
+
const i = (1 * 3 + 1) * 4;
|
|
211
|
+
expect(dest.data[i]).toBe(0x33);
|
|
212
|
+
expect(dest.data[i + 1]).toBe(0x66);
|
|
213
|
+
expect(dest.data[i + 2]).toBe(0x99);
|
|
214
|
+
expect(dest.data[i + 3]).toBe(0xff);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('overwrites existing content', () => {
|
|
218
|
+
const dest = createSurface(1, 1, 0x0000ffff);
|
|
219
|
+
const pixels = new Uint8ClampedArray([0xff, 0, 0, 0xff]);
|
|
220
|
+
writeSurfacePixels(region(dest), pixels);
|
|
221
|
+
expect(dest.data[2]).toBe(0);
|
|
222
|
+
expect(dest.data[0]).toBe(0xff);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('silently clips writes outside destination bounds', () => {
|
|
226
|
+
const dest = createSurface(1, 1);
|
|
227
|
+
const pixels = new Uint8ClampedArray([0xff, 0xff, 0xff, 0xff]);
|
|
228
|
+
writeSurfacePixels(region(dest, 5, 5, 1, 1), pixels);
|
|
229
|
+
expect(dest.data[3]).toBe(0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('writeSurfacePixels32', () => {
|
|
234
|
+
it('unpacks each 0xRRGGBBAA entry into the destination region', () => {
|
|
235
|
+
const dest = createSurface(3, 3);
|
|
236
|
+
writeSurfacePixels32(region(dest, 1, 1, 1, 1), new Uint32Array([0x33669900 | 0xff]));
|
|
237
|
+
const i = (1 * 3 + 1) * 4;
|
|
238
|
+
expect(dest.data[i]).toBe(0x33);
|
|
239
|
+
expect(dest.data[i + 1]).toBe(0x66);
|
|
240
|
+
expect(dest.data[i + 2]).toBe(0x99);
|
|
241
|
+
expect(dest.data[i + 3]).toBe(0xff);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('overwrites existing content', () => {
|
|
245
|
+
const dest = createSurface(1, 1, 0x0000ffff);
|
|
246
|
+
writeSurfacePixels32(region(dest), new Uint32Array([0xff0000ff]));
|
|
247
|
+
expect(dest.data[0]).toBe(0xff);
|
|
248
|
+
expect(dest.data[2]).toBe(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('silently clips writes outside destination bounds', () => {
|
|
252
|
+
const dest = createSurface(1, 1);
|
|
253
|
+
writeSurfacePixels32(region(dest, 5, 5, 1, 1), new Uint32Array([0xffffffff]));
|
|
254
|
+
expect(dest.data[3]).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createSurface } from './surface';
|
|
2
|
+
import { convolveSurface } from './surfaceConvolution';
|
|
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('convolveSurface', () => {
|
|
15
|
+
it('applies a convolution matrix to the source region', () => {
|
|
16
|
+
const source = createSurface(3, 1);
|
|
17
|
+
source.data[0] = 10;
|
|
18
|
+
source.data[4] = 20;
|
|
19
|
+
source.data[8] = 30;
|
|
20
|
+
const out = new Uint8ClampedArray(4);
|
|
21
|
+
convolveSurface(out, region(source, 1, 0, 1, 1), {
|
|
22
|
+
matrix: [1, 1, 1],
|
|
23
|
+
matrixX: 3,
|
|
24
|
+
matrixY: 1,
|
|
25
|
+
preserveAlpha: false,
|
|
26
|
+
});
|
|
27
|
+
expect(out[0]).toBe(20);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("'clamp' edge mode (default) repeats the nearest edge pixel", () => {
|
|
31
|
+
// 1px source, 3-wide kernel: all three samples hit the single source pixel
|
|
32
|
+
const source = createSurface(1, 1, 0x606060ff);
|
|
33
|
+
const out = new Uint8ClampedArray(4);
|
|
34
|
+
convolveSurface(out, region(source), {
|
|
35
|
+
matrix: [1, 1, 1],
|
|
36
|
+
matrixX: 3,
|
|
37
|
+
matrixY: 1,
|
|
38
|
+
divisor: 3,
|
|
39
|
+
preserveAlpha: false,
|
|
40
|
+
});
|
|
41
|
+
expect(out[0]).toBe(0x60);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("'mirror' edge mode reflects at boundaries", () => {
|
|
45
|
+
// 3px source [10, 20, 30]. 5-wide kernel [1, 0, 0, 0, 0] picks 2 positions left.
|
|
46
|
+
// For px=0, offsetX=2: rawSampleX = 0 + 0 + 0 - 2 = -2.
|
|
47
|
+
// mirror(-2, 3) reflects to x=1 (value 20), while clamp(-2) gives x=0 (value 10).
|
|
48
|
+
const source = createSurface(3, 1);
|
|
49
|
+
source.data[0] = 10;
|
|
50
|
+
source.data[4] = 20;
|
|
51
|
+
source.data[8] = 30;
|
|
52
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
53
|
+
convolveSurface(out, region(source), {
|
|
54
|
+
edge: 'mirror',
|
|
55
|
+
matrix: [1, 0, 0, 0, 0],
|
|
56
|
+
matrixX: 5,
|
|
57
|
+
matrixY: 1,
|
|
58
|
+
divisor: 1,
|
|
59
|
+
preserveAlpha: false,
|
|
60
|
+
});
|
|
61
|
+
expect(out[0]).toBe(20);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("'transparent' edge mode treats out-of-bounds as transparent black", () => {
|
|
65
|
+
// 1px source (opaque black), 3-wide kernel [1, 0, 0] — leftmost sample
|
|
66
|
+
// is out of bounds and contributes transparent black (all zeros).
|
|
67
|
+
const source = createSurface(1, 1, 0x000000ff);
|
|
68
|
+
const out = new Uint8ClampedArray(4);
|
|
69
|
+
convolveSurface(out, region(source), {
|
|
70
|
+
edge: 'transparent',
|
|
71
|
+
divisor: 1,
|
|
72
|
+
matrix: [1, 0, 0],
|
|
73
|
+
matrixX: 3,
|
|
74
|
+
matrixY: 1,
|
|
75
|
+
preserveAlpha: false,
|
|
76
|
+
});
|
|
77
|
+
expect(out[0]).toBe(0);
|
|
78
|
+
expect(out[3]).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("'wrap' edge mode tiles the source toroidally", () => {
|
|
82
|
+
// 3px source [10, 20, 30]. The leftmost kernel position for px=0 wraps to px=2.
|
|
83
|
+
const source = createSurface(3, 1);
|
|
84
|
+
source.data[0] = 10;
|
|
85
|
+
source.data[4] = 20;
|
|
86
|
+
source.data[8] = 30;
|
|
87
|
+
const out = new Uint8ClampedArray(3 * 4);
|
|
88
|
+
convolveSurface(out, region(source), {
|
|
89
|
+
edge: 'wrap',
|
|
90
|
+
matrix: [1, 0, 0], // picks left neighbor only
|
|
91
|
+
matrixX: 3,
|
|
92
|
+
matrixY: 1,
|
|
93
|
+
divisor: 1,
|
|
94
|
+
preserveAlpha: false,
|
|
95
|
+
});
|
|
96
|
+
// px=0 left neighbor wraps to px=2 → value 30
|
|
97
|
+
expect(out[0]).toBe(30);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('preserves source alpha by default', () => {
|
|
101
|
+
const source = createSurface(1, 1, 0x00000044);
|
|
102
|
+
const out = new Uint8ClampedArray(4);
|
|
103
|
+
convolveSurface(out, region(source), {
|
|
104
|
+
bias: 255,
|
|
105
|
+
matrix: [1],
|
|
106
|
+
matrixX: 1,
|
|
107
|
+
matrixY: 1,
|
|
108
|
+
});
|
|
109
|
+
expect(out[3]).toBe(0x44);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('throws when matrix dimensions are not positive', () => {
|
|
113
|
+
const source = createSurface(1, 1);
|
|
114
|
+
const out = new Uint8ClampedArray(4);
|
|
115
|
+
expect(() => convolveSurface(out, region(source), { matrix: [], matrixX: 0, matrixY: 1 })).toThrow();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('throws when matrix is too short for its declared dimensions', () => {
|
|
119
|
+
const source = createSurface(1, 1);
|
|
120
|
+
const out = new Uint8ClampedArray(4);
|
|
121
|
+
expect(() => convolveSurface(out, region(source), { matrix: [1], matrixX: 3, matrixY: 3 })).toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('treats an explicit divisor of 0 as passthrough (no division by zero)', () => {
|
|
125
|
+
const source = createSurface(1, 1, 0x804020ff);
|
|
126
|
+
const out = new Uint8ClampedArray(4);
|
|
127
|
+
convolveSurface(out, region(source), {
|
|
128
|
+
divisor: 0,
|
|
129
|
+
matrix: [1],
|
|
130
|
+
matrixX: 1,
|
|
131
|
+
matrixY: 1,
|
|
132
|
+
preserveAlpha: false,
|
|
133
|
+
});
|
|
134
|
+
expect(out[0]).toBe(0x80);
|
|
135
|
+
});
|
|
136
|
+
});
|