@flighthq/effects 0.1.0 → 0.2.1-next.410.a23f189
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/bevelEffect.d.ts +3 -0
- package/dist/bevelEffect.d.ts.map +1 -0
- package/dist/bevelEffect.js +5 -0
- package/dist/bevelEffect.js.map +1 -0
- package/dist/blendEffect.d.ts +3 -0
- package/dist/blendEffect.d.ts.map +1 -0
- package/dist/blendEffect.js +10 -0
- package/dist/blendEffect.js.map +1 -0
- package/dist/blendModeMath.d.ts +6 -0
- package/dist/blendModeMath.d.ts.map +1 -0
- package/dist/blendModeMath.js +172 -0
- package/dist/blendModeMath.js.map +1 -0
- package/dist/blurDownsample.d.ts +17 -0
- package/dist/blurDownsample.d.ts.map +1 -0
- package/dist/blurDownsample.js +35 -0
- package/dist/blurDownsample.js.map +1 -0
- package/dist/blurEffect.d.ts +3 -0
- package/dist/blurEffect.d.ts.map +1 -0
- package/dist/blurEffect.js +7 -0
- package/dist/blurEffect.js.map +1 -0
- package/dist/boxBlurMath.d.ts +27 -0
- package/dist/boxBlurMath.d.ts.map +1 -0
- package/dist/boxBlurMath.js +64 -0
- package/dist/boxBlurMath.js.map +1 -0
- package/dist/compositeEffect.d.ts +3 -0
- package/dist/compositeEffect.d.ts.map +1 -0
- package/dist/compositeEffect.js +10 -0
- package/dist/compositeEffect.js.map +1 -0
- package/dist/compositeOperatorMath.d.ts +3 -0
- package/dist/compositeOperatorMath.d.ts.map +1 -0
- package/dist/compositeOperatorMath.js +65 -0
- package/dist/compositeOperatorMath.js.map +1 -0
- package/dist/convolutionEffect.d.ts +3 -0
- package/dist/convolutionEffect.d.ts.map +1 -0
- package/dist/convolutionEffect.js +4 -0
- package/dist/convolutionEffect.js.map +1 -0
- package/dist/dropShadowEffect.d.ts +3 -0
- package/dist/dropShadowEffect.d.ts.map +1 -0
- package/dist/dropShadowEffect.js +5 -0
- package/dist/dropShadowEffect.js.map +1 -0
- package/dist/gaussianKernel.d.ts +17 -0
- package/dist/gaussianKernel.d.ts.map +1 -0
- package/dist/gaussianKernel.js +49 -0
- package/dist/gaussianKernel.js.map +1 -0
- package/dist/gradientBevelEffect.d.ts +3 -0
- package/dist/gradientBevelEffect.d.ts.map +1 -0
- package/dist/gradientBevelEffect.js +5 -0
- package/dist/gradientBevelEffect.js.map +1 -0
- package/dist/gradientGlowEffect.d.ts +3 -0
- package/dist/gradientGlowEffect.d.ts.map +1 -0
- package/dist/gradientGlowEffect.js +5 -0
- package/dist/gradientGlowEffect.js.map +1 -0
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -13
- package/dist/index.js.map +1 -1
- package/dist/innerGlowEffect.d.ts +3 -0
- package/dist/innerGlowEffect.d.ts.map +1 -0
- package/dist/innerGlowEffect.js +5 -0
- package/dist/innerGlowEffect.js.map +1 -0
- package/dist/innerShadowEffect.d.ts +3 -0
- package/dist/innerShadowEffect.d.ts.map +1 -0
- package/dist/innerShadowEffect.js +5 -0
- package/dist/innerShadowEffect.js.map +1 -0
- package/dist/linearSampledGaussian.d.ts +22 -0
- package/dist/linearSampledGaussian.d.ts.map +1 -0
- package/dist/linearSampledGaussian.js +56 -0
- package/dist/linearSampledGaussian.js.map +1 -0
- package/dist/medianEffect.d.ts +3 -0
- package/dist/medianEffect.d.ts.map +1 -0
- package/dist/medianEffect.js +4 -0
- package/dist/medianEffect.js.map +1 -0
- package/dist/outerGlowEffect.d.ts +3 -0
- package/dist/outerGlowEffect.d.ts.map +1 -0
- package/dist/outerGlowEffect.js +5 -0
- package/dist/outerGlowEffect.js.map +1 -0
- package/dist/renderEffectDefaults.js +51 -20
- package/dist/renderEffectDefaults.js.map +1 -1
- package/dist/renderEffectInputs.js +11 -11
- package/dist/renderEffectInputs.js.map +1 -1
- package/package.json +7 -2
- package/src/bevelEffect.test.ts +14 -0
- package/src/blendEffect.test.ts +23 -0
- package/src/blendModeMath.test.ts +160 -0
- package/src/blurDownsample.test.ts +46 -0
- package/src/blurEffect.test.ts +17 -0
- package/src/boxBlurMath.test.ts +96 -0
- package/src/compositeEffect.test.ts +21 -0
- package/src/compositeOperatorMath.test.ts +45 -0
- package/src/convolutionEffect.test.ts +20 -0
- package/src/dropShadowEffect.test.ts +14 -0
- package/src/gaussianKernel.test.ts +80 -0
- package/src/gradientBevelEffect.test.ts +21 -0
- package/src/gradientGlowEffect.test.ts +21 -0
- package/src/innerGlowEffect.test.ts +15 -0
- package/src/innerShadowEffect.test.ts +15 -0
- package/src/linearSampledGaussian.test.ts +87 -0
- package/src/medianEffect.test.ts +11 -0
- package/src/outerGlowEffect.test.ts +14 -0
- package/src/renderEffectDefaults.test.ts +13 -5
- package/src/renderEffectInputs.test.ts +3 -5
- package/src/renderEffectInterpolation.test.ts +6 -9
- package/src/renderEffectValidation.test.ts +2 -2
- package/dist/brightnessContrastEffect.d.ts +0 -3
- package/dist/brightnessContrastEffect.d.ts.map +0 -1
- package/dist/brightnessContrastEffect.js +0 -4
- package/dist/brightnessContrastEffect.js.map +0 -1
- package/dist/channelMixerEffect.d.ts +0 -3
- package/dist/channelMixerEffect.d.ts.map +0 -1
- package/dist/channelMixerEffect.js +0 -4
- package/dist/channelMixerEffect.js.map +0 -1
- package/dist/colorBlindSimulationEffect.d.ts +0 -3
- package/dist/colorBlindSimulationEffect.d.ts.map +0 -1
- package/dist/colorBlindSimulationEffect.js +0 -4
- package/dist/colorBlindSimulationEffect.js.map +0 -1
- package/dist/colorGradeEffect.d.ts +0 -3
- package/dist/colorGradeEffect.d.ts.map +0 -1
- package/dist/colorGradeEffect.js +0 -4
- package/dist/colorGradeEffect.js.map +0 -1
- package/dist/colorScienceMath.d.ts +0 -9
- package/dist/colorScienceMath.d.ts.map +0 -1
- package/dist/colorScienceMath.js +0 -135
- package/dist/colorScienceMath.js.map +0 -1
- package/dist/exposureEffect.d.ts +0 -3
- package/dist/exposureEffect.d.ts.map +0 -1
- package/dist/exposureEffect.js +0 -4
- package/dist/exposureEffect.js.map +0 -1
- package/dist/grayscaleEffect.d.ts +0 -3
- package/dist/grayscaleEffect.d.ts.map +0 -1
- package/dist/grayscaleEffect.js +0 -4
- package/dist/grayscaleEffect.js.map +0 -1
- package/dist/hueSaturationEffect.d.ts +0 -3
- package/dist/hueSaturationEffect.d.ts.map +0 -1
- package/dist/hueSaturationEffect.js +0 -4
- package/dist/hueSaturationEffect.js.map +0 -1
- package/dist/invertEffect.d.ts +0 -3
- package/dist/invertEffect.d.ts.map +0 -1
- package/dist/invertEffect.js +0 -4
- package/dist/invertEffect.js.map +0 -1
- package/dist/liftGammaGainEffect.d.ts +0 -3
- package/dist/liftGammaGainEffect.d.ts.map +0 -1
- package/dist/liftGammaGainEffect.js +0 -4
- package/dist/liftGammaGainEffect.js.map +0 -1
- package/dist/lookupTableGradeEffect.d.ts +0 -3
- package/dist/lookupTableGradeEffect.d.ts.map +0 -1
- package/dist/lookupTableGradeEffect.js +0 -4
- package/dist/lookupTableGradeEffect.js.map +0 -1
- package/dist/lutMath.d.ts +0 -7
- package/dist/lutMath.d.ts.map +0 -1
- package/dist/lutMath.js +0 -39
- package/dist/lutMath.js.map +0 -1
- package/dist/sepiaEffect.d.ts +0 -3
- package/dist/sepiaEffect.d.ts.map +0 -1
- package/dist/sepiaEffect.js +0 -4
- package/dist/sepiaEffect.js.map +0 -1
- package/src/brightnessContrastEffect.test.ts +0 -14
- package/src/channelMixerEffect.test.ts +0 -12
- package/src/colorBlindSimulationEffect.test.ts +0 -13
- package/src/colorGradeEffect.test.ts +0 -11
- package/src/colorScienceMath.test.ts +0 -160
- package/src/exposureEffect.test.ts +0 -7
- package/src/grayscaleEffect.test.ts +0 -11
- package/src/hueSaturationEffect.test.ts +0 -15
- package/src/invertEffect.test.ts +0 -11
- package/src/liftGammaGainEffect.test.ts +0 -15
- package/src/lookupTableGradeEffect.test.ts +0 -11
- package/src/lutMath.test.ts +0 -67
- package/src/sepiaEffect.test.ts +0 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderEffectInputs.js","sourceRoot":"","sources":["../src/renderEffectInputs.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,wFAAwF;AACxF,0EAA0E;AAC1E,MAAM,UAAU,qBAAqB,CAAC,MAA8B;IAClE,OAAO,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,4FAA4F;AAC5F,6FAA6F;AAC7F,MAAM,UAAU,oBAAoB;IAClC,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED,+FAA+F;AAC/F,+FAA+F;AAC/F,mGAAmG;AACnG,mFAAmF;AACnF,MAAM,oBAAoB,GAA2D;IACnF,kBAAkB,EAAE,CAAC,KAAK,CAAC;IAC3B,WAAW,EAAE,CAAC,KAAK,CAAC;IACpB,uBAAuB,EAAE,CAAC,OAAO,CAAC;IAClC,sBAAsB,EAAE,CAAC,QAAQ,CAAC;IAClC,oBAAoB,EAAE,CAAC,OAAO,CAAC;IAC/B,gBAAgB,EAAE,CAAC,QAAQ,CAAC;IAC5B,oBAAoB,EAAE,CAAC,OAAO,CAAC;IAC/B,UAAU,EAAE,CAAC,OAAO,CAAC;IACrB,SAAS,EAAE,CAAC,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,UAAU,CAAC;IACvB,eAAe,EAAE,CAAC,OAAO,CAAC;IAC1B,aAAa,EAAE,CAAC,KAAK,CAAC;IACtB,qBAAqB,EAAE,CAAC,OAAO,CAAC;CACjC,CAAC;AAEF,8FAA8F;AAC9F,iFAAiF;AACjF,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,oBAAoB;IACpB,wBAAwB;IACxB,aAAa;IACb,yBAAyB;IACzB,
|
|
1
|
+
{"version":3,"file":"renderEffectInputs.js","sourceRoot":"","sources":["../src/renderEffectInputs.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,wFAAwF;AACxF,0EAA0E;AAC1E,MAAM,UAAU,qBAAqB,CAAC,MAA8B;IAClE,OAAO,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,4FAA4F;AAC5F,6FAA6F;AAC7F,MAAM,UAAU,oBAAoB;IAClC,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED,+FAA+F;AAC/F,+FAA+F;AAC/F,mGAAmG;AACnG,mFAAmF;AACnF,MAAM,oBAAoB,GAA2D;IACnF,kBAAkB,EAAE,CAAC,KAAK,CAAC;IAC3B,WAAW,EAAE,CAAC,KAAK,CAAC;IACpB,uBAAuB,EAAE,CAAC,OAAO,CAAC;IAClC,sBAAsB,EAAE,CAAC,QAAQ,CAAC;IAClC,oBAAoB,EAAE,CAAC,OAAO,CAAC;IAC/B,gBAAgB,EAAE,CAAC,QAAQ,CAAC;IAC5B,oBAAoB,EAAE,CAAC,OAAO,CAAC;IAC/B,UAAU,EAAE,CAAC,OAAO,CAAC;IACrB,SAAS,EAAE,CAAC,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,UAAU,CAAC;IACvB,eAAe,EAAE,CAAC,OAAO,CAAC;IAC1B,aAAa,EAAE,CAAC,KAAK,CAAC;IACtB,qBAAqB,EAAE,CAAC,OAAO,CAAC;CACjC,CAAC;AAEF,8FAA8F;AAC9F,iFAAiF;AACjF,MAAM,CAAC,MAAM,mBAAmB,GAAsB;IACpD,oBAAoB;IACpB,wBAAwB;IACxB,aAAa;IACb,aAAa;IACb,aAAa;IACb,YAAY;IACZ,yBAAyB;IACzB,wBAAwB;IACxB,2BAA2B;IAC3B,sBAAsB;IACtB,mBAAmB;IACnB,WAAW;IACX,oBAAoB;IACpB,uBAAuB;IACvB,oBAAoB;IACpB,cAAc;IACd,kBAAkB;IAClB,qBAAqB;IACrB,iBAAiB;IACjB,YAAY;IACZ,cAAc;IACd,eAAe;IACf,qBAAqB;IACrB,oBAAoB;IACpB,gBAAgB;IAChB,iBAAiB;IACjB,mBAAmB;IACnB,gBAAgB;IAChB,gBAAgB;IAChB,sBAAsB;IACtB,iBAAiB;IACjB,cAAc;IACd,kBAAkB;IAClB,iBAAiB;IACjB,eAAe;IACf,yBAAyB;IACzB,gBAAgB;IAChB,iBAAiB;IACjB,kBAAkB;IAClB,iBAAiB;IACjB,sBAAsB;IACtB,eAAe;IACf,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,WAAW;IACX,iBAAiB;IACjB,eAAe;IACf,gBAAgB;IAChB,uBAAuB;IACvB,oBAAoB;CACrB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/effects",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.2.1-next.410.a23f189",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
7
|
+
"directory": "packages/effects"
|
|
8
|
+
},
|
|
4
9
|
"type": "module",
|
|
5
10
|
"main": "dist/index.js",
|
|
6
11
|
"types": "dist/index.d.ts",
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
33
|
},
|
|
29
34
|
"dependencies": {
|
|
30
|
-
"@flighthq/types": "0.1.
|
|
35
|
+
"@flighthq/types": "0.2.1-next.410.a23f189"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
33
38
|
"typescript": "^5.3.0"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createBevelEffect } from './bevelEffect';
|
|
2
|
+
|
|
3
|
+
describe('createBevelEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createBevelEffect().kind).toBe('BevelEffect');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('carries options', () => {
|
|
9
|
+
expect(createBevelEffect({ sourceMode: 'knockout', strength: 2 })).toMatchObject({
|
|
10
|
+
sourceMode: 'knockout',
|
|
11
|
+
strength: 2,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AdvancedBlendMode } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createBlendEffect } from './blendEffect';
|
|
4
|
+
|
|
5
|
+
describe('createBlendEffect', () => {
|
|
6
|
+
it('builds a BlendEffect carrying the requested mode', () => {
|
|
7
|
+
const effect = createBlendEffect(AdvancedBlendMode.Overlay);
|
|
8
|
+
expect(effect.kind).toBe('BlendEffect');
|
|
9
|
+
expect(effect.mode).toBe('Overlay');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('spreads backdropKey and opacity options', () => {
|
|
13
|
+
const effect = createBlendEffect(AdvancedBlendMode.SoftLight, { backdropKey: 'scene.backdrop', opacity: 0.5 });
|
|
14
|
+
expect(effect.backdropKey).toBe('scene.backdrop');
|
|
15
|
+
expect(effect.opacity).toBe(0.5);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('leaves backdropKey and opacity undefined when omitted', () => {
|
|
19
|
+
const effect = createBlendEffect(AdvancedBlendMode.Hue);
|
|
20
|
+
expect(effect.backdropKey).toBeUndefined();
|
|
21
|
+
expect(effect.opacity).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { AdvancedBlendMode } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
blendNonSeparableRgb,
|
|
5
|
+
getAdvancedBlendRgb,
|
|
6
|
+
getSeparableBlendChannel,
|
|
7
|
+
isNonSeparableBlendMode,
|
|
8
|
+
} from './blendModeMath';
|
|
9
|
+
|
|
10
|
+
const LUM = (r: number, g: number, b: number): number => 0.3 * r + 0.59 * g + 0.11 * b;
|
|
11
|
+
const SAT = (r: number, g: number, b: number): number => Math.max(r, g, b) - Math.min(r, g, b);
|
|
12
|
+
|
|
13
|
+
describe('blendNonSeparableRgb', () => {
|
|
14
|
+
it('Luminosity replaces backdrop luma with source luma, preserving backdrop hue/sat', () => {
|
|
15
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
16
|
+
const cb: [number, number, number] = [0.8, 0.2, 0.4];
|
|
17
|
+
const cs: [number, number, number] = [0.5, 0.5, 0.5];
|
|
18
|
+
blendNonSeparableRgb(AdvancedBlendMode.Luminosity, cb[0], cb[1], cb[2], cs[0], cs[1], cs[2], out);
|
|
19
|
+
// Result luma equals the source's luma.
|
|
20
|
+
expect(LUM(out[0], out[1], out[2])).toBeCloseTo(LUM(cs[0], cs[1], cs[2]), 5);
|
|
21
|
+
// Backdrop saturation is preserved (chroma range unchanged) since no channel clipped.
|
|
22
|
+
expect(SAT(out[0], out[1], out[2])).toBeCloseTo(SAT(cb[0], cb[1], cb[2]), 5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('Color takes source hue+sat with backdrop luma', () => {
|
|
26
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
27
|
+
const cb: [number, number, number] = [0.2, 0.2, 0.2]; // gray backdrop, luma 0.2
|
|
28
|
+
const cs: [number, number, number] = [0.9, 0.1, 0.1];
|
|
29
|
+
blendNonSeparableRgb(AdvancedBlendMode.Color, cb[0], cb[1], cb[2], cs[0], cs[1], cs[2], out);
|
|
30
|
+
expect(LUM(out[0], out[1], out[2])).toBeCloseTo(LUM(cb[0], cb[1], cb[2]), 5);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('Hue takes source hue, backdrop sat and luma', () => {
|
|
34
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
35
|
+
const cb: [number, number, number] = [0.4, 0.5, 0.6];
|
|
36
|
+
const cs: [number, number, number] = [0.9, 0.2, 0.2];
|
|
37
|
+
blendNonSeparableRgb(AdvancedBlendMode.Hue, cb[0], cb[1], cb[2], cs[0], cs[1], cs[2], out);
|
|
38
|
+
expect(LUM(out[0], out[1], out[2])).toBeCloseTo(LUM(cb[0], cb[1], cb[2]), 5);
|
|
39
|
+
expect(SAT(out[0], out[1], out[2])).toBeCloseTo(SAT(cb[0], cb[1], cb[2]), 5);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('Saturation takes source sat, backdrop hue and luma', () => {
|
|
43
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
44
|
+
const cb: [number, number, number] = [0.4, 0.5, 0.6];
|
|
45
|
+
const cs: [number, number, number] = [0.9, 0.2, 0.2]; // sat 0.7
|
|
46
|
+
blendNonSeparableRgb(AdvancedBlendMode.Saturation, cb[0], cb[1], cb[2], cs[0], cs[1], cs[2], out);
|
|
47
|
+
expect(SAT(out[0], out[1], out[2])).toBeCloseTo(SAT(cs[0], cs[1], cs[2]), 5);
|
|
48
|
+
expect(LUM(out[0], out[1], out[2])).toBeCloseTo(LUM(cb[0], cb[1], cb[2]), 5);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps every channel in 0..1 after gamut clipping', () => {
|
|
52
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
53
|
+
blendNonSeparableRgb(AdvancedBlendMode.Color, 0.95, 0.95, 0.95, 1, 0, 0, out);
|
|
54
|
+
for (const c of out) {
|
|
55
|
+
expect(c).toBeGreaterThanOrEqual(0);
|
|
56
|
+
expect(c).toBeLessThanOrEqual(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('falls through to the source triple for an unknown mode', () => {
|
|
61
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
62
|
+
blendNonSeparableRgb('acme.Nope', 0.1, 0.2, 0.3, 0.7, 0.8, 0.9, out);
|
|
63
|
+
expect(out).toEqual([0.7, 0.8, 0.9]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getAdvancedBlendRgb', () => {
|
|
68
|
+
it('falls through to the source triple for a non-advanced mode', () => {
|
|
69
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
70
|
+
// A fixed-function mode name is not an AdvancedBlendMode, so it takes the separable Normal path.
|
|
71
|
+
getAdvancedBlendRgb('Multiply', 0.5, 0.5, 0.5, 0.2, 0.3, 0.4, out);
|
|
72
|
+
expect(out).toEqual([0.2, 0.3, 0.4]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('computes Difference per channel', () => {
|
|
76
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
77
|
+
getAdvancedBlendRgb(AdvancedBlendMode.Difference, 0.8, 0.5, 0.2, 0.3, 0.5, 0.9, out);
|
|
78
|
+
expect(out[0]).toBeCloseTo(0.5, 5);
|
|
79
|
+
expect(out[1]).toBeCloseTo(0, 5);
|
|
80
|
+
expect(out[2]).toBeCloseTo(0.7, 5);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('routes HSL modes through the non-separable path', () => {
|
|
84
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
85
|
+
getAdvancedBlendRgb(AdvancedBlendMode.Luminosity, 0.8, 0.2, 0.4, 0.5, 0.5, 0.5, out);
|
|
86
|
+
expect(LUM(out[0], out[1], out[2])).toBeCloseTo(0.5, 5);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('getSeparableBlendChannel', () => {
|
|
91
|
+
it('Overlay equals HardLight with operands swapped', () => {
|
|
92
|
+
const cb = 0.6;
|
|
93
|
+
const cs = 0.3;
|
|
94
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.Overlay, cb, cs)).toBeCloseTo(
|
|
95
|
+
getSeparableBlendChannel(AdvancedBlendMode.HardLight, cs, cb),
|
|
96
|
+
6,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('Difference is the absolute channel distance', () => {
|
|
101
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.Difference, 0.8, 0.3)).toBeCloseTo(0.5, 6);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('Exclusion(x, 0.5) collapses to 0.5 regardless of backdrop', () => {
|
|
105
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.Exclusion, 0.2, 0.5)).toBeCloseTo(0.5, 6);
|
|
106
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.Exclusion, 0.9, 0.5)).toBeCloseTo(0.5, 6);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('ColorDodge clamps to white when source is 1 and stays 0 for a black backdrop', () => {
|
|
110
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.ColorDodge, 0.4, 1)).toBe(1);
|
|
111
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.ColorDodge, 0, 0.5)).toBe(0);
|
|
112
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.ColorDodge, 0.25, 0.5)).toBeCloseTo(0.5, 6);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('ColorBurn clamps to black when source is 0 and stays 1 for a white backdrop', () => {
|
|
116
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.ColorBurn, 0.6, 0)).toBe(0);
|
|
117
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.ColorBurn, 1, 0.5)).toBe(1);
|
|
118
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.ColorBurn, 0.75, 0.5)).toBeCloseTo(0.5, 6);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('SoftLight leaves the backdrop unchanged for a mid-gray source', () => {
|
|
122
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.SoftLight, 0.6, 0.5)).toBeCloseTo(0.6, 6);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('HardLight over a 0.5 source is a straight overlay of the backdrop', () => {
|
|
126
|
+
// cs = 0.5 sits on the branch boundary; both branches agree there.
|
|
127
|
+
expect(getSeparableBlendChannel(AdvancedBlendMode.HardLight, 0.4, 0.5)).toBeCloseTo(0.4, 6);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns the source channel (Normal) for an unknown mode', () => {
|
|
131
|
+
expect(getSeparableBlendChannel('acme.Nope', 0.2, 0.7)).toBe(0.7);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('isNonSeparableBlendMode', () => {
|
|
136
|
+
it('is true only for the four HSL modes', () => {
|
|
137
|
+
for (const mode of [
|
|
138
|
+
AdvancedBlendMode.Hue,
|
|
139
|
+
AdvancedBlendMode.Saturation,
|
|
140
|
+
AdvancedBlendMode.Color,
|
|
141
|
+
AdvancedBlendMode.Luminosity,
|
|
142
|
+
]) {
|
|
143
|
+
expect(isNonSeparableBlendMode(mode)).toBe(true);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('is false for the separable modes', () => {
|
|
148
|
+
for (const mode of [
|
|
149
|
+
AdvancedBlendMode.Overlay,
|
|
150
|
+
AdvancedBlendMode.HardLight,
|
|
151
|
+
AdvancedBlendMode.SoftLight,
|
|
152
|
+
AdvancedBlendMode.Difference,
|
|
153
|
+
AdvancedBlendMode.Exclusion,
|
|
154
|
+
AdvancedBlendMode.ColorDodge,
|
|
155
|
+
AdvancedBlendMode.ColorBurn,
|
|
156
|
+
]) {
|
|
157
|
+
expect(isNonSeparableBlendMode(mode)).toBe(false);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getBlurDownsampleLevel, getBlurResidualSigma } from './blurDownsample';
|
|
2
|
+
|
|
3
|
+
describe('getBlurDownsampleLevel', () => {
|
|
4
|
+
it('stays at full resolution at or below the max sigma threshold', () => {
|
|
5
|
+
expect(getBlurDownsampleLevel(1)).toBe(0);
|
|
6
|
+
expect(getBlurDownsampleLevel(2)).toBe(0);
|
|
7
|
+
expect(getBlurDownsampleLevel(4)).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('adds one level per doubling of sigma past the threshold', () => {
|
|
11
|
+
expect(getBlurDownsampleLevel(8)).toBe(1);
|
|
12
|
+
expect(getBlurDownsampleLevel(16)).toBe(2);
|
|
13
|
+
expect(getBlurDownsampleLevel(32)).toBe(3);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('rounds a partial doubling up to the next level', () => {
|
|
17
|
+
// sigma 5 → log2(5/4) ≈ 0.32 → ceil → level 1.
|
|
18
|
+
expect(getBlurDownsampleLevel(5)).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns 0 for non-positive sigma', () => {
|
|
22
|
+
expect(getBlurDownsampleLevel(0)).toBe(0);
|
|
23
|
+
expect(getBlurDownsampleLevel(-4)).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('never leaves a residual sigma above the threshold', () => {
|
|
27
|
+
for (const sigma of [4, 5, 8, 13, 16, 40, 100]) {
|
|
28
|
+
const level = getBlurDownsampleLevel(sigma);
|
|
29
|
+
expect(getBlurResidualSigma(sigma, level)).toBeLessThanOrEqual(4);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getBlurResidualSigma', () => {
|
|
35
|
+
it('divides sigma by 2^level', () => {
|
|
36
|
+
expect(getBlurResidualSigma(8, 1)).toBe(4);
|
|
37
|
+
expect(getBlurResidualSigma(16, 2)).toBe(4);
|
|
38
|
+
expect(getBlurResidualSigma(4, 0)).toBe(4);
|
|
39
|
+
expect(getBlurResidualSigma(32, 3)).toBe(4);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns 0 for non-positive sigma', () => {
|
|
43
|
+
expect(getBlurResidualSigma(0, 2)).toBe(0);
|
|
44
|
+
expect(getBlurResidualSigma(-8, 1)).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createBlurEffect } from './blurEffect';
|
|
2
|
+
|
|
3
|
+
describe('createBlurEffect', () => {
|
|
4
|
+
it('sets the kind', () => {
|
|
5
|
+
expect(createBlurEffect().kind).toBe('BlurEffect');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('defaults to no options beyond kind', () => {
|
|
9
|
+
expect(createBlurEffect()).toEqual({ kind: 'BlurEffect' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('carries through blurX and blurY', () => {
|
|
13
|
+
const effect = createBlurEffect({ blurX: 6, blurY: 3 });
|
|
14
|
+
expect(effect.blurX).toBe(6);
|
|
15
|
+
expect(effect.blurY).toBe(3);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { computeBoxBlurPassRadius, computeBoxBlurRadius, computeGaussianSigmaForBlurRadius } from './boxBlurMath';
|
|
2
|
+
|
|
3
|
+
describe('computeBoxBlurPassRadius', () => {
|
|
4
|
+
// Combined variance of n box passes of radius rᵢ is Σ (rᵢ² + rᵢ) / 3; the effective σ is its
|
|
5
|
+
// square root. These assert the construction tracks the target σ rather than overshooting.
|
|
6
|
+
const effectiveSigma = (radii: ReadonlyArray<number>): number =>
|
|
7
|
+
Math.sqrt(radii.reduce((sum, r) => sum + (r * r + r) / 3, 0));
|
|
8
|
+
|
|
9
|
+
const passRadii = (sigma: number, passes: number): number[] =>
|
|
10
|
+
Array.from({ length: passes }, (_unused, pass) => computeBoxBlurPassRadius(sigma, passes, pass));
|
|
11
|
+
|
|
12
|
+
it('returns 0 for every pass when sigma is 0', () => {
|
|
13
|
+
expect(passRadii(0, 3)).toEqual([0, 0, 0]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns 0 for every pass when sigma is negative', () => {
|
|
17
|
+
expect(passRadii(-1, 2)).toEqual([0, 0]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('uses at most two distinct box sizes', () => {
|
|
21
|
+
expect(new Set(passRadii(12, 3)).size).toBeLessThanOrEqual(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('is non-decreasing in pass index', () => {
|
|
25
|
+
const radii = passRadii(4, 3);
|
|
26
|
+
for (let i = 1; i < radii.length; i++) {
|
|
27
|
+
expect(radii[i]).toBeGreaterThanOrEqual(radii[i - 1]);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('approximates the target sigma within a few percent without overshooting', () => {
|
|
32
|
+
for (const sigma of [4, 8, 12]) {
|
|
33
|
+
const sigmaEff = effectiveSigma(passRadii(sigma, 3));
|
|
34
|
+
expect(sigmaEff).toBeGreaterThan(sigma * 0.9);
|
|
35
|
+
expect(sigmaEff).toBeLessThanOrEqual(sigma * 1.02);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not overshoot the way a single repeated rounded radius does', () => {
|
|
40
|
+
// The uniform approach rounds 3.53 up to 4 → σ_eff ≈ 4.47 for σ=4, n=3.
|
|
41
|
+
const naiveRadius = computeBoxBlurRadius(4, 3);
|
|
42
|
+
const naiveSigma = effectiveSigma([naiveRadius, naiveRadius, naiveRadius]);
|
|
43
|
+
const splitSigma = effectiveSigma(passRadii(4, 3));
|
|
44
|
+
expect(naiveSigma).toBeGreaterThan(4); // documents the overshoot
|
|
45
|
+
expect(Math.abs(splitSigma - 4)).toBeLessThan(Math.abs(naiveSigma - 4));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('computeBoxBlurRadius', () => {
|
|
50
|
+
it('returns 0 for sigma 0', () => {
|
|
51
|
+
expect(computeBoxBlurRadius(0, 1)).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns 0 for negative sigma', () => {
|
|
55
|
+
expect(computeBoxBlurRadius(-4, 1)).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns a positive radius for positive sigma', () => {
|
|
59
|
+
expect(computeBoxBlurRadius(4, 1)).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns a smaller radius for more passes at the same sigma', () => {
|
|
63
|
+
const r1 = computeBoxBlurRadius(10, 1);
|
|
64
|
+
const r3 = computeBoxBlurRadius(10, 3);
|
|
65
|
+
expect(r3).toBeLessThan(r1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns radius 6 for sigma 4 with 1 pass', () => {
|
|
69
|
+
// round((-1 + sqrt(1 + 12·16/1)) / 2) = round((-1 + sqrt(193)) / 2) ≈ round(6.44) = 6
|
|
70
|
+
expect(computeBoxBlurRadius(4, 1)).toBe(6);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('computeGaussianSigmaForBlurRadius', () => {
|
|
75
|
+
it('returns 0 for radius 0', () => {
|
|
76
|
+
expect(computeGaussianSigmaForBlurRadius(0, 1)).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns 0 for negative radius', () => {
|
|
80
|
+
expect(computeGaussianSigmaForBlurRadius(-1, 1)).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns 0 for non-positive passes', () => {
|
|
84
|
+
expect(computeGaussianSigmaForBlurRadius(4, 0)).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns a positive sigma for positive radius and passes', () => {
|
|
88
|
+
expect(computeGaussianSigmaForBlurRadius(6, 1)).toBeGreaterThan(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('produces a larger sigma for more passes at the same radius', () => {
|
|
92
|
+
const s1 = computeGaussianSigmaForBlurRadius(4, 1);
|
|
93
|
+
const s3 = computeGaussianSigmaForBlurRadius(4, 3);
|
|
94
|
+
expect(s3).toBeGreaterThan(s1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CompositeOperator } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { createCompositeEffect } from './compositeEffect';
|
|
4
|
+
|
|
5
|
+
describe('createCompositeEffect', () => {
|
|
6
|
+
it('builds a CompositeEffect carrying the requested operator', () => {
|
|
7
|
+
const effect = createCompositeEffect(CompositeOperator.DestinationOut);
|
|
8
|
+
expect(effect.kind).toBe('CompositeEffect');
|
|
9
|
+
expect(effect.operator).toBe('DestinationOut');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('spreads the backdropKey option', () => {
|
|
13
|
+
const effect = createCompositeEffect(CompositeOperator.DestinationIn, { backdropKey: 'scene.backdrop' });
|
|
14
|
+
expect(effect.backdropKey).toBe('scene.backdrop');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('leaves backdropKey undefined when omitted', () => {
|
|
18
|
+
const effect = createCompositeEffect(CompositeOperator.Xor);
|
|
19
|
+
expect(effect.backdropKey).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CompositeOperator } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { getCompositeOperatorFactors } from './compositeOperatorMath';
|
|
4
|
+
|
|
5
|
+
function factors(operator: CompositeOperator, as: number, ab: number): [number, number] {
|
|
6
|
+
const out: [number, number] = [0, 0];
|
|
7
|
+
getCompositeOperatorFactors(operator, as, ab, out);
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('getCompositeOperatorFactors', () => {
|
|
12
|
+
it('gives source-over its canonical [1, 1 - as]', () => {
|
|
13
|
+
expect(factors(CompositeOperator.SourceOver, 0.6, 0.4)).toEqual([1, 1 - 0.6]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('erases (DestinationOut) by keeping only the backdrop scaled by 1 - as', () => {
|
|
17
|
+
expect(factors(CompositeOperator.DestinationOut, 0.75, 1)).toEqual([0, 1 - 0.75]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('masks (DestinationIn) by keeping only the backdrop scaled by as', () => {
|
|
21
|
+
expect(factors(CompositeOperator.DestinationIn, 0.75, 1)).toEqual([0, 0.75]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('copies the source and drops the backdrop', () => {
|
|
25
|
+
expect(factors(CompositeOperator.Copy, 0.3, 0.9)).toEqual([1, 0]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('clears both contributions', () => {
|
|
29
|
+
expect(factors(CompositeOperator.Clear, 0.3, 0.9)).toEqual([0, 0]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('xors to the mutually-uncovered regions', () => {
|
|
33
|
+
expect(factors(CompositeOperator.Xor, 0.5, 0.25)).toEqual([1 - 0.25, 1 - 0.5]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('falls through to source-over for an unknown (vendor) operator', () => {
|
|
37
|
+
expect(factors('acme.custom', 0.6, 0.4)).toEqual([1, 1 - 0.6]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is alias-safe writing into a supplied out array', () => {
|
|
41
|
+
const out: [number, number] = [9, 9];
|
|
42
|
+
getCompositeOperatorFactors(CompositeOperator.DestinationOver, 0.2, 0.8, out);
|
|
43
|
+
expect(out).toEqual([1 - 0.8, 1]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createConvolutionEffect } from './convolutionEffect';
|
|
2
|
+
|
|
3
|
+
describe('createConvolutionEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createConvolutionEffect({ matrix: [1], matrixX: 1, matrixY: 1 }).kind).toBe('ConvolutionEffect');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('carries required fields', () => {
|
|
9
|
+
const matrix = [0, 1, 0, 1, -4, 1, 0, 1, 0];
|
|
10
|
+
const effect = createConvolutionEffect({ matrix, matrixX: 3, matrixY: 3 });
|
|
11
|
+
expect(effect.matrixX).toBe(3);
|
|
12
|
+
expect(effect.matrixY).toBe(3);
|
|
13
|
+
expect(effect.matrix).toBe(matrix);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('carries optional fields', () => {
|
|
17
|
+
const effect = createConvolutionEffect({ matrix: [1], matrixX: 1, matrixY: 1, bias: 8, preserveAlpha: false });
|
|
18
|
+
expect(effect).toMatchObject({ bias: 8, preserveAlpha: false });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createDropShadowEffect } from './dropShadowEffect';
|
|
2
|
+
|
|
3
|
+
describe('createDropShadowEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createDropShadowEffect().kind).toBe('DropShadowEffect');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('carries options', () => {
|
|
9
|
+
expect(createDropShadowEffect({ sourceMode: 'knockout', strength: 2 })).toMatchObject({
|
|
10
|
+
sourceMode: 'knockout',
|
|
11
|
+
strength: 2,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { computeGaussianKernelWeights, getGaussianKernelSize } from './gaussianKernel';
|
|
2
|
+
|
|
3
|
+
describe('computeGaussianKernelWeights', () => {
|
|
4
|
+
const sumOf = (weights: ReadonlyArray<number>): number => weights.reduce((total, weight) => total + weight, 0);
|
|
5
|
+
|
|
6
|
+
it('produces weights that sum to exactly 1 across several sigmas', () => {
|
|
7
|
+
for (const sigma of [0.75, 1, 2, 3.5, 6]) {
|
|
8
|
+
const out: number[] = [];
|
|
9
|
+
computeGaussianKernelWeights(sigma, out);
|
|
10
|
+
expect(sumOf(out)).toBeCloseTo(1, 12);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('produces a symmetric kernel', () => {
|
|
15
|
+
const out: number[] = [];
|
|
16
|
+
computeGaussianKernelWeights(2, out);
|
|
17
|
+
for (let i = 0; i < out.length; i++) {
|
|
18
|
+
expect(out[i]).toBeCloseTo(out[out.length - 1 - i], 12);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('makes the center weight the maximum', () => {
|
|
23
|
+
const out: number[] = [];
|
|
24
|
+
computeGaussianKernelWeights(2.5, out);
|
|
25
|
+
const center = (out.length - 1) / 2;
|
|
26
|
+
for (let i = 0; i < out.length; i++) {
|
|
27
|
+
expect(out[center]).toBeGreaterThanOrEqual(out[i]);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('matches the closed-form Gaussian ratio between taps', () => {
|
|
32
|
+
// w[center+d] / w[center] = exp(-d^2 / (2 sigma^2)), independent of the normalization constant.
|
|
33
|
+
const sigma = 2;
|
|
34
|
+
const out: number[] = [];
|
|
35
|
+
computeGaussianKernelWeights(sigma, out);
|
|
36
|
+
const center = (out.length - 1) / 2;
|
|
37
|
+
for (const d of [1, 2, 3]) {
|
|
38
|
+
const expectedRatio = Math.exp(-(d * d) / (2 * sigma * sigma));
|
|
39
|
+
expect(out[center + d] / out[center]).toBeCloseTo(expectedRatio, 12);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('sizes out to getGaussianKernelSize and reuses the array', () => {
|
|
44
|
+
const out: number[] = [9, 9, 9, 9, 9, 9, 9, 9, 9, 9];
|
|
45
|
+
computeGaussianKernelWeights(2, out);
|
|
46
|
+
expect(out.length).toBe(getGaussianKernelSize(2));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns the identity kernel for non-positive sigma', () => {
|
|
50
|
+
const out: number[] = [];
|
|
51
|
+
computeGaussianKernelWeights(0, out);
|
|
52
|
+
expect(out).toEqual([1]);
|
|
53
|
+
computeGaussianKernelWeights(-3, out);
|
|
54
|
+
expect(out).toEqual([1]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns out', () => {
|
|
58
|
+
const out: number[] = [];
|
|
59
|
+
expect(computeGaussianKernelWeights(1.5, out)).toBe(out);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getGaussianKernelSize', () => {
|
|
64
|
+
it('is radius*2+1 with radius = ceil(3*sigma)', () => {
|
|
65
|
+
expect(getGaussianKernelSize(1)).toBe(2 * 3 + 1);
|
|
66
|
+
expect(getGaussianKernelSize(2)).toBe(2 * 6 + 1);
|
|
67
|
+
expect(getGaussianKernelSize(2.5)).toBe(2 * Math.ceil(7.5) + 1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('is always odd', () => {
|
|
71
|
+
for (const sigma of [0.5, 1, 1.7, 4, 9]) {
|
|
72
|
+
expect(getGaussianKernelSize(sigma) % 2).toBe(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns 1 for non-positive sigma', () => {
|
|
77
|
+
expect(getGaussianKernelSize(0)).toBe(1);
|
|
78
|
+
expect(getGaussianKernelSize(-2)).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createGradientBevelEffect } from './gradientBevelEffect';
|
|
2
|
+
|
|
3
|
+
describe('createGradientBevelEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createGradientBevelEffect({ colors: [0xff0000, 0x00ff00], alphas: [1, 1], ratios: [0, 255] }).kind).toBe(
|
|
6
|
+
'GradientBevelEffect',
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('carries options', () => {
|
|
11
|
+
expect(
|
|
12
|
+
createGradientBevelEffect({
|
|
13
|
+
colors: [0xff0000, 0x00ff00],
|
|
14
|
+
alphas: [1, 1],
|
|
15
|
+
ratios: [0, 255],
|
|
16
|
+
sourceMode: 'hide',
|
|
17
|
+
strength: 2,
|
|
18
|
+
}),
|
|
19
|
+
).toMatchObject({ sourceMode: 'hide', strength: 2 });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createGradientGlowEffect } from './gradientGlowEffect';
|
|
2
|
+
|
|
3
|
+
describe('createGradientGlowEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createGradientGlowEffect({ colors: [0xff0000, 0x00ff00], alphas: [1, 1], ratios: [0, 255] }).kind).toBe(
|
|
6
|
+
'GradientGlowEffect',
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('carries options', () => {
|
|
11
|
+
expect(
|
|
12
|
+
createGradientGlowEffect({
|
|
13
|
+
colors: [0xff0000, 0x00ff00],
|
|
14
|
+
alphas: [1, 1],
|
|
15
|
+
ratios: [0, 255],
|
|
16
|
+
sourceMode: 'knockout',
|
|
17
|
+
strength: 2,
|
|
18
|
+
}),
|
|
19
|
+
).toMatchObject({ sourceMode: 'knockout', strength: 2 });
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createInnerGlowEffect } from './innerGlowEffect';
|
|
2
|
+
|
|
3
|
+
describe('createInnerGlowEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createInnerGlowEffect().kind).toBe('InnerGlowEffect');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('carries options', () => {
|
|
9
|
+
expect(createInnerGlowEffect({ strength: 2 })).toMatchObject({ strength: 2 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('carries source mode', () => {
|
|
13
|
+
expect(createInnerGlowEffect({ sourceMode: 'hide' })).toMatchObject({ sourceMode: 'hide' });
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createInnerShadowEffect } from './innerShadowEffect';
|
|
2
|
+
|
|
3
|
+
describe('createInnerShadowEffect', () => {
|
|
4
|
+
it('tags the intent type', () => {
|
|
5
|
+
expect(createInnerShadowEffect().kind).toBe('InnerShadowEffect');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('carries options', () => {
|
|
9
|
+
expect(createInnerShadowEffect({ strength: 2 })).toMatchObject({ strength: 2 });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('carries source mode', () => {
|
|
13
|
+
expect(createInnerShadowEffect({ sourceMode: 'hide' })).toMatchObject({ sourceMode: 'hide' });
|
|
14
|
+
});
|
|
15
|
+
});
|