148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import { EffectCategory } from "../src/categories";
|
|
import type { EffectDefinition } from "../src/types";
|
|
|
|
import { vignetteEffectDefinition } from "../src/effects/vignette";
|
|
import { filmGrainEffectDefinition } from "../src/effects/film-grain";
|
|
import { sharpenEffectDefinition } from "../src/effects/sharpen";
|
|
import { pixelateEffectDefinition } from "../src/effects/pixelate";
|
|
import { chromaticAberrationEffectDefinition } from "../src/effects/chromatic-aberration";
|
|
import { glitchEffectDefinition } from "../src/effects/glitch";
|
|
import { neonGlowEffectDefinition } from "../src/effects/neon-glow";
|
|
import { sketchEffectDefinition } from "../src/effects/sketch";
|
|
import { oilPaintEffectDefinition } from "../src/effects/oil-paint";
|
|
|
|
const defaultArgs = { width: 1920, height: 1080, time: 1.5 };
|
|
|
|
/** Validate common artistic effect structure */
|
|
function validateArtisticEffect(def: EffectDefinition, expectedType: string, expectedPasses: number) {
|
|
test(`${expectedType}: has correct type and category`, () => {
|
|
expect(def.type).toBe(expectedType);
|
|
expect(def.category).toBe(EffectCategory.ARTISTIC);
|
|
});
|
|
|
|
test(`${expectedType}: has ${expectedPasses} pass(es)`, () => {
|
|
expect(def.renderer.passes).toHaveLength(expectedPasses);
|
|
});
|
|
|
|
test(`${expectedType}: uniforms factory works with defaults`, () => {
|
|
const defaults: Record<string, number | string | boolean> = {};
|
|
for (const p of def.params) defaults[p.key] = p.default;
|
|
for (const pass of def.renderer.passes) {
|
|
const result = pass.uniforms({ effectParams: defaults, ...defaultArgs });
|
|
expect(typeof result).toBe("object");
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Single-pass effects ---
|
|
|
|
describe("Vignette", () => {
|
|
validateArtisticEffect(vignetteEffectDefinition, "vignette", 1);
|
|
|
|
test("has 3 params: intensity, radius, softness", () => {
|
|
expect(vignetteEffectDefinition.params.map((p) => p.key)).toEqual(["intensity", "radius", "softness"]);
|
|
});
|
|
});
|
|
|
|
describe("Film Grain", () => {
|
|
validateArtisticEffect(filmGrainEffectDefinition, "film-grain", 1);
|
|
|
|
test("has animated boolean param", () => {
|
|
const animated = filmGrainEffectDefinition.params.find((p) => p.key === "animated");
|
|
expect(animated?.type).toBe("boolean");
|
|
});
|
|
|
|
test("uses time when animated", () => {
|
|
const pass = filmGrainEffectDefinition.renderer.passes[0];
|
|
const withTime = pass.uniforms({ effectParams: { intensity: 30, size: 2, animated: true }, ...defaultArgs });
|
|
expect(withTime.u_time).toBe(1.5);
|
|
});
|
|
|
|
test("ignores time when not animated", () => {
|
|
const pass = filmGrainEffectDefinition.renderer.passes[0];
|
|
const noTime = pass.uniforms({ effectParams: { intensity: 30, size: 2, animated: false }, ...defaultArgs });
|
|
expect(noTime.u_time).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("Sharpen", () => {
|
|
validateArtisticEffect(sharpenEffectDefinition, "sharpen", 1);
|
|
});
|
|
|
|
describe("Pixelate", () => {
|
|
validateArtisticEffect(pixelateEffectDefinition, "pixelate", 1);
|
|
|
|
test("blockSize default is 10", () => {
|
|
expect(pixelateEffectDefinition.params[0].default).toBe(10);
|
|
});
|
|
|
|
test("blockSize passed directly as uniform", () => {
|
|
const pass = pixelateEffectDefinition.renderer.passes[0];
|
|
const result = pass.uniforms({ effectParams: { blockSize: 20 }, ...defaultArgs });
|
|
expect(result.u_blockSize).toBe(20);
|
|
});
|
|
});
|
|
|
|
describe("Chromatic Aberration", () => {
|
|
validateArtisticEffect(chromaticAberrationEffectDefinition, "chromatic-aberration", 1);
|
|
|
|
test("angle converted to radians", () => {
|
|
const pass = chromaticAberrationEffectDefinition.renderer.passes[0];
|
|
const result = pass.uniforms({ effectParams: { intensity: 50, angle: 180 }, ...defaultArgs });
|
|
expect(result.u_angle).toBeCloseTo(Math.PI, 5);
|
|
});
|
|
});
|
|
|
|
// --- Multi-pass effects ---
|
|
|
|
describe("Glitch", () => {
|
|
validateArtisticEffect(glitchEffectDefinition, "glitch", 2);
|
|
|
|
test("has 3 params: intensity, speed, colorSplit", () => {
|
|
expect(glitchEffectDefinition.params.map((p) => p.key)).toEqual(["intensity", "speed", "colorSplit"]);
|
|
});
|
|
|
|
test("both passes use time", () => {
|
|
const defaults: Record<string, number> = { intensity: 50, speed: 50, colorSplit: 30 };
|
|
for (const pass of glitchEffectDefinition.renderer.passes) {
|
|
const result = pass.uniforms({ effectParams: defaults, ...defaultArgs });
|
|
expect(result.u_time).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Neon Glow", () => {
|
|
validateArtisticEffect(neonGlowEffectDefinition, "neon-glow", 2);
|
|
|
|
test("has color param", () => {
|
|
const colorParam = neonGlowEffectDefinition.params.find((p) => p.key === "color");
|
|
expect(colorParam?.type).toBe("color");
|
|
});
|
|
|
|
test("pass 2 converts hex color to RGB array", () => {
|
|
const pass2 = neonGlowEffectDefinition.renderer.passes[1];
|
|
const result = pass2.uniforms({ effectParams: { intensity: 50, color: "#ff0000", threshold: 50 }, ...defaultArgs });
|
|
expect(result.u_glowColor).toEqual([1, 0, 0]);
|
|
});
|
|
});
|
|
|
|
describe("Sketch", () => {
|
|
validateArtisticEffect(sketchEffectDefinition, "sketch", 2);
|
|
|
|
test("has threshold and lineWidth params", () => {
|
|
expect(sketchEffectDefinition.params.map((p) => p.key)).toEqual(["threshold", "lineWidth"]);
|
|
});
|
|
});
|
|
|
|
describe("Oil Paint", () => {
|
|
validateArtisticEffect(oilPaintEffectDefinition, "oil-paint", 2);
|
|
|
|
test("radius max is capped at 5 for performance", () => {
|
|
const radiusParam = oilPaintEffectDefinition.params.find((p) => p.key === "radius");
|
|
if (radiusParam?.type === "number") {
|
|
expect(radiusParam.max).toBeLessThanOrEqual(5);
|
|
}
|
|
});
|
|
});
|