feat: add 23 new video effects across 5 categories

Implement color (hue rotate, temperature, tint, posterize, duotone, cross process),
distortion (pixelate, chromatic aberration, glitch, wave, mirror, kaleidoscope, fisheye),
light (sharpen, glow, exposure, shadows/highlights), edge (detection, emboss), and
style (film grain, halftone, scanlines, color key) effects as WGSL shaders with Rust
pack functions and TypeScript definitions.
This commit is contained in:
Luis Esteban Acevedo Ladino 2026-04-27 09:12:06 -05:00
parent 96eeb2901f
commit 1daba0e9ea
49 changed files with 2676 additions and 0 deletions

View File

@ -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,
}),
},
],
},
};

View File

@ -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"),
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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],
};
},
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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 {

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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,
}),
},
],
},
};

View File

@ -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],
};
},
},
],
},
};

View File

@ -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,
}),
},
],
},
};

100
docs/effects-proposal.md Normal file
View File

@ -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<vec4f, 2>, // hasta 8 floats
vectors: array<vec4f, 2>, // 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/<name>.wgsl`
2. `rust/crates/effects/src/pipeline.rs` (SHADER_REGISTRY + pack function)
3. `apps/web/src/lib/effects/definitions/<name>.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

View File

@ -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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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<GenericUniformBuffer, EffectsError> {
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)
}

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 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);
}

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 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);
}

View File

@ -0,0 +1,33 @@
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 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);
}

View File

@ -0,0 +1,34 @@
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 / 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);
}

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 / 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);
}

View File

@ -0,0 +1,47 @@
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;
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);
}

View File

@ -0,0 +1,33 @@
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 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);
}

View File

@ -0,0 +1,25 @@
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 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);
}

View File

@ -0,0 +1,38 @@
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;
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);
}

View File

@ -0,0 +1,46 @@
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 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);
}

View File

@ -0,0 +1,46 @@
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;
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);
}

View File

@ -0,0 +1,52 @@
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 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);
}

View File

@ -0,0 +1,64 @@
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;
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);
}

View File

@ -0,0 +1,77 @@
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;
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);
}

View File

@ -0,0 +1,38 @@
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;
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);
}

View File

@ -0,0 +1,37 @@
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 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);
}

View File

@ -0,0 +1,31 @@
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 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);
}

View File

@ -0,0 +1,32 @@
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 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);
}

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 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);
}

View File

@ -0,0 +1,35 @@
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 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);
}

View File

@ -0,0 +1,34 @@
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 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);
}

View File

@ -0,0 +1,31 @@
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 / 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);
}

View File

@ -0,0 +1,31 @@
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 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);
}