181 lines
6.1 KiB
TypeScript
181 lines
6.1 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
||
import { EffectCategory } from "../src/categories";
|
||
import type { EffectDefinition } from "../src/types";
|
||
|
||
import { brightnessEffectDefinition } from "../src/effects/brightness";
|
||
import { contrastEffectDefinition } from "../src/effects/contrast";
|
||
import { saturationEffectDefinition } from "../src/effects/saturation";
|
||
import { hueShiftEffectDefinition } from "../src/effects/hue-shift";
|
||
import { temperatureEffectDefinition } from "../src/effects/temperature";
|
||
import { exposureEffectDefinition } from "../src/effects/exposure";
|
||
import { gammaEffectDefinition } from "../src/effects/gamma";
|
||
import { colorBalanceEffectDefinition } from "../src/effects/color-balance";
|
||
|
||
const defaultUniformArgs = { width: 1920, height: 1080 };
|
||
|
||
/** Helper: validate common structure of all color/tone effects */
|
||
function validateColorEffect(def: EffectDefinition, expectedType: string) {
|
||
test(`${expectedType}: has correct type`, () => {
|
||
expect(def.type).toBe(expectedType);
|
||
});
|
||
|
||
test(`${expectedType}: is COLOR_TONE category`, () => {
|
||
expect(def.category).toBe(EffectCategory.COLOR_TONE);
|
||
});
|
||
|
||
test(`${expectedType}: has single-pass WebGL renderer`, () => {
|
||
expect(def.renderer.type).toBe("webgl");
|
||
expect(def.renderer.passes).toHaveLength(1);
|
||
});
|
||
|
||
test(`${expectedType}: has at least one param`, () => {
|
||
expect(def.params.length).toBeGreaterThanOrEqual(1);
|
||
});
|
||
|
||
test(`${expectedType}: has keywords`, () => {
|
||
expect(def.keywords.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test(`${expectedType}: uniforms factory returns object`, () => {
|
||
const defaults: Record<string, number | string | boolean> = {};
|
||
for (const p of def.params) {
|
||
defaults[p.key] = p.default;
|
||
}
|
||
const result = def.renderer.passes[0].uniforms({
|
||
effectParams: defaults,
|
||
...defaultUniformArgs,
|
||
});
|
||
expect(typeof result).toBe("object");
|
||
});
|
||
}
|
||
|
||
describe("Brightness", () => {
|
||
validateColorEffect(brightnessEffectDefinition, "brightness");
|
||
|
||
test("default intensity is 0 (no change)", () => {
|
||
expect(brightnessEffectDefinition.params[0].default).toBe(0);
|
||
});
|
||
|
||
test("uniforms map intensity to -1..1 range", () => {
|
||
const pass = brightnessEffectDefinition.renderer.passes[0];
|
||
const at100 = pass.uniforms({ effectParams: { intensity: 100 }, ...defaultUniformArgs });
|
||
const atNeg100 = pass.uniforms({ effectParams: { intensity: -100 }, ...defaultUniformArgs });
|
||
expect(at100.u_intensity).toBe(1);
|
||
expect(atNeg100.u_intensity).toBe(-1);
|
||
});
|
||
});
|
||
|
||
describe("Contrast", () => {
|
||
validateColorEffect(contrastEffectDefinition, "contrast");
|
||
|
||
test("uniforms map 0 → 1.0 (no change)", () => {
|
||
const pass = contrastEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: { intensity: 0 }, ...defaultUniformArgs });
|
||
expect(result.u_contrast).toBe(1);
|
||
});
|
||
|
||
test("uniforms map 100 → 2.0 (max contrast)", () => {
|
||
const pass = contrastEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: { intensity: 100 }, ...defaultUniformArgs });
|
||
expect(result.u_contrast).toBe(2);
|
||
});
|
||
});
|
||
|
||
describe("Saturation", () => {
|
||
validateColorEffect(saturationEffectDefinition, "saturation");
|
||
|
||
test("uniforms map -100 → 0.0 (grayscale)", () => {
|
||
const pass = saturationEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: { intensity: -100 }, ...defaultUniformArgs });
|
||
expect(result.u_saturation).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe("Hue Shift", () => {
|
||
validateColorEffect(hueShiftEffectDefinition, "hue-shift");
|
||
|
||
test("param range is 0..360 degrees", () => {
|
||
const param = hueShiftEffectDefinition.params[0];
|
||
if (param.type === "number") {
|
||
expect(param.min).toBe(0);
|
||
expect(param.max).toBe(360);
|
||
}
|
||
});
|
||
|
||
test("uniforms convert degrees to 0..1 range", () => {
|
||
const pass = hueShiftEffectDefinition.renderer.passes[0];
|
||
const at180 = pass.uniforms({ effectParams: { degrees: 180 }, ...defaultUniformArgs });
|
||
expect(at180.u_hueShift).toBe(0.5);
|
||
});
|
||
});
|
||
|
||
describe("Temperature", () => {
|
||
validateColorEffect(temperatureEffectDefinition, "temperature");
|
||
|
||
test("uniforms produce small shift values", () => {
|
||
const pass = temperatureEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: { warmth: 100 }, ...defaultUniformArgs });
|
||
expect(result.u_temperature).toBe(0.25);
|
||
});
|
||
});
|
||
|
||
describe("Exposure", () => {
|
||
validateColorEffect(exposureEffectDefinition, "exposure");
|
||
|
||
test("param range is -3..3 stops", () => {
|
||
const param = exposureEffectDefinition.params[0];
|
||
if (param.type === "number") {
|
||
expect(param.min).toBe(-3);
|
||
expect(param.max).toBe(3);
|
||
expect(param.step).toBe(0.1);
|
||
}
|
||
});
|
||
|
||
test("uniforms pass stops directly", () => {
|
||
const pass = exposureEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: { stops: 1.5 }, ...defaultUniformArgs });
|
||
expect(result.u_exposure).toBe(1.5);
|
||
});
|
||
});
|
||
|
||
describe("Gamma", () => {
|
||
validateColorEffect(gammaEffectDefinition, "gamma");
|
||
|
||
test("default gamma is 1.0 (linear)", () => {
|
||
expect(gammaEffectDefinition.params[0].default).toBe(1.0);
|
||
});
|
||
|
||
test("uniforms pass value directly", () => {
|
||
const pass = gammaEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: { value: 2.2 }, ...defaultUniformArgs });
|
||
expect(result.u_gamma).toBe(2.2);
|
||
});
|
||
});
|
||
|
||
describe("Color Balance", () => {
|
||
validateColorEffect(colorBalanceEffectDefinition, "color-balance");
|
||
|
||
test("has 9 params (3 channels × 3 regions)", () => {
|
||
expect(colorBalanceEffectDefinition.params).toHaveLength(9);
|
||
});
|
||
|
||
test("all params default to 0", () => {
|
||
for (const param of colorBalanceEffectDefinition.params) {
|
||
expect(param.default).toBe(0);
|
||
}
|
||
});
|
||
|
||
test("uniforms produce 3 vec3 arrays", () => {
|
||
const defaults: Record<string, number> = {};
|
||
for (const p of colorBalanceEffectDefinition.params) {
|
||
defaults[p.key] = 50;
|
||
}
|
||
const pass = colorBalanceEffectDefinition.renderer.passes[0];
|
||
const result = pass.uniforms({ effectParams: defaults, ...defaultUniformArgs });
|
||
expect(Array.isArray(result.u_shadows)).toBe(true);
|
||
expect(Array.isArray(result.u_midtones)).toBe(true);
|
||
expect(Array.isArray(result.u_highlights)).toBe(true);
|
||
expect((result.u_shadows as number[]).length).toBe(3);
|
||
});
|
||
});
|