chore: add util for get effectParams

This commit is contained in:
anh doan 2026-03-23 16:41:42 +07:00
parent 04c3eed7e0
commit 4f74ec9057
No known key found for this signature in database
25 changed files with 125 additions and 47 deletions

View File

@ -19,6 +19,9 @@ let pendingTimeoutId: ReturnType<typeof setTimeout> | null = null;
/** Detection timeout in milliseconds */
const DETECTION_TIMEOUT_MS = 5000;
/** MediaPipe Face Mesh version — must match package.json dependency */
const MEDIAPIPE_VERSION = "0.4.1657299874";
/** Types that MediaPipe FaceMesh accepts */
type MediaPipeImageSource =
| HTMLImageElement
@ -80,7 +83,7 @@ async function loadFaceMesh(): Promise<FaceMeshType | null> {
const { FaceMesh } = await import("@mediapipe/face_mesh");
const fm = new FaceMesh({
locateFile: (file: string) =>
`https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`,
`https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@${MEDIAPIPE_VERSION}/${file}`,
});
fm.setOptions({
maxNumFaces: 1,

View File

@ -1,6 +1,7 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { hexToRgb } from "../../utils/color";
import { getNumberParam, getStringParam } from "../../utils/params";
import fragmentShader from "./blush.frag.glsl";
/** Blush — soft color blend at cheek positions from face landmarks */
@ -19,8 +20,8 @@ export const blushEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams, context }) => ({
u_intensity: (effectParams.intensity as number) / 100,
u_color: hexToRgb(effectParams.color as string),
u_intensity: getNumberParam(effectParams, "intensity") / 100,
u_color: hexToRgb(getStringParam(effectParams, "color", "#ff9999")),
u_cheekLeft: context?.cheekLeft ?? [0.35, 0.55],
u_cheekRight: context?.cheekRight ?? [0.65, 0.55],
u_cheekRadius: context?.cheekRadius ?? 0.08,

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./brightness.frag.glsl";
export const brightnessEffectDefinition: EffectDefinition = {
@ -24,7 +25,7 @@ export const brightnessEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 100,
u_intensity: getNumberParam(effectParams, "intensity") / 100,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./chromatic-aberration.frag.glsl";
export const chromaticAberrationEffectDefinition: EffectDefinition = {
@ -17,8 +18,8 @@ export const chromaticAberrationEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 10,
u_angle: ((effectParams.angle as number) * Math.PI) / 180,
u_intensity: getNumberParam(effectParams, "intensity") / 10,
u_angle: (getNumberParam(effectParams, "angle") * Math.PI) / 180,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./color-balance.frag.glsl";
/** 3-way color grading: shadows, midtones, highlights */
@ -26,19 +27,19 @@ export const colorBalanceEffectDefinition: EffectDefinition = {
fragmentShader,
uniforms: ({ effectParams }) => ({
u_shadows: [
(effectParams.shadowsRed as number) / 200,
(effectParams.shadowsGreen as number) / 200,
(effectParams.shadowsBlue as number) / 200,
getNumberParam(effectParams, "shadowsRed") / 200,
getNumberParam(effectParams, "shadowsGreen") / 200,
getNumberParam(effectParams, "shadowsBlue") / 200,
],
u_midtones: [
(effectParams.midtonesRed as number) / 200,
(effectParams.midtonesGreen as number) / 200,
(effectParams.midtonesBlue as number) / 200,
getNumberParam(effectParams, "midtonesRed") / 200,
getNumberParam(effectParams, "midtonesGreen") / 200,
getNumberParam(effectParams, "midtonesBlue") / 200,
],
u_highlights: [
(effectParams.highlightsRed as number) / 200,
(effectParams.highlightsGreen as number) / 200,
(effectParams.highlightsBlue as number) / 200,
getNumberParam(effectParams, "highlightsRed") / 200,
getNumberParam(effectParams, "highlightsGreen") / 200,
getNumberParam(effectParams, "highlightsBlue") / 200,
],
}),
},

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./contrast.frag.glsl";
export const contrastEffectDefinition: EffectDefinition = {
@ -25,7 +26,7 @@ export const contrastEffectDefinition: EffectDefinition = {
fragmentShader,
uniforms: ({ effectParams }) => ({
// Map -100..100 → 0..2 (0 = no contrast, 1 = normal, 2 = max)
u_contrast: 1 + (effectParams.intensity as number) / 100,
u_contrast: 1 + getNumberParam(effectParams, "intensity") / 100,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./exposure.frag.glsl";
export const exposureEffectDefinition: EffectDefinition = {
@ -24,7 +25,7 @@ export const exposureEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_exposure: effectParams.stops as number,
u_exposure: getNumberParam(effectParams, "stops"),
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./eye-enhance.frag.glsl";
/** Eye enhance — brighten and sharpen eye regions */
@ -18,8 +19,8 @@ export const eyeEnhanceEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_brightness: (effectParams.brightness as number) / 400,
u_contrast: 1 + (effectParams.contrast as number) / 200,
u_brightness: getNumberParam(effectParams, "brightness") / 400,
u_contrast: 1 + getNumberParam(effectParams, "contrast") / 200,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./face-brighten.frag.glsl";
/** Face brighten — soft light blend, optionally masked to face region */
@ -17,7 +18,7 @@ export const faceBrightenEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 200,
u_intensity: getNumberParam(effectParams, "intensity") / 200,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam, getBooleanParam } from "../../utils/params";
import fragmentShader from "./film-grain.frag.glsl";
export const filmGrainEffectDefinition: EffectDefinition = {
@ -18,9 +19,9 @@ export const filmGrainEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams, time }) => ({
u_intensity: (effectParams.intensity as number) / 400,
u_grainSize: effectParams.size as number,
u_time: effectParams.animated ? (time ?? 0) : 0,
u_intensity: getNumberParam(effectParams, "intensity") / 400,
u_grainSize: getNumberParam(effectParams, "size", 2),
u_time: getBooleanParam(effectParams, "animated", true) ? (time ?? 0) : 0,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./gamma.frag.glsl";
export const gammaEffectDefinition: EffectDefinition = {
@ -24,7 +25,7 @@ export const gammaEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_gamma: effectParams.value as number,
u_gamma: getNumberParam(effectParams, "value", 1.0),
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import pass1Shader from "./glitch-pass1.frag.glsl";
import pass2Shader from "./glitch-pass2.frag.glsl";
@ -20,16 +21,16 @@ export const glitchEffectDefinition: EffectDefinition = {
{
fragmentShader: pass1Shader,
uniforms: ({ effectParams, width, time }) => ({
u_intensity: (effectParams.intensity as number) / 1000,
u_colorSplit: (effectParams.colorSplit as number) / (width * 10),
u_time: (time ?? 0) * ((effectParams.speed as number) / 50),
u_intensity: getNumberParam(effectParams, "intensity") / 1000,
u_colorSplit: getNumberParam(effectParams, "colorSplit") / (width * 10),
u_time: (time ?? 0) * (getNumberParam(effectParams, "speed", 50) / 50),
}),
},
{
fragmentShader: pass2Shader,
uniforms: ({ effectParams, time }) => ({
u_intensity: (effectParams.intensity as number) / 100,
u_time: (time ?? 0) * ((effectParams.speed as number) / 50),
u_intensity: getNumberParam(effectParams, "intensity") / 100,
u_time: (time ?? 0) * (getNumberParam(effectParams, "speed", 50) / 50),
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./hue-shift.frag.glsl";
export const hueShiftEffectDefinition: EffectDefinition = {
@ -25,7 +26,7 @@ export const hueShiftEffectDefinition: EffectDefinition = {
fragmentShader,
uniforms: ({ effectParams }) => ({
// Convert degrees to 0..1 range for HSL rotation
u_hueShift: (effectParams.degrees as number) / 360,
u_hueShift: getNumberParam(effectParams, "degrees") / 360,
}),
},
],

View File

@ -1,6 +1,7 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { hexToRgb } from "../../utils/color";
import { getNumberParam, getStringParam } from "../../utils/params";
import pass1Shader from "./neon-glow-pass1.frag.glsl";
import pass2Shader from "./neon-glow-pass2.frag.glsl";
@ -21,14 +22,14 @@ export const neonGlowEffectDefinition: EffectDefinition = {
{
fragmentShader: pass1Shader,
uniforms: ({ effectParams }) => ({
u_threshold: (effectParams.threshold as number) / 100,
u_threshold: getNumberParam(effectParams, "threshold") / 100,
}),
},
{
fragmentShader: pass2Shader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 50,
u_glowColor: hexToRgb(effectParams.color as string),
u_intensity: getNumberParam(effectParams, "intensity") / 50,
u_glowColor: hexToRgb(getStringParam(effectParams, "color", "#00ff88")),
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import pass1Shader from "./oil-paint-pass1.frag.glsl";
import pass2Shader from "./oil-paint-pass2.frag.glsl";
@ -19,13 +20,13 @@ export const oilPaintEffectDefinition: EffectDefinition = {
{
fragmentShader: pass1Shader,
uniforms: ({ effectParams }) => ({
u_levels: effectParams.levels as number,
u_levels: getNumberParam(effectParams, "levels", 8),
}),
},
{
fragmentShader: pass2Shader,
uniforms: ({ effectParams }) => ({
u_radius: effectParams.radius as number,
u_radius: getNumberParam(effectParams, "radius", 4),
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./pixelate.frag.glsl";
export const pixelateEffectDefinition: EffectDefinition = {
@ -16,7 +17,7 @@ export const pixelateEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_blockSize: effectParams.blockSize as number,
u_blockSize: getNumberParam(effectParams, "blockSize", 10),
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./saturation.frag.glsl";
export const saturationEffectDefinition: EffectDefinition = {
@ -25,7 +26,7 @@ export const saturationEffectDefinition: EffectDefinition = {
fragmentShader,
uniforms: ({ effectParams }) => ({
// Map -100..100 → 0..2 (0 = grayscale, 1 = normal, 2 = oversaturated)
u_saturation: 1 + (effectParams.intensity as number) / 100,
u_saturation: 1 + getNumberParam(effectParams, "intensity") / 100,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./sharpen.frag.glsl";
export const sharpenEffectDefinition: EffectDefinition = {
@ -16,7 +17,7 @@ export const sharpenEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 100,
u_intensity: getNumberParam(effectParams, "intensity") / 100,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import pass1Shader from "./sketch-pass1.frag.glsl";
import pass2Shader from "./sketch-pass2.frag.glsl";
@ -19,13 +20,13 @@ export const sketchEffectDefinition: EffectDefinition = {
{
fragmentShader: pass1Shader,
uniforms: ({ effectParams }) => ({
u_lineWidth: effectParams.lineWidth as number,
u_lineWidth: getNumberParam(effectParams, "lineWidth", 1),
}),
},
{
fragmentShader: pass2Shader,
uniforms: ({ effectParams }) => ({
u_threshold: (effectParams.threshold as number) / 100,
u_threshold: getNumberParam(effectParams, "threshold") / 100,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import pass1Shader from "./skin-smooth-pass1.frag.glsl";
import pass2Shader from "./skin-smooth-pass2.frag.glsl";
@ -18,7 +19,7 @@ export const skinSmoothEffectDefinition: EffectDefinition = {
{
fragmentShader: pass1Shader,
uniforms: ({ effectParams }) => ({
u_intensity: ((effectParams.intensity as number) / 100) * 3,
u_intensity: (getNumberParam(effectParams, "intensity") / 100) * 3,
}),
},
{

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./slim-face.frag.glsl";
/** Slim face — pinch warp along jawline for face slimming */
@ -24,7 +25,7 @@ export const slimFaceEffectDefinition: EffectDefinition = {
return {
// No-op when jaw landmarks are missing to avoid warping arbitrary content
u_intensity: jawCenter ? (effectParams.intensity as number) / 100 : 0,
u_intensity: jawCenter ? getNumberParam(effectParams, "intensity") / 100 : 0,
u_jawCenter: jawCenter ?? [0.5, 0.6],
};
},

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./teeth-whiten.frag.glsl";
/** Teeth whiten — desaturate + brighten, ideally masked to mouth region */
@ -17,7 +18,7 @@ export const teethWhitenEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 200,
u_intensity: getNumberParam(effectParams, "intensity") / 200,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./temperature.frag.glsl";
export const temperatureEffectDefinition: EffectDefinition = {
@ -24,7 +25,7 @@ export const temperatureEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_temperature: (effectParams.warmth as number) / 400,
u_temperature: getNumberParam(effectParams, "warmth") / 400,
}),
},
],

View File

@ -1,5 +1,6 @@
import type { EffectDefinition } from "../../types";
import { EffectCategory } from "../../categories";
import { getNumberParam } from "../../utils/params";
import fragmentShader from "./vignette.frag.glsl";
export const vignetteEffectDefinition: EffectDefinition = {
@ -18,9 +19,9 @@ export const vignetteEffectDefinition: EffectDefinition = {
{
fragmentShader,
uniforms: ({ effectParams }) => ({
u_intensity: (effectParams.intensity as number) / 100,
u_radius: (effectParams.radius as number) / 100 * 0.707,
u_softness: (effectParams.softness as number) / 100 * 0.5,
u_intensity: getNumberParam(effectParams, "intensity") / 100,
u_radius: getNumberParam(effectParams, "radius") / 100 * 0.707,
u_softness: getNumberParam(effectParams, "softness") / 100 * 0.5,
}),
},
],

View File

@ -0,0 +1,52 @@
import type { EffectParamValues } from "../types";
/**
* Type-safe parameter getters for effect uniforms.
* These eliminate the need for `as number` / `as string` casts throughout effect definitions.
*/
/** Get a number parameter with fallback default */
export function getNumberParam(
params: EffectParamValues,
key: string,
defaultValue = 0,
): number {
const value = params[key];
if (typeof value === "number") {
return value;
}
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : defaultValue;
}
return defaultValue;
}
/** Get a string parameter with fallback default */
export function getStringParam(
params: EffectParamValues,
key: string,
defaultValue = "",
): string {
const value = params[key];
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return defaultValue;
}
/** Get a boolean parameter with fallback default */
export function getBooleanParam(
params: EffectParamValues,
key: string,
defaultValue = false,
): boolean {
const value = params[key];
if (typeof value === "boolean") {
return value;
}
return defaultValue;
}