feat: add saturation, sepia, invert, and vignette effects

This commit is contained in:
Luis Esteban Acevedo Ladino 2026-04-26 15:48:15 -05:00
parent ab9119f163
commit a8297c6eae
10 changed files with 342 additions and 0 deletions

View File

@ -2,11 +2,19 @@ import { effectsRegistry } from "../registry";
import { blurEffectDefinition } from "./blur";
import { brightnessContrastEffectDefinition } from "./brightness-contrast";
import { grayscaleEffectDefinition } from "./grayscale";
import { invertEffectDefinition } from "./invert";
import { saturationEffectDefinition } from "./saturation";
import { sepiaEffectDefinition } from "./sepia";
import { vignetteEffectDefinition } from "./vignette";
const defaultEffects = [
blurEffectDefinition,
brightnessContrastEffectDefinition,
grayscaleEffectDefinition,
saturationEffectDefinition,
sepiaEffectDefinition,
invertEffectDefinition,
vignetteEffectDefinition,
];
export function registerDefaultEffects(): void {

View File

@ -0,0 +1,33 @@
import type { EffectDefinition } from "@/lib/effects/types";
export const INVERT_SHADER = "invert";
export const invertEffectDefinition: EffectDefinition = {
type: "invert",
name: "Invert",
keywords: ["invert", "negative", "reverse", "opposite"],
params: [
{
key: "intensity",
label: "Intensity",
type: "number",
default: 100,
min: 0,
max: 100,
step: 1,
},
],
renderer: {
passes: [
{
shader: INVERT_SHADER,
uniforms: ({ effectParams }) => {
const raw = effectParams.intensity;
const intensity =
typeof raw === "number" ? raw / 100 : 1;
return { u_intensity: intensity };
},
},
],
},
};

View File

@ -0,0 +1,33 @@
import type { EffectDefinition } from "@/lib/effects/types";
export const SATURATION_SHADER = "saturation";
export const saturationEffectDefinition: EffectDefinition = {
type: "saturation",
name: "Saturation",
keywords: ["saturation", "vibrance", "vivid", "color", "desaturate"],
params: [
{
key: "saturation",
label: "Saturation",
type: "number",
default: 100,
min: 0,
max: 300,
step: 1,
},
],
renderer: {
passes: [
{
shader: SATURATION_SHADER,
uniforms: ({ effectParams }) => {
const raw = effectParams.saturation;
const saturation =
typeof raw === "number" ? raw / 100 : 1;
return { u_saturation: saturation };
},
},
],
},
};

View File

@ -0,0 +1,33 @@
import type { EffectDefinition } from "@/lib/effects/types";
export const SEPIA_SHADER = "sepia";
export const sepiaEffectDefinition: EffectDefinition = {
type: "sepia",
name: "Sepia",
keywords: ["sepia", "vintage", "warm", "old", "retro", "antique"],
params: [
{
key: "intensity",
label: "Intensity",
type: "number",
default: 100,
min: 0,
max: 100,
step: 1,
},
],
renderer: {
passes: [
{
shader: SEPIA_SHADER,
uniforms: ({ effectParams }) => {
const raw = effectParams.intensity;
const intensity =
typeof raw === "number" ? raw / 100 : 1;
return { u_intensity: intensity };
},
},
],
},
};

View File

@ -0,0 +1,48 @@
import type { EffectDefinition } from "@/lib/effects/types";
export const VIGNETTE_SHADER = "vignette";
export const vignetteEffectDefinition: EffectDefinition = {
type: "vignette",
name: "Vignette",
keywords: ["vignette", "dark edges", "focus", "spotlight", "center"],
params: [
{
key: "radius",
label: "Radius",
type: "number",
default: 75,
min: 10,
max: 100,
step: 1,
},
{
key: "softness",
label: "Softness",
type: "number",
default: 35,
min: 5,
max: 80,
step: 1,
},
],
renderer: {
passes: [
{
shader: VIGNETTE_SHADER,
uniforms: ({ effectParams }) => {
const rawRadius = effectParams.radius;
const rawSoftness = effectParams.softness;
const radius =
(typeof rawRadius === "number" ? rawRadius : 75) /
100;
const softness =
(typeof rawSoftness === "number"
? rawSoftness
: 35) / 100;
return { u_radius: radius, u_softness: softness };
},
},
],
},
};

View File

@ -25,6 +25,26 @@ const SHADER_REGISTRY: &[(&str, &str, UniformPacker)] = &[
include_str!("shaders/grayscale.wgsl"),
pack_grayscale as UniformPacker,
),
(
"saturation",
include_str!("shaders/saturation.wgsl"),
pack_saturation as UniformPacker,
),
(
"sepia",
include_str!("shaders/sepia.wgsl"),
pack_sepia as UniformPacker,
),
(
"invert",
include_str!("shaders/invert.wgsl"),
pack_invert as UniformPacker,
),
(
"vignette",
include_str!("shaders/vignette.wgsl"),
pack_vignette as UniformPacker,
),
];
pub struct ApplyEffectsOptions<'a> {
@ -406,3 +426,57 @@ fn pack_grayscale(
buf.scalars[0][0] = intensity;
Ok(buf)
}
fn pack_saturation(
pass: &EffectPass,
width: u32,
height: u32,
) -> Result<GenericUniformBuffer, EffectsError> {
let saturation = read_number_uniform_or(pass, "u_saturation", 1.0);
let mut buf = GenericUniformBuffer::default();
buf.resolution = [width as f32, height as f32];
buf.scalars[0][0] = saturation;
Ok(buf)
}
fn pack_sepia(
pass: &EffectPass,
width: u32,
height: u32,
) -> Result<GenericUniformBuffer, EffectsError> {
let intensity = read_number_uniform_or(pass, "u_intensity", 1.0);
let mut buf = GenericUniformBuffer::default();
buf.resolution = [width as f32, height as f32];
buf.scalars[0][0] = intensity;
Ok(buf)
}
fn pack_invert(
pass: &EffectPass,
width: u32,
height: u32,
) -> Result<GenericUniformBuffer, EffectsError> {
let intensity = read_number_uniform_or(pass, "u_intensity", 1.0);
let mut buf = GenericUniformBuffer::default();
buf.resolution = [width as f32, height as f32];
buf.scalars[0][0] = intensity;
Ok(buf)
}
fn pack_vignette(
pass: &EffectPass,
width: u32,
height: u32,
) -> Result<GenericUniformBuffer, EffectsError> {
let radius = read_number_uniform_or(pass, "u_radius", 0.75);
let softness = read_number_uniform_or(pass, "u_softness", 0.35);
let mut buf = GenericUniformBuffer::default();
buf.resolution = [width as f32, height as f32];
buf.scalars[0][0] = radius;
buf.scalars[0][1] = softness;
Ok(buf)
}

