feat: add saturation, sepia, invert, and vignette effects
This commit is contained in:
parent
ab9119f163
commit
a8297c6eae
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -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 };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -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 };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -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 };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue