diff --git a/apps/web/src/lib/effects/definitions/chromatic-aberration.ts b/apps/web/src/lib/effects/definitions/chromatic-aberration.ts new file mode 100644 index 00000000..6ad85ed1 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/chromatic-aberration.ts @@ -0,0 +1,41 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const CHROMATIC_ABERRATION_SHADER = "chromatic-aberration"; + +export const chromaticAberrationEffectDefinition: EffectDefinition = { + type: "chromatic-aberration", + name: "Chromatic Aberration", + keywords: [ + "chromatic aberration", + "chromatic", + "aberration", + "rgb split", + "fringe", + "prism", + "color fringe", + ], + params: [ + { + key: "offset", + label: "Offset", + type: "number", + default: 15, + min: 0, + max: 50, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: CHROMATIC_ABERRATION_SHADER, + uniforms: ({ effectParams }) => ({ + u_offset: + typeof effectParams.offset === "number" + ? effectParams.offset + : 15, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/color-key.ts b/apps/web/src/lib/effects/definitions/color-key.ts new file mode 100644 index 00000000..d0b9d295 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/color-key.ts @@ -0,0 +1,74 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const COLOR_KEY_SHADER = "color-key"; + +function hexToRgb(hex: string): [number, number, number] { + const clean = hex.replace("#", ""); + return [ + Number.parseInt(clean.substring(0, 2), 16) / 255, + Number.parseInt(clean.substring(2, 4), 16) / 255, + Number.parseInt(clean.substring(4, 6), 16) / 255, + ]; +} + +export const colorKeyEffectDefinition: EffectDefinition = { + type: "color-key", + name: "Color Key", + keywords: [ + "color key", + "colorkey", + "green screen", + "chroma key", + "chromakey", + "remove color", + "transparent", + "background", + ], + params: [ + { + key: "keyColor", + label: "Key Color", + type: "color", + default: "#00ff00", + }, + { + key: "tolerance", + label: "Tolerance", + type: "number", + default: 40, + min: 0, + max: 100, + step: 1, + }, + { + key: "softness", + label: "Softness", + type: "number", + default: 10, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: COLOR_KEY_SHADER, + uniforms: ({ effectParams }) => ({ + u_tolerance: + typeof effectParams.tolerance === "number" + ? effectParams.tolerance + : 40, + u_softness: + typeof effectParams.softness === "number" + ? effectParams.softness + : 10, + u_key_color: + typeof effectParams.keyColor === "string" + ? hexToRgb(effectParams.keyColor) + : hexToRgb("#00ff00"), + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/color-temperature.ts b/apps/web/src/lib/effects/definitions/color-temperature.ts new file mode 100644 index 00000000..77c8b2c8 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/color-temperature.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const COLOR_TEMPERATURE_SHADER = "color-temperature"; + +export const colorTemperatureEffectDefinition: EffectDefinition = { + type: "color-temperature", + name: "Color Temperature", + keywords: ["temperature", "warm", "cool", "white balance", "kelvin"], + params: [ + { + key: "temperature", + label: "Temperature", + type: "number", + default: 0, + min: -100, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: COLOR_TEMPERATURE_SHADER, + uniforms: ({ effectParams }) => ({ + u_temperature: + typeof effectParams.temperature === "number" + ? effectParams.temperature / 100 + : 0, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/cross-process.ts b/apps/web/src/lib/effects/definitions/cross-process.ts new file mode 100644 index 00000000..94fd0e41 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/cross-process.ts @@ -0,0 +1,41 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const CROSS_PROCESS_SHADER = "cross-process"; + +export const crossProcessEffectDefinition: EffectDefinition = { + type: "cross-process", + name: "Cross Process", + keywords: [ + "cross process", + "crossprocess", + "film", + "analog", + "vintage", + "lo-fi", + "lofi", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: CROSS_PROCESS_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 100, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/duotone.ts b/apps/web/src/lib/effects/definitions/duotone.ts new file mode 100644 index 00000000..123a2a89 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/duotone.ts @@ -0,0 +1,73 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const DUOTONE_SHADER = "duotone"; + +function hexToRgb(hex: string): [number, number, number] { + const cleaned = hex.replace("#", ""); + if (cleaned.length === 3) { + const r = parseInt(cleaned[0] + cleaned[0], 16) / 255; + const g = parseInt(cleaned[1] + cleaned[1], 16) / 255; + const b = parseInt(cleaned[2] + cleaned[2], 16) / 255; + return [r, g, b]; + } + const r = parseInt(cleaned.substring(0, 2), 16) / 255; + const g = parseInt(cleaned.substring(2, 4), 16) / 255; + const b = parseInt(cleaned.substring(4, 6), 16) / 255; + return [r, g, b]; +} + +export const duotoneEffectDefinition: EffectDefinition = { + type: "duotone", + name: "Duotone", + keywords: ["duotone", "two tone", "two color", "dual tone"], + params: [ + { + key: "shadowColor", + label: "Shadow Color", + type: "color", + default: "#001428", + }, + { + key: "highlightColor", + label: "Highlight Color", + type: "color", + default: "#ff6b35", + }, + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: DUOTONE_SHADER, + uniforms: ({ effectParams }) => { + const shadowHex = + typeof effectParams.shadowColor === "string" + ? effectParams.shadowColor + : "#001428"; + const highlightHex = + typeof effectParams.highlightColor === "string" + ? effectParams.highlightColor + : "#ff6b35"; + const [sr, sg, sb] = hexToRgb(shadowHex); + const [hr, hg, hb] = hexToRgb(highlightHex); + return { + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 100, + u_shadow_color: [sr, sg, sb], + u_highlight_color: [hr, hg, hb], + }; + }, + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/edge-detection.ts b/apps/web/src/lib/effects/definitions/edge-detection.ts new file mode 100644 index 00000000..381a14c3 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/edge-detection.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const EDGE_DETECTION_SHADER = "edge-detection"; + +export const edgeDetectionEffectDefinition: EffectDefinition = { + type: "edge-detection", + name: "Edge Detection", + keywords: [ + "edge", + "edges", + "detection", + "sobel", + "outline", + "contour", + "border", + "trace", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 100, + min: 0, + max: 100, + step: 1, + }, + { + key: "threshold", + label: "Threshold", + type: "number", + default: 10, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: EDGE_DETECTION_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 100, + u_threshold: + typeof effectParams.threshold === "number" + ? effectParams.threshold + : 10, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/emboss.ts b/apps/web/src/lib/effects/definitions/emboss.ts new file mode 100644 index 00000000..fceb49f4 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/emboss.ts @@ -0,0 +1,54 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const EMBOSS_SHADER = "emboss"; + +export const embossEffectDefinition: EffectDefinition = { + type: "emboss", + name: "Emboss", + keywords: [ + "emboss", + "relief", + "3d", + "raised", + "bump", + "sculpture", + "engrave", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 50, + min: 0, + max: 100, + step: 1, + }, + { + key: "angle", + label: "Angle", + type: "number", + default: 135, + min: 0, + max: 360, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: EMBOSS_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 50, + u_angle: + typeof effectParams.angle === "number" + ? effectParams.angle + : 135, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/exposure.ts b/apps/web/src/lib/effects/definitions/exposure.ts new file mode 100644 index 00000000..b159a463 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/exposure.ts @@ -0,0 +1,42 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const EXPOSURE_SHADER = "exposure"; + +export const exposureEffectDefinition: EffectDefinition = { + type: "exposure", + name: "Exposure", + keywords: [ + "exposure", + "brightness", + "light", + "photo", + "ev", + "camera", + "overexpose", + "underexpose", + ], + params: [ + { + key: "exposure", + label: "Exposure", + type: "number", + default: 0, + min: -3, + max: 3, + step: 0.1, + }, + ], + renderer: { + passes: [ + { + shader: EXPOSURE_SHADER, + uniforms: ({ effectParams }) => ({ + u_exposure: + typeof effectParams.exposure === "number" + ? effectParams.exposure + : 0, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/film-grain.ts b/apps/web/src/lib/effects/definitions/film-grain.ts new file mode 100644 index 00000000..d89a7224 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/film-grain.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const FILM_GRAIN_SHADER = "film-grain"; + +export const filmGrainEffectDefinition: EffectDefinition = { + type: "film-grain", + name: "Film Grain", + keywords: [ + "film grain", + "grain", + "noise", + "film", + "analog", + "dust", + "scratch", + "vintage", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 30, + min: 0, + max: 100, + step: 1, + }, + { + key: "size", + label: "Grain Size", + type: "number", + default: 1, + min: 1, + max: 10, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: FILM_GRAIN_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 30, + u_size: + typeof effectParams.size === "number" + ? effectParams.size + : 1, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/fisheye.ts b/apps/web/src/lib/effects/definitions/fisheye.ts new file mode 100644 index 00000000..579321f8 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/fisheye.ts @@ -0,0 +1,41 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const FISHEYE_SHADER = "fisheye"; + +export const fisheyeEffectDefinition: EffectDefinition = { + type: "fisheye", + name: "Fisheye", + keywords: [ + "fisheye", + "fish eye", + "barrel", + "pincushion", + "lens", + "wide angle", + "distortion", + ], + params: [ + { + key: "strength", + label: "Strength", + type: "number", + default: 50, + min: -100, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: FISHEYE_SHADER, + uniforms: ({ effectParams }) => ({ + u_strength: + typeof effectParams.strength === "number" + ? effectParams.strength + : 50, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/glitch.ts b/apps/web/src/lib/effects/definitions/glitch.ts new file mode 100644 index 00000000..86a748a8 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/glitch.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const GLITCH_SHADER = "glitch"; + +export const glitchEffectDefinition: EffectDefinition = { + type: "glitch", + name: "Glitch", + keywords: [ + "glitch", + "corruption", + "digital", + "error", + "bug", + "distort", + "vhs", + "corrupt", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 50, + min: 0, + max: 100, + step: 1, + }, + { + key: "blockSize", + label: "Block Size", + type: "number", + default: 20, + min: 1, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: GLITCH_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 50, + u_block_size: + typeof effectParams.blockSize === "number" + ? effectParams.blockSize + : 20, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/glow.ts b/apps/web/src/lib/effects/definitions/glow.ts new file mode 100644 index 00000000..38feed84 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/glow.ts @@ -0,0 +1,67 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const GLOW_SHADER = "glow"; + +export const glowEffectDefinition: EffectDefinition = { + type: "glow", + name: "Glow", + keywords: [ + "glow", + "bloom", + "light", + "radiance", + "luminous", + "shine", + "bright glow", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 50, + min: 0, + max: 100, + step: 1, + }, + { + key: "threshold", + label: "Threshold", + type: "number", + default: 70, + min: 0, + max: 100, + step: 1, + }, + { + key: "radius", + label: "Radius", + type: "number", + default: 4, + min: 1, + max: 20, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: GLOW_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 50, + u_threshold: + typeof effectParams.threshold === "number" + ? effectParams.threshold + : 70, + u_radius: + typeof effectParams.radius === "number" + ? effectParams.radius + : 4, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/halftone.ts b/apps/web/src/lib/effects/definitions/halftone.ts new file mode 100644 index 00000000..fce44f44 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/halftone.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const HALFTONE_SHADER = "halftone"; + +export const halftoneEffectDefinition: EffectDefinition = { + type: "halftone", + name: "Halftone", + keywords: [ + "halftone", + "half tone", + "dots", + "print", + "newspaper", + "comic", + "pop art", + "screen", + ], + params: [ + { + key: "dotSize", + label: "Dot Size", + type: "number", + default: 4, + min: 1, + max: 20, + step: 1, + }, + { + key: "angle", + label: "Angle", + type: "number", + default: 45, + min: 0, + max: 360, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: HALFTONE_SHADER, + uniforms: ({ effectParams }) => ({ + u_dot_size: + typeof effectParams.dotSize === "number" + ? effectParams.dotSize + : 4, + u_angle: + typeof effectParams.angle === "number" + ? effectParams.angle + : 45, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/hue-rotate.ts b/apps/web/src/lib/effects/definitions/hue-rotate.ts new file mode 100644 index 00000000..355a7a8d --- /dev/null +++ b/apps/web/src/lib/effects/definitions/hue-rotate.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const HUE_ROTATE_SHADER = "hue-rotate"; + +export const hueRotateEffectDefinition: EffectDefinition = { + type: "hue-rotate", + name: "Hue Rotate", + keywords: ["hue", "rotate", "color shift", "color wheel", "phase"], + params: [ + { + key: "angle", + label: "Angle", + type: "number", + default: 0, + min: 0, + max: 360, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: HUE_ROTATE_SHADER, + uniforms: ({ effectParams }) => ({ + u_angle: + (typeof effectParams.angle === "number" + ? effectParams.angle + : 0) / 360, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/index.ts b/apps/web/src/lib/effects/definitions/index.ts index 7cac8cc8..57a19eeb 100644 --- a/apps/web/src/lib/effects/definitions/index.ts +++ b/apps/web/src/lib/effects/definitions/index.ts @@ -6,6 +6,29 @@ import { invertEffectDefinition } from "./invert"; import { saturationEffectDefinition } from "./saturation"; import { sepiaEffectDefinition } from "./sepia"; import { vignetteEffectDefinition } from "./vignette"; +import { hueRotateEffectDefinition } from "./hue-rotate"; +import { colorTemperatureEffectDefinition } from "./color-temperature"; +import { tintEffectDefinition } from "./tint"; +import { posterizeEffectDefinition } from "./posterize"; +import { duotoneEffectDefinition } from "./duotone"; +import { crossProcessEffectDefinition } from "./cross-process"; +import { pixelateEffectDefinition } from "./pixelate"; +import { chromaticAberrationEffectDefinition } from "./chromatic-aberration"; +import { glitchEffectDefinition } from "./glitch"; +import { waveEffectDefinition } from "./wave"; +import { mirrorEffectDefinition } from "./mirror"; +import { kaleidoscopeEffectDefinition } from "./kaleidoscope"; +import { fisheyeEffectDefinition } from "./fisheye"; +import { sharpenEffectDefinition } from "./sharpen"; +import { glowEffectDefinition } from "./glow"; +import { exposureEffectDefinition } from "./exposure"; +import { shadowsHighlightsEffectDefinition } from "./shadows-highlights"; +import { edgeDetectionEffectDefinition } from "./edge-detection"; +import { embossEffectDefinition } from "./emboss"; +import { filmGrainEffectDefinition } from "./film-grain"; +import { halftoneEffectDefinition } from "./halftone"; +import { scanlinesEffectDefinition } from "./scanlines"; +import { colorKeyEffectDefinition } from "./color-key"; const defaultEffects = [ blurEffectDefinition, @@ -15,6 +38,29 @@ const defaultEffects = [ sepiaEffectDefinition, invertEffectDefinition, vignetteEffectDefinition, + hueRotateEffectDefinition, + colorTemperatureEffectDefinition, + tintEffectDefinition, + posterizeEffectDefinition, + duotoneEffectDefinition, + crossProcessEffectDefinition, + pixelateEffectDefinition, + chromaticAberrationEffectDefinition, + glitchEffectDefinition, + waveEffectDefinition, + mirrorEffectDefinition, + kaleidoscopeEffectDefinition, + fisheyeEffectDefinition, + sharpenEffectDefinition, + glowEffectDefinition, + exposureEffectDefinition, + shadowsHighlightsEffectDefinition, + edgeDetectionEffectDefinition, + embossEffectDefinition, + filmGrainEffectDefinition, + halftoneEffectDefinition, + scanlinesEffectDefinition, + colorKeyEffectDefinition, ]; export function registerDefaultEffects(): void { diff --git a/apps/web/src/lib/effects/definitions/kaleidoscope.ts b/apps/web/src/lib/effects/definitions/kaleidoscope.ts new file mode 100644 index 00000000..254e1fbb --- /dev/null +++ b/apps/web/src/lib/effects/definitions/kaleidoscope.ts @@ -0,0 +1,54 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const KALEIDOSCOPE_SHADER = "kaleidoscope"; + +export const kaleidoscopeEffectDefinition: EffectDefinition = { + type: "kaleidoscope", + name: "Kaleidoscope", + keywords: [ + "kaleidoscope", + "kaleidoscopic", + "radial", + "symmetry", + "pattern", + "mandala", + "segments", + ], + params: [ + { + key: "segments", + label: "Segments", + type: "number", + default: 6, + min: 2, + max: 24, + step: 1, + }, + { + key: "angle", + label: "Angle", + type: "number", + default: 0, + min: 0, + max: 360, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: KALEIDOSCOPE_SHADER, + uniforms: ({ effectParams }) => ({ + u_segments: + typeof effectParams.segments === "number" + ? effectParams.segments + : 6, + u_angle: + typeof effectParams.angle === "number" + ? effectParams.angle + : 0, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/mirror.ts b/apps/web/src/lib/effects/definitions/mirror.ts new file mode 100644 index 00000000..bc98714e --- /dev/null +++ b/apps/web/src/lib/effects/definitions/mirror.ts @@ -0,0 +1,53 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const MIRROR_SHADER = "mirror"; + +export const mirrorEffectDefinition: EffectDefinition = { + type: "mirror", + name: "Mirror", + keywords: [ + "mirror", + "reflect", + "flip", + "symmetry", + "reflection", + "symmetric", + ], + params: [ + { + key: "axis", + label: "Axis", + type: "number", + default: 0, + min: 0, + max: 1, + step: 1, + }, + { + key: "position", + label: "Position", + type: "number", + default: 50, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: MIRROR_SHADER, + uniforms: ({ effectParams }) => ({ + u_axis: + typeof effectParams.axis === "number" + ? effectParams.axis + : 0, + u_position: + typeof effectParams.position === "number" + ? effectParams.position + : 50, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/pixelate.ts b/apps/web/src/lib/effects/definitions/pixelate.ts new file mode 100644 index 00000000..952cbdfe --- /dev/null +++ b/apps/web/src/lib/effects/definitions/pixelate.ts @@ -0,0 +1,41 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const PIXELATE_SHADER = "pixelate"; + +export const pixelateEffectDefinition: EffectDefinition = { + type: "pixelate", + name: "Pixelate", + keywords: [ + "pixelate", + "pixel", + "mosaic", + "mosaic", + "censor", + "blocky", + "pixel art", + ], + params: [ + { + key: "size", + label: "Pixel Size", + type: "number", + default: 10, + min: 1, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: PIXELATE_SHADER, + uniforms: ({ effectParams }) => ({ + u_size: + typeof effectParams.size === "number" + ? effectParams.size + : 10, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/posterize.ts b/apps/web/src/lib/effects/definitions/posterize.ts new file mode 100644 index 00000000..c4023b0d --- /dev/null +++ b/apps/web/src/lib/effects/definitions/posterize.ts @@ -0,0 +1,33 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const POSTERIZE_SHADER = "posterize"; + +export const posterizeEffectDefinition: EffectDefinition = { + type: "posterize", + name: "Posterize", + keywords: ["posterize", "poster", "levels", "reduce colors", "flat"], + params: [ + { + key: "levels", + label: "Levels", + type: "number", + default: 8, + min: 2, + max: 32, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: POSTERIZE_SHADER, + uniforms: ({ effectParams }) => ({ + u_levels: + typeof effectParams.levels === "number" + ? effectParams.levels + : 8, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/scanlines.ts b/apps/web/src/lib/effects/definitions/scanlines.ts new file mode 100644 index 00000000..dcf088b9 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/scanlines.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const SCANLINES_SHADER = "scanlines"; + +export const scanlinesEffectDefinition: EffectDefinition = { + type: "scanlines", + name: "Scanlines", + keywords: [ + "scanlines", + "scan lines", + "crt", + "tv", + "television", + "retro", + "vhs", + "display", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 30, + min: 0, + max: 100, + step: 1, + }, + { + key: "count", + label: "Line Count", + type: "number", + default: 240, + min: 10, + max: 1080, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: SCANLINES_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 30, + u_count: + typeof effectParams.count === "number" + ? effectParams.count + : 240, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/shadows-highlights.ts b/apps/web/src/lib/effects/definitions/shadows-highlights.ts new file mode 100644 index 00000000..554e71a9 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/shadows-highlights.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const SHADOWS_HIGHLIGHTS_SHADER = "shadows-highlights"; + +export const shadowsHighlightsEffectDefinition: EffectDefinition = { + type: "shadows-highlights", + name: "Shadows / Highlights", + keywords: [ + "shadows", + "highlights", + "shadow", + "highlight", + "tone", + "recover", + "fill light", + "contrast", + ], + params: [ + { + key: "shadows", + label: "Shadows", + type: "number", + default: 0, + min: -100, + max: 100, + step: 1, + }, + { + key: "highlights", + label: "Highlights", + type: "number", + default: 0, + min: -100, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: SHADOWS_HIGHLIGHTS_SHADER, + uniforms: ({ effectParams }) => ({ + u_shadows: + typeof effectParams.shadows === "number" + ? effectParams.shadows + : 0, + u_highlights: + typeof effectParams.highlights === "number" + ? effectParams.highlights + : 0, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/sharpen.ts b/apps/web/src/lib/effects/definitions/sharpen.ts new file mode 100644 index 00000000..bfbdc035 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/sharpen.ts @@ -0,0 +1,41 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const SHARPEN_SHADER = "sharpen"; + +export const sharpenEffectDefinition: EffectDefinition = { + type: "sharpen", + name: "Sharpen", + keywords: [ + "sharpen", + "sharp", + "focus", + "detail", + "crisp", + "enhance", + "unsharp mask", + ], + params: [ + { + key: "intensity", + label: "Intensity", + type: "number", + default: 50, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: SHARPEN_SHADER, + uniforms: ({ effectParams }) => ({ + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 50, + }), + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/tint.ts b/apps/web/src/lib/effects/definitions/tint.ts new file mode 100644 index 00000000..392ca717 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/tint.ts @@ -0,0 +1,61 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const TINT_SHADER = "tint"; + +function hexToRgb(hex: string): [number, number, number] { + const cleaned = hex.replace("#", ""); + if (cleaned.length === 3) { + const r = parseInt(cleaned[0] + cleaned[0], 16) / 255; + const g = parseInt(cleaned[1] + cleaned[1], 16) / 255; + const b = parseInt(cleaned[2] + cleaned[2], 16) / 255; + return [r, g, b]; + } + const r = parseInt(cleaned.substring(0, 2), 16) / 255; + const g = parseInt(cleaned.substring(2, 4), 16) / 255; + const b = parseInt(cleaned.substring(4, 6), 16) / 255; + return [r, g, b]; +} + +export const tintEffectDefinition: EffectDefinition = { + type: "tint", + name: "Tint", + keywords: ["tint", "color overlay", "colorize", "wash"], + params: [ + { + key: "color", + label: "Color", + type: "color", + default: "#ff0000", + }, + { + key: "intensity", + label: "Intensity", + type: "number", + default: 50, + min: 0, + max: 100, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: TINT_SHADER, + uniforms: ({ effectParams }) => { + const hex = + typeof effectParams.color === "string" + ? effectParams.color + : "#ff0000"; + const [r, g, b] = hexToRgb(hex); + return { + u_intensity: + typeof effectParams.intensity === "number" + ? effectParams.intensity + : 50, + u_tint_color: [r, g, b], + }; + }, + }, + ], + }, +}; diff --git a/apps/web/src/lib/effects/definitions/wave.ts b/apps/web/src/lib/effects/definitions/wave.ts new file mode 100644 index 00000000..8568fd94 --- /dev/null +++ b/apps/web/src/lib/effects/definitions/wave.ts @@ -0,0 +1,55 @@ +import type { EffectDefinition } from "@/lib/effects/types"; + +export const WAVE_SHADER = "wave"; + +export const waveEffectDefinition: EffectDefinition = { + type: "wave", + name: "Wave", + keywords: [ + "wave", + "warp", + "undulate", + "sinusoidal", + "ripple", + "sine", + "wavy", + "wiggle", + ], + params: [ + { + key: "amplitude", + label: "Amplitude", + type: "number", + default: 30, + min: 0, + max: 100, + step: 1, + }, + { + key: "frequency", + label: "Frequency", + type: "number", + default: 5, + min: 1, + max: 50, + step: 1, + }, + ], + renderer: { + passes: [ + { + shader: WAVE_SHADER, + uniforms: ({ effectParams }) => ({ + u_amplitude: + typeof effectParams.amplitude === "number" + ? effectParams.amplitude + : 30, + u_frequency: + typeof effectParams.frequency === "number" + ? effectParams.frequency + : 5, + }), + }, + ], + }, +}; diff --git a/docs/effects-proposal.md b/docs/effects-proposal.md new file mode 100644 index 00000000..fca74308 --- /dev/null +++ b/docs/effects-proposal.md @@ -0,0 +1,100 @@ +# NeuralCut — Propuesta de Nuevos Efectos + +## Arquitectura del sistema de efectos + +Los efectos usan **WGSL fragment shaders** via wgpu con un buffer de uniforms genérico: + +``` +GenericUniformBuffer { + resolution: vec2f, + scalars: array, // hasta 8 floats + vectors: array, // hasta 8 componentes vectoriales +} +``` + +Pipeline: TypeScript definition → `resolveEffectPasses()` → JSON → Rust/WASM → wgpu shader → fullscreen quad → ping-pong entre passes. + +Para agregar un efecto se tocan exactamente 4 archivos: +1. `rust/crates/effects/src/shaders/.wgsl` +2. `rust/crates/effects/src/pipeline.rs` (SHADER_REGISTRY + pack function) +3. `apps/web/src/lib/effects/definitions/.ts` +4. `apps/web/src/lib/effects/definitions/index.ts` + +--- + +## Efectos actuales (7) + +| Tipo | Nombre | Params | +|------|--------|--------| +| `blur` | Blur | `intensity` (0-100) | +| `brightness-contrast` | Brightness / Contrast | `brightness` (-1 a 1), `contrast` (0-3) | +| `grayscale` | Grayscale | `intensity` (0-100) | +| `saturation` | Saturation | `saturation` (0-300) | +| `sepia` | Sepia | `intensity` (0-100) | +| `invert` | Invert | `intensity` (0-100) | +| `vignette` | Vignette | `radius` (10-100), `softness` (5-80) | + +--- + +## Efectos propuestos + +### Color (single-pass) + +| Efecto | Tipo | Params | Descripción | +|--------|------|--------|-------------| +| **Hue Rotate** | `hue-rotate` | `angle` (0-360) | Rota el hue en espacio HSL | +| **Color Temperature** | `color-temperature` | `temperature` (-100 a 100) | Cálido (naranja) ↔ frío (azul) | +| **Tint** | `tint` | `color` (hex), `intensity` (0-100) | Superpone un tinte de color | +| **Posterize** | `posterize` | `levels` (2-32) | Reduce niveles de color (efecto póster) | +| **Duotone** | `duotone` | `shadowColor`, `highlightColor`, `intensity` | Mapea tonos a dos colores | +| **Cross Process** | `cross-process` | `intensity` (0-100) | Simula revelado cruzado analógico | + +### Distorsión (single o multi-pass) + +| Efecto | Tipo | Params | Descripción | +|--------|------|--------|-------------| +| **Pixelate** | `pixelate` | `size` (1-100) | Agrupa píxeles en bloques | +| **Chromatic Aberration** | `chromatic-aberration` | `offset` (0-50) | Separa canales RGB | +| **Glitch** | `glitch` | `intensity`, `blockSize` | Corrupción digital + RGB split | +| **Wave / Warp** | `wave` | `amplitude`, `frequency` | Distorsión sinusoidal | +| **Mirror** | `mirror` | `axis`, `position` | Refleja la mitad de la imagen | +| **Kaleidoscope** | `kaleidoscope` | `segments`, `angle` | Simetría radial | +| **Fisheye** | `fisheye` | `strength` (-100 a 100) | Distorsión barrel/pincushion | + +### Luz (Glow es multi-pass, el resto single) + +| Efecto | Tipo | Params | Descripción | +|--------|------|--------|-------------| +| **Sharpen** | `sharpen` | `intensity` (0-100) | Convolución de enfoque 3x3 | +| **Glow / Bloom** | `glow` | `intensity`, `threshold`, `radius` | Brillo expandido (2-3 passes) | +| **Exposure** | `exposure` | `exposure` (-3 a 3) | Ajuste fotográfico multiplicativo | +| **Shadows/Highlights** | `shadows-highlights` | `shadows`, `highlights` | Ajuste independiente | + +### Bordes (single-pass, convolución) + +| Efecto | Tipo | Params | Descripción | +|--------|------|--------|-------------| +| **Edge Detection** | `edge-detection` | `intensity`, `threshold` | Detección Sobel | +| **Emboss** | `emboss` | `intensity`, `angle` | Efecto relieve | + +### Estilo (single-pass) + +| Efecto | Tipo | Params | Descripción | +|--------|------|--------|-------------| +| **Film Grain** | `film-grain` | `intensity`, `size` | Ruido fotográfico analógico | +| **Halftone** | `halftone` | `dotSize`, `angle` | Medio tono | +| **Scanlines** | `scanlines` | `intensity`, `count`, `speed` | Líneas CRT | +| **Color Key** | `color-key` | `keyColor`, `tolerance`, `softness` | Elimina color de fondo | + +--- + +## Prioridad recomendada + +1. **Chromatic Aberration** — muy popular, fácil +2. **Hue Rotate** — básico esperado en cualquier editor +3. **Color Temperature** — ajuste fundamental +4. **Pixelate** — simple, útil para censura +5. **Film Grain** — muy pedido +6. **Glow/Bloom** — complejo pero visualmente impactante (3 passes) +7. **Sharpen** — utilidad básica +8. **Posterize** — artístico simple diff --git a/rust/crates/effects/src/pipeline.rs b/rust/crates/effects/src/pipeline.rs index c189a28a..09d9d0a0 100644 --- a/rust/crates/effects/src/pipeline.rs +++ b/rust/crates/effects/src/pipeline.rs @@ -45,6 +45,121 @@ const SHADER_REGISTRY: &[(&str, &str, UniformPacker)] = &[ include_str!("shaders/vignette.wgsl"), pack_vignette as UniformPacker, ), + ( + "hue-rotate", + include_str!("shaders/hue_rotate.wgsl"), + pack_hue_rotate as UniformPacker, + ), + ( + "color-temperature", + include_str!("shaders/color_temperature.wgsl"), + pack_color_temperature as UniformPacker, + ), + ( + "tint", + include_str!("shaders/tint.wgsl"), + pack_tint as UniformPacker, + ), + ( + "posterize", + include_str!("shaders/posterize.wgsl"), + pack_posterize as UniformPacker, + ), + ( + "duotone", + include_str!("shaders/duotone.wgsl"), + pack_duotone as UniformPacker, + ), + ( + "cross-process", + include_str!("shaders/cross_process.wgsl"), + pack_cross_process as UniformPacker, + ), + ( + "pixelate", + include_str!("shaders/pixelate.wgsl"), + pack_pixelate as UniformPacker, + ), + ( + "chromatic-aberration", + include_str!("shaders/chromatic_aberration.wgsl"), + pack_chromatic_aberration as UniformPacker, + ), + ( + "glitch", + include_str!("shaders/glitch.wgsl"), + pack_glitch as UniformPacker, + ), + ( + "wave", + include_str!("shaders/wave.wgsl"), + pack_wave as UniformPacker, + ), + ( + "mirror", + include_str!("shaders/mirror.wgsl"), + pack_mirror as UniformPacker, + ), + ( + "kaleidoscope", + include_str!("shaders/kaleidoscope.wgsl"), + pack_kaleidoscope as UniformPacker, + ), + ( + "fisheye", + include_str!("shaders/fisheye.wgsl"), + pack_fisheye as UniformPacker, + ), + ( + "sharpen", + include_str!("shaders/sharpen.wgsl"), + pack_sharpen as UniformPacker, + ), + ( + "glow", + include_str!("shaders/glow.wgsl"), + pack_glow as UniformPacker, + ), + ( + "exposure", + include_str!("shaders/exposure.wgsl"), + pack_exposure as UniformPacker, + ), + ( + "shadows-highlights", + include_str!("shaders/shadows_highlights.wgsl"), + pack_shadows_highlights as UniformPacker, + ), + ( + "edge-detection", + include_str!("shaders/edge_detection.wgsl"), + pack_edge_detection as UniformPacker, + ), + ( + "emboss", + include_str!("shaders/emboss.wgsl"), + pack_emboss as UniformPacker, + ), + ( + "film-grain", + include_str!("shaders/film_grain.wgsl"), + pack_film_grain as UniformPacker, + ), + ( + "halftone", + include_str!("shaders/halftone.wgsl"), + pack_halftone as UniformPacker, + ), + ( + "scanlines", + include_str!("shaders/scanlines.wgsl"), + pack_scanlines as UniformPacker, + ), + ( + "color-key", + include_str!("shaders/color_key.wgsl"), + pack_color_key as UniformPacker, + ), ]; pub struct ApplyEffectsOptions<'a> { @@ -381,6 +496,30 @@ fn read_vec2_uniform(pass: &EffectPass, uniform: &str) -> Result<[f32; 2], Effec Ok([values[0], values[1]]) } +fn read_vec3_uniform(pass: &EffectPass, uniform: &str) -> Result<[f32; 3], EffectsError> { + let Some(value) = pass.uniforms.get(uniform) else { + return Err(EffectsError::MissingUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + }); + }; + let UniformValue::Vector(values) = value else { + return Err(EffectsError::InvalidVectorUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + expected_length: 3, + }); + }; + if values.len() != 3 { + return Err(EffectsError::InvalidVectorUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + expected_length: 3, + }); + } + Ok([values[0], values[1], values[2]]) +} + fn pack_gaussian_blur( pass: &EffectPass, width: u32, @@ -480,3 +619,344 @@ fn pack_vignette( buf.scalars[0][1] = softness; Ok(buf) } + +fn pack_hue_rotate( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let angle = read_number_uniform_or(pass, "u_angle", 0.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = angle; + Ok(buf) +} + +fn pack_color_temperature( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let temperature = read_number_uniform_or(pass, "u_temperature", 0.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = temperature; + Ok(buf) +} + +fn pack_tint( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 50.0); + let tint_color = read_vec3_uniform(pass, "u_tint_color")?; + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.vectors[0][0] = tint_color[0]; + buf.vectors[0][1] = tint_color[1]; + buf.vectors[0][2] = tint_color[2]; + Ok(buf) +} + +fn pack_posterize( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let levels = read_number_uniform_or(pass, "u_levels", 8.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = levels; + Ok(buf) +} + +fn pack_duotone( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 100.0); + let shadow_color = read_vec3_uniform(pass, "u_shadow_color")?; + let highlight_color = read_vec3_uniform(pass, "u_highlight_color")?; + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.vectors[0][0] = shadow_color[0]; + buf.vectors[0][1] = shadow_color[1]; + buf.vectors[0][2] = shadow_color[2]; + buf.vectors[1][0] = highlight_color[0]; + buf.vectors[1][1] = highlight_color[1]; + buf.vectors[1][2] = highlight_color[2]; + Ok(buf) +} + +fn pack_cross_process( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 100.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + Ok(buf) +} + +fn pack_pixelate( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let size = read_number_uniform_or(pass, "u_size", 10.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = size; + Ok(buf) +} + +fn pack_chromatic_aberration( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let offset = read_number_uniform_or(pass, "u_offset", 15.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = offset; + Ok(buf) +} + +fn pack_glitch( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 50.0); + let block_size = read_number_uniform_or(pass, "u_block_size", 20.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.scalars[0][1] = block_size; + Ok(buf) +} + +fn pack_wave( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let amplitude = read_number_uniform_or(pass, "u_amplitude", 30.0); + let frequency = read_number_uniform_or(pass, "u_frequency", 5.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = amplitude; + buf.scalars[0][1] = frequency; + Ok(buf) +} + +fn pack_mirror( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let axis = read_number_uniform_or(pass, "u_axis", 0.0); + let position = read_number_uniform_or(pass, "u_position", 50.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = axis; + buf.scalars[0][1] = position; + Ok(buf) +} + +fn pack_kaleidoscope( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let segments = read_number_uniform_or(pass, "u_segments", 6.0); + let angle = read_number_uniform_or(pass, "u_angle", 0.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = segments; + buf.scalars[0][1] = angle; + Ok(buf) +} + +fn pack_fisheye( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let strength = read_number_uniform_or(pass, "u_strength", 50.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = strength; + Ok(buf) +} + +fn pack_sharpen( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 50.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + Ok(buf) +} + +fn pack_glow( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 50.0); + let threshold = read_number_uniform_or(pass, "u_threshold", 70.0); + let radius = read_number_uniform_or(pass, "u_radius", 4.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.scalars[0][1] = threshold; + buf.scalars[1][0] = radius; + Ok(buf) +} + +fn pack_exposure( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let exposure = read_number_uniform_or(pass, "u_exposure", 0.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = exposure; + Ok(buf) +} + +fn pack_shadows_highlights( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let shadows = read_number_uniform_or(pass, "u_shadows", 0.0); + let highlights = read_number_uniform_or(pass, "u_highlights", 0.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = shadows; + buf.scalars[0][1] = highlights; + Ok(buf) +} + +fn pack_edge_detection( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 100.0); + let threshold = read_number_uniform_or(pass, "u_threshold", 10.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.scalars[0][1] = threshold; + Ok(buf) +} + +fn pack_emboss( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 50.0); + let angle = read_number_uniform_or(pass, "u_angle", 135.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.scalars[0][1] = angle; + Ok(buf) +} + +fn pack_film_grain( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 30.0); + let size = read_number_uniform_or(pass, "u_size", 1.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.scalars[0][1] = size; + Ok(buf) +} + +fn pack_halftone( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let dot_size = read_number_uniform_or(pass, "u_dot_size", 4.0); + let angle = read_number_uniform_or(pass, "u_angle", 45.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = dot_size; + buf.scalars[0][1] = angle; + Ok(buf) +} + +fn pack_scanlines( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let intensity = read_number_uniform_or(pass, "u_intensity", 30.0); + let count = read_number_uniform_or(pass, "u_count", 240.0); + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = intensity; + buf.scalars[0][1] = count; + Ok(buf) +} + +fn pack_color_key( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result { + let tolerance = read_number_uniform_or(pass, "u_tolerance", 40.0); + let softness = read_number_uniform_or(pass, "u_softness", 10.0); + let key_color = read_vec3_uniform(pass, "u_key_color")?; + + let mut buf = GenericUniformBuffer::default(); + buf.resolution = [width as f32, height as f32]; + buf.scalars[0][0] = tolerance; + buf.scalars[0][1] = softness; + buf.vectors[0][0] = key_color[0]; + buf.vectors[0][1] = key_color[1]; + buf.vectors[0][2] = key_color[2]; + Ok(buf) +} diff --git a/rust/crates/effects/src/shaders/chromatic_aberration.wgsl b/rust/crates/effects/src/shaders/chromatic_aberration.wgsl new file mode 100644 index 00000000..5ab35b56 --- /dev/null +++ b/rust/crates/effects/src/shaders/chromatic_aberration.wgsl @@ -0,0 +1,29 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let offset = uniforms.scalars[0].x / 100.0; + let direction = normalize(input.tex_coord - vec2f(0.5)); + let displacement = direction * offset * 0.02; + + let r = textureSample(input_texture, input_sampler, input.tex_coord + displacement).r; + let g = textureSample(input_texture, input_sampler, input.tex_coord).g; + let b = textureSample(input_texture, input_sampler, input.tex_coord - displacement).b; + let a = textureSample(input_texture, input_sampler, input.tex_coord).a; + + return vec4f(r, g, b, a); +} diff --git a/rust/crates/effects/src/shaders/color_key.wgsl b/rust/crates/effects/src/shaders/color_key.wgsl new file mode 100644 index 00000000..5316c164 --- /dev/null +++ b/rust/crates/effects/src/shaders/color_key.wgsl @@ -0,0 +1,30 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let tolerance = uniforms.scalars[0].x / 100.0; + let softness = uniforms.scalars[0].y / 100.0; + let key_color = uniforms.vectors[0].xyz; + + let diff = distance(color.rgb, key_color); + + let edge = softness + 0.001; + let alpha = 1.0 - smoothstep(tolerance - edge, tolerance + edge, diff); + + return vec4f(color.rgb, color.a * alpha); +} diff --git a/rust/crates/effects/src/shaders/color_temperature.wgsl b/rust/crates/effects/src/shaders/color_temperature.wgsl new file mode 100644 index 00000000..f55434a6 --- /dev/null +++ b/rust/crates/effects/src/shaders/color_temperature.wgsl @@ -0,0 +1,33 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let temperature = uniforms.scalars[0].x; + + let warm = vec3f(1.0, 0.85, 0.65); + let cool = vec3f(0.65, 0.85, 1.0); + let tint = select( + mix(vec3f(0.0), cool, -temperature), + mix(vec3f(0.0), warm, temperature), + temperature >= 0.0 + ); + + let result = color.rgb + tint * 0.3; + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/cross_process.wgsl b/rust/crates/effects/src/shaders/cross_process.wgsl new file mode 100644 index 00000000..0ac2fbc8 --- /dev/null +++ b/rust/crates/effects/src/shaders/cross_process.wgsl @@ -0,0 +1,34 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let intensity = uniforms.scalars[0].x / 100.0; + + let r = min(color.r * 1.2 + 0.05, 1.0); + let g = max(color.g * 0.8 - 0.02, 0.0); + let b = min(color.b * 1.4 + 0.1, 1.0); + + let contrast_r = (r - 0.5) * 1.2 + 0.5; + let contrast_g = (g - 0.5) * 1.1 + 0.5; + let contrast_b = (b - 0.5) * 1.3 + 0.5; + + let cross = vec3f(clamp(contrast_r, 0.0, 1.0), clamp(contrast_g, 0.0, 1.0), clamp(contrast_b, 0.0, 1.0)); + let result = mix(color.rgb, cross, intensity); + + return vec4f(result, color.a); +} diff --git a/rust/crates/effects/src/shaders/duotone.wgsl b/rust/crates/effects/src/shaders/duotone.wgsl new file mode 100644 index 00000000..40a01196 --- /dev/null +++ b/rust/crates/effects/src/shaders/duotone.wgsl @@ -0,0 +1,30 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let intensity = uniforms.scalars[0].x / 100.0; + + let shadow = vec3f(uniforms.vectors[0].x, uniforms.vectors[0].y, uniforms.vectors[0].z); + let highlight = vec3f(uniforms.vectors[1].x, uniforms.vectors[1].y, uniforms.vectors[1].z); + + let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + let duotone = mix(shadow, highlight, luminance); + let result = mix(color.rgb, duotone, intensity); + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/edge_detection.wgsl b/rust/crates/effects/src/shaders/edge_detection.wgsl new file mode 100644 index 00000000..b97d8a3c --- /dev/null +++ b/rust/crates/effects/src/shaders/edge_detection.wgsl @@ -0,0 +1,47 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +fn luminance(color: vec3f) -> f32 { + return dot(color, vec3f(0.2126, 0.7152, 0.0722)); +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let intensity = uniforms.scalars[0].x / 100.0; + let threshold = uniforms.scalars[0].y / 100.0; + let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; + + let tl = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f(-texel_size.x, -texel_size.y)).rgb); + let t = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f( 0.0, -texel_size.y)).rgb); + let tr = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f( texel_size.x, -texel_size.y)).rgb); + let l = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f(-texel_size.x, 0.0)).rgb); + let r = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f( texel_size.x, 0.0)).rgb); + let bl = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f(-texel_size.x, texel_size.y)).rgb); + let b = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f( 0.0, texel_size.y)).rgb); + let br = luminance(textureSample(input_texture, input_sampler, input.tex_coord + vec2f( texel_size.x, texel_size.y)).rgb); + + let gx = -tl - 2.0 * l - bl + tr + 2.0 * r + br; + let gy = -tl - 2.0 * t - tr + bl + 2.0 * b + br; + + let edge = sqrt(gx * gx + gy * gy); + + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let edge_mask = smoothstep(threshold, threshold + 0.1, edge); + + let result = mix(color.rgb, vec3f(edge_mask), intensity); + + return vec4f(result, color.a); +} diff --git a/rust/crates/effects/src/shaders/emboss.wgsl b/rust/crates/effects/src/shaders/emboss.wgsl new file mode 100644 index 00000000..1a9d4a66 --- /dev/null +++ b/rust/crates/effects/src/shaders/emboss.wgsl @@ -0,0 +1,33 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let intensity = uniforms.scalars[0].x / 100.0; + let angle = uniforms.scalars[0].y * 3.141592653589793 / 180.0; + let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; + + let dx = cos(angle) * texel_size.x; + let dy = sin(angle) * texel_size.y; + + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let back = textureSample(input_texture, input_sampler, input.tex_coord + vec2f(-dx, -dy)); + let front = textureSample(input_texture, input_sampler, input.tex_coord + vec2f( dx, dy)); + + let emboss = (front.rgb - back.rgb + 0.5) * intensity + color.rgb * (1.0 - intensity); + + return vec4f(clamp(emboss, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/exposure.wgsl b/rust/crates/effects/src/shaders/exposure.wgsl new file mode 100644 index 00000000..c8c95646 --- /dev/null +++ b/rust/crates/effects/src/shaders/exposure.wgsl @@ -0,0 +1,25 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let exposure = uniforms.scalars[0].x; + + let result = color.rgb * pow(2.0, exposure); + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/film_grain.wgsl b/rust/crates/effects/src/shaders/film_grain.wgsl new file mode 100644 index 00000000..eea36772 --- /dev/null +++ b/rust/crates/effects/src/shaders/film_grain.wgsl @@ -0,0 +1,38 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +fn hash21(p: vec2f) -> f32 { + var p3 = fract(vec3f(p.x, p.y, p.x) * vec3f(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, vec3f(p3.y, p3.z, p3.x) + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let intensity = uniforms.scalars[0].x / 100.0; + let size = max(uniforms.scalars[0].y, 1.0); + + let color = textureSample(input_texture, input_sampler, input.tex_coord); + + let pixel_pos = input.tex_coord * uniforms.resolution; + let grain_uv = floor(pixel_pos / size); + let noise = hash21(grain_uv) * 2.0 - 1.0; + + let grain_amount = intensity * 0.3; + let result = color.rgb + vec3f(noise * grain_amount); + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/fisheye.wgsl b/rust/crates/effects/src/shaders/fisheye.wgsl new file mode 100644 index 00000000..aeb4a87c --- /dev/null +++ b/rust/crates/effects/src/shaders/fisheye.wgsl @@ -0,0 +1,46 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let strength = uniforms.scalars[0].x / 100.0; + + let centered = input.tex_coord - vec2f(0.5); + let r = length(centered); + + if (r < 0.001) { + return textureSample(input_texture, input_sampler, input.tex_coord); + } + + let normalized_r = r * 2.0; + + var distorted_r: f32; + if (strength >= 0.0) { + distorted_r = pow(normalized_r, 1.0 + strength * 1.5) * 0.5; + } else { + let abs_s = abs(strength); + distorted_r = pow(normalized_r, 1.0 / (1.0 + abs_s * 1.5)) * 0.5; + } + + let theta = atan2(centered.y, centered.x); + let distorted_uv = vec2f(0.5) + vec2f(cos(theta), sin(theta)) * distorted_r; + + if (distorted_uv.x < 0.0 || distorted_uv.x > 1.0 || distorted_uv.y < 0.0 || distorted_uv.y > 1.0) { + return vec4f(0.0, 0.0, 0.0, 1.0); + } + + return textureSample(input_texture, input_sampler, distorted_uv); +} diff --git a/rust/crates/effects/src/shaders/glitch.wgsl b/rust/crates/effects/src/shaders/glitch.wgsl new file mode 100644 index 00000000..8764902d --- /dev/null +++ b/rust/crates/effects/src/shaders/glitch.wgsl @@ -0,0 +1,46 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +fn hash21(p: vec2f) -> f32 { + var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031); + p3 = p3 + dot(p3, vec3f(p3.y, p3.z, p3.x) + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let intensity = uniforms.scalars[0].x / 100.0; + let block_size = max(uniforms.scalars[0].y, 1.0); + + let block_uv = floor(input.tex_coord * (uniforms.resolution / block_size)); + let block_hash = hash21(block_uv); + + var uv = input.tex_coord; + + let shift_threshold = 1.0 - intensity * 0.8; + if (block_hash > shift_threshold) { + let shift = (hash21(block_uv + vec2f(42.0, 0.0)) - 0.5) * intensity * 0.15; + uv = vec2f(uv.x + shift, uv.y); + } + + let rgb_shift = intensity * 0.008; + let r = textureSample(input_texture, input_sampler, vec2f(uv.x + rgb_shift, uv.y)).r; + let g = textureSample(input_texture, input_sampler, uv).g; + let b = textureSample(input_texture, input_sampler, vec2f(uv.x - rgb_shift, uv.y)).b; + let a = textureSample(input_texture, input_sampler, uv).a; + + return vec4f(r, g, b, a); +} diff --git a/rust/crates/effects/src/shaders/glow.wgsl b/rust/crates/effects/src/shaders/glow.wgsl new file mode 100644 index 00000000..0bd87fe8 --- /dev/null +++ b/rust/crates/effects/src/shaders/glow.wgsl @@ -0,0 +1,52 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; + let intensity = uniforms.scalars[0].x / 100.0; + let threshold = uniforms.scalars[0].y / 100.0; + let radius = uniforms.scalars[1].x; + + let center = textureSample(input_texture, input_sampler, input.tex_coord); + let luminance = dot(center.rgb, vec3f(0.2126, 0.7152, 0.0722)); + + if (luminance < threshold) { + return center; + } + + let samples = 16.0; + var bloom = vec3f(0.0); + let step = radius / samples; + + for (var i = -samples; i <= samples; i = i + 1.0) { + for (var j = -samples; j <= samples; j = j + 1.0) { + let offset = vec2f(i, j) * step * texel_size; + let sample_color = textureSample(input_texture, input_sampler, input.tex_coord + offset); + let sample_lum = dot(sample_color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + if (sample_lum >= threshold) { + bloom = bloom + sample_color.rgb; + } + } + } + + let total = (samples * 2.0 + 1.0) * (samples * 2.0 + 1.0); + bloom = bloom / total; + + let result = mix(center.rgb, center.rgb + bloom * intensity, intensity); + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), center.a); +} diff --git a/rust/crates/effects/src/shaders/halftone.wgsl b/rust/crates/effects/src/shaders/halftone.wgsl new file mode 100644 index 00000000..bf3f0c80 --- /dev/null +++ b/rust/crates/effects/src/shaders/halftone.wgsl @@ -0,0 +1,64 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +const PI: f32 = 3.141592653589793; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let dot_size = max(uniforms.scalars[0].x, 1.0); + let angle = uniforms.scalars[0].y * PI / 180.0; + + let aspect = uniforms.resolution.x / uniforms.resolution.y; + let grid_uv = vec2f( + input.tex_coord.x * aspect, + input.tex_coord.y, + ); + + let rotated_x = grid_uv.x * cos(angle) - grid_uv.y * sin(angle); + let rotated_y = grid_uv.x * sin(angle) + grid_uv.y * cos(angle); + + let grid_spacing = dot_size * 2.0 / uniforms.resolution.y; + let cell = vec2f( + floor(rotated_x / grid_spacing), + floor(rotated_y / grid_spacing), + ); + + let cell_center = (cell + vec2f(0.5)) * grid_spacing; + + let inv_cos = cos(-angle); + let inv_sin = sin(-angle); + let sample_uv = vec2f( + cell_center.x * inv_cos - cell_center.y * inv_sin, + cell_center.x * inv_sin + cell_center.y * inv_cos, + ); + + let sample_coord = vec2f(sample_uv.x / aspect, sample_uv.y); + + if (sample_coord.x < 0.0 || sample_coord.x > 1.0 || sample_coord.y < 0.0 || sample_coord.y > 1.0) { + return vec4f(0.0, 0.0, 0.0, 1.0); + } + + let color = textureSample(input_texture, input_sampler, sample_coord); + let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + + let dist = length(vec2f(rotated_x, rotated_y) - cell_center); + let max_radius = grid_spacing * 0.5; + let dot_radius = max_radius * (1.0 - luminance); + + let mask = 1.0 - smoothstep(dot_radius - 0.5, dot_radius + 0.5, dist); + + return vec4f(color.rgb * mask, color.a); +} diff --git a/rust/crates/effects/src/shaders/hue_rotate.wgsl b/rust/crates/effects/src/shaders/hue_rotate.wgsl new file mode 100644 index 00000000..dbad787c --- /dev/null +++ b/rust/crates/effects/src/shaders/hue_rotate.wgsl @@ -0,0 +1,77 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +fn rgb_to_hsl(c: vec3f) -> vec3f { + let max_c = max(max(c.r, c.g), c.b); + let min_c = min(min(c.r, c.g), c.b); + let l = (max_c + min_c) * 0.5; + var h = 0.0; + var s = 0.0; + if (max_c != min_c) { + let d = max_c - min_c; + if (l > 0.5) { + s = d / (2.0 - max_c - min_c); + } else { + s = d / (max_c + min_c); + } + if (max_c == c.r) { + h = (c.g - c.b) / d + select(6.0, 0.0, c.g >= c.b); + } else if (max_c == c.g) { + h = (c.b - c.r) / d + 2.0; + } else { + h = (c.r - c.g) / d + 4.0; + } + h = h / 6.0; + } + return vec3f(h, s, l); +} + +fn hsl_to_rgb(hsl: vec3f) -> vec3f { + if (hsl.y == 0.0) { + return vec3f(hsl.z); + } + let h = hsl.x; + let s = hsl.y; + let l = hsl.z; + let q = select(l * (1.0 + s), l + s - l * s, l < 0.5); + let p = 2.0 * l - q; + let r = hue_to_rgb(p, q, h + 1.0 / 3.0); + let g = hue_to_rgb(p, q, h); + let b = hue_to_rgb(p, q, h - 1.0 / 3.0); + return vec3f(r, g, b); +} + +fn hue_to_rgb(p: f32, q: f32, t_in: f32) -> f32 { + var t = t_in; + if (t < 0.0) { t += 1.0; } + if (t > 1.0) { t -= 1.0; } + if (t < 1.0 / 6.0) { return p + (q - p) * 6.0 * t; } + if (t < 1.0 / 2.0) { return q; } + if (t < 2.0 / 3.0) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; } + return p; +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let angle = uniforms.scalars[0].x; + + let hsl = rgb_to_hsl(color.rgb); + let rotated_h = fract(hsl.x + angle); + let rotated = hsl_to_rgb(vec3f(rotated_h, hsl.y, hsl.z)); + + return vec4f(rotated, color.a); +} diff --git a/rust/crates/effects/src/shaders/kaleidoscope.wgsl b/rust/crates/effects/src/shaders/kaleidoscope.wgsl new file mode 100644 index 00000000..b8fd7739 --- /dev/null +++ b/rust/crates/effects/src/shaders/kaleidoscope.wgsl @@ -0,0 +1,38 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +const PI: f32 = 3.141592653589793; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let segments = max(uniforms.scalars[0].x, 2.0); + let angle_offset = uniforms.scalars[0].y * PI / 180.0; + + let centered = input.tex_coord - vec2f(0.5); + let r = length(centered); + var theta = atan2(centered.y, centered.x) + angle_offset; + + let segment_angle = 2.0 * PI / segments; + theta = abs(mod(theta, segment_angle) - segment_angle * 0.5); + + let uv = vec2f(0.5) + vec2f(cos(theta), sin(theta)) * r; + + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { + return vec4f(0.0, 0.0, 0.0, 1.0); + } + + return textureSample(input_texture, input_sampler, uv); +} diff --git a/rust/crates/effects/src/shaders/mirror.wgsl b/rust/crates/effects/src/shaders/mirror.wgsl new file mode 100644 index 00000000..71e2302f --- /dev/null +++ b/rust/crates/effects/src/shaders/mirror.wgsl @@ -0,0 +1,37 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let axis = uniforms.scalars[0].x; + let position = uniforms.scalars[0].y / 100.0; + + var uv = input.tex_coord; + + if (axis < 0.5) { + if (uv.x > position) { + uv.x = position - (uv.x - position); + } + } else { + if (uv.y > position) { + uv.y = position - (uv.y - position); + } + } + + uv = clamp(uv, vec2f(0.0), vec2f(1.0)); + + return textureSample(input_texture, input_sampler, uv); +} diff --git a/rust/crates/effects/src/shaders/pixelate.wgsl b/rust/crates/effects/src/shaders/pixelate.wgsl new file mode 100644 index 00000000..8e32676a --- /dev/null +++ b/rust/crates/effects/src/shaders/pixelate.wgsl @@ -0,0 +1,31 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let pixel_size = max(uniforms.scalars[0].x, 1.0); + let aspect = uniforms.resolution.x / uniforms.resolution.y; + + let cell_x = pixel_size / uniforms.resolution.x; + let cell_y = pixel_size / uniforms.resolution.y; + + let snapped_x = floor(input.tex_coord.x / cell_x) * cell_x + cell_x * 0.5; + let snapped_y = floor(input.tex_coord.y / cell_y) * cell_y + cell_y * 0.5; + + let snapped_uv = vec2f(clamp(snapped_x, 0.0, 1.0), clamp(snapped_y, 0.0, 1.0)); + + return textureSample(input_texture, input_sampler, snapped_uv); +} diff --git a/rust/crates/effects/src/shaders/posterize.wgsl b/rust/crates/effects/src/shaders/posterize.wgsl new file mode 100644 index 00000000..72aaf5d2 --- /dev/null +++ b/rust/crates/effects/src/shaders/posterize.wgsl @@ -0,0 +1,32 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let levels = uniforms.scalars[0].x; + + if (levels < 2.0) { + return color; + } + + let step = 1.0 / (levels - 1.0); + let r = round(color.r * (levels - 1.0)) * step; + let g = round(color.g * (levels - 1.0)) * step; + let b = round(color.b * (levels - 1.0)) * step; + + return vec4f(r, g, b, color.a); +} diff --git a/rust/crates/effects/src/shaders/scanlines.wgsl b/rust/crates/effects/src/shaders/scanlines.wgsl new file mode 100644 index 00000000..0b3006aa --- /dev/null +++ b/rust/crates/effects/src/shaders/scanlines.wgsl @@ -0,0 +1,30 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let intensity = uniforms.scalars[0].x / 100.0; + let count = max(uniforms.scalars[0].y, 1.0); + + let color = textureSample(input_texture, input_sampler, input.tex_coord); + + let line_y = input.tex_coord.y * count; + let line = smoothstep(0.4, 0.5, fract(line_y)); + + let darkness = 1.0 - line * intensity; + + return vec4f(color.rgb * darkness, color.a); +} diff --git a/rust/crates/effects/src/shaders/shadows_highlights.wgsl b/rust/crates/effects/src/shaders/shadows_highlights.wgsl new file mode 100644 index 00000000..ece93370 --- /dev/null +++ b/rust/crates/effects/src/shaders/shadows_highlights.wgsl @@ -0,0 +1,35 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let shadows_adj = uniforms.scalars[0].x / 100.0; + let highlights_adj = uniforms.scalars[0].y / 100.0; + + let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + + let shadow_weight = 1.0 - smoothstep(0.0, 0.5, luminance); + let highlight_weight = smoothstep(0.5, 1.0, luminance); + + let shadow_correction = shadow_weight * shadows_adj * 0.5; + let highlight_correction = highlight_weight * highlights_adj * 0.5; + + let shadow_result = color.rgb + shadow_correction; + let highlight_result = shadow_result - highlight_correction; + + return vec4f(clamp(highlight_result, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/sharpen.wgsl b/rust/crates/effects/src/shaders/sharpen.wgsl new file mode 100644 index 00000000..28d0ea1c --- /dev/null +++ b/rust/crates/effects/src/shaders/sharpen.wgsl @@ -0,0 +1,34 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let intensity = uniforms.scalars[0].x / 100.0; + let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; + + let center = textureSample(input_texture, input_sampler, input.tex_coord); + let top = textureSample(input_texture, input_sampler, input.tex_coord + vec2f(0.0, -texel_size.y)); + let bottom = textureSample(input_texture, input_sampler, input.tex_coord + vec2f(0.0, texel_size.y)); + let left = textureSample(input_texture, input_sampler, input.tex_coord + vec2f(-texel_size.x, 0.0)); + let right = textureSample(input_texture, input_sampler, input.tex_coord + vec2f( texel_size.x, 0.0)); + + let neighbors = top.rgb + bottom.rgb + left.rgb + right.rgb; + let sharpened = center.rgb * 5.0 - neighbors; + + let result = mix(center.rgb, sharpened, intensity); + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), center.a); +} diff --git a/rust/crates/effects/src/shaders/tint.wgsl b/rust/crates/effects/src/shaders/tint.wgsl new file mode 100644 index 00000000..ab832f27 --- /dev/null +++ b/rust/crates/effects/src/shaders/tint.wgsl @@ -0,0 +1,31 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let color = textureSample(input_texture, input_sampler, input.tex_coord); + let intensity = uniforms.scalars[0].x / 100.0; + let tint_r = uniforms.vectors[0].x; + let tint_g = uniforms.vectors[0].y; + let tint_b = uniforms.vectors[0].z; + + let tint_color = vec3f(tint_r, tint_g, tint_b); + let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722)); + let tinted = mix(vec3f(luminance), tint_color, 0.5); + let result = mix(color.rgb, tinted, intensity); + + return vec4f(clamp(result, vec3f(0.0), vec3f(1.0)), color.a); +} diff --git a/rust/crates/effects/src/shaders/wave.wgsl b/rust/crates/effects/src/shaders/wave.wgsl new file mode 100644 index 00000000..bbcf01e7 --- /dev/null +++ b/rust/crates/effects/src/shaders/wave.wgsl @@ -0,0 +1,31 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + _pad_res: vec2f, + scalars: array, + vectors: array, +} + +@group(0) @binding(0) var input_texture: texture_2d; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let amplitude = uniforms.scalars[0].x / 100.0; + let frequency = uniforms.scalars[0].y; + + let wave_x = sin(input.tex_coord.y * frequency * 6.28318530718) * amplitude * 0.02; + let wave_y = cos(input.tex_coord.x * frequency * 6.28318530718) * amplitude * 0.02; + + let distorted_uv = vec2f( + clamp(input.tex_coord.x + wave_x, 0.0, 1.0), + clamp(input.tex_coord.y + wave_y, 0.0, 1.0), + ); + + return textureSample(input_texture, input_sampler, distorted_uv); +}