View File

@ -0,0 +1,26 @@
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) tex_coord: vec2f,
}
struct EffectUniforms {
resolution: vec2f,
_pad_res: vec2f,
scalars: array<vec4f, 2>,
vectors: array<vec4f, 2>,
}
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;
@group(1) @binding(0) var<uniform> 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;
let inverted = vec3f(1.0 - color.r, 1.0 - color.g, 1.0 - color.b);
let result = mix(color.rgb, inverted, intensity);
return vec4f(result, color.a);
}

View File

@ -0,0 +1,28 @@
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) tex_coord: vec2f,
}
struct EffectUniforms {
resolution: vec2f,
_pad_res: vec2f,
scalars: array<vec4f, 2>,
vectors: array<vec4f, 2>,
}
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;
@group(1) @binding(0) var<uniform> uniforms: EffectUniforms;
@fragment
fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
let color = textureSample(input_texture, input_sampler, input.tex_coord);
let saturation = uniforms.scalars[0].x;
let luminance = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722));
let gray = vec3f(luminance);
let result = mix(gray, color.rgb, saturation);
return vec4f(result, color.a);
}

View File

@ -0,0 +1,30 @@
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) tex_coord: vec2f,
}
struct EffectUniforms {
resolution: vec2f,
_pad_res: vec2f,
scalars: array<vec4f, 2>,
vectors: array<vec4f, 2>,
}
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;
@group(1) @binding(0) var<uniform> 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;
let r = min(color.r * 0.393 + color.g * 0.769 + color.b * 0.189, 1.0);
let g = min(color.r * 0.349 + color.g * 0.686 + color.b * 0.168, 1.0);
let b = min(color.r * 0.272 + color.g * 0.534 + color.b * 0.131, 1.0);
let sepia = vec3f(r, g, b);
let result = mix(color.rgb, sepia, intensity);
return vec4f(result, color.a);
}

View File

@ -0,0 +1,29 @@
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) tex_coord: vec2f,
}
struct EffectUniforms {
resolution: vec2f,
_pad_res: vec2f,
scalars: array<vec4f, 2>,
vectors: array<vec4f, 2>,
}
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var input_sampler: sampler;
@group(1) @binding(0) var<uniform> uniforms: EffectUniforms;
@fragment
fn fragment_main(input: VertexOutput) -> @location(0) vec4f {
let color = textureSample(input_texture, input_sampler, input.tex_coord);
let radius = uniforms.scalars[0].x;
let softness = uniforms.scalars[0].y;
let center = input.tex_coord - vec2f(0.5);
let dist = length(center);
let edge = smoothstep(radius, radius - softness, dist);
return vec4f(color.rgb * edge, color.a);
}