export const VERTEX_SHADER = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform float u_flipY;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
// When u_flipY is -1.0, we flip the texture Y coordinate
// This corrects for the difference between image coordinates (top-left origin)
// and WebGL coordinates (bottom-left origin)
v_texCoord = vec2(a_texCoord.x, u_flipY < 0.0 ? 1.0 - a_texCoord.y : a_texCoord.y);
}
`;
export const PASSTHROUGH_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
`;
export const NOISE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_intensity;
uniform float u_scale;
uniform float u_seed;
uniform bool u_monochrome;
uniform int u_blendMode;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
// High-quality hash function for noise generation
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// 2D noise function with smooth interpolation
float noise2D(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// Smoothstep for smoother interpolation
vec2 u = f * f * (3.0 - 2.0 * f);
// Four corners
float a = hash12(i);
float b = hash12(i + vec2(1.0, 0.0));
float c = hash12(i + vec2(0.0, 1.0));
float d = hash12(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// Multi-octave noise for more natural film grain texture
float filmGrain(vec2 uv, float seed) {
float grain = 0.0;
float amplitude = 1.0;
float frequency = 1.0;
for (int i = 0; i < 3; i++) {
grain += amplitude * noise2D(uv * frequency + seed);
amplitude *= 0.5;
frequency *= 2.0;
}
return grain / 1.75; // Normalize to 0-1 range
}
// Blend modes
vec3 blendOverlay(vec3 base, vec3 blend) {
return mix(
2.0 * base * blend,
1.0 - 2.0 * (1.0 - base) * (1.0 - blend),
step(0.5, base)
);
}
vec3 blendSoftLight(vec3 base, vec3 blend) {
return mix(
base - (1.0 - 2.0 * blend) * base * (1.0 - base),
base + (2.0 * blend - 1.0) * (sqrt(base) - base),
step(0.5, blend)
);
}
vec3 blendMultiply(vec3 base, vec3 blend) {
return base * blend;
}
vec3 blendScreen(vec3 base, vec3 blend) {
return 1.0 - (1.0 - base) * (1.0 - blend);
}
vec3 applyBlend(vec3 base, vec3 blend, int mode) {
if (mode == 0) return blendOverlay(base, blend);
if (mode == 1) return blendSoftLight(base, blend);
if (mode == 2) return blendMultiply(base, blend);
if (mode == 3) return blendScreen(base, blend);
return base;
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
// Calculate grain coordinates with proper scaling
vec2 grainCoord = v_texCoord * u_resolution / u_scale;
vec3 grain;
if (u_monochrome) {
float g = filmGrain(grainCoord, u_seed);
grain = vec3(g);
} else {
grain = vec3(
filmGrain(grainCoord, u_seed),
filmGrain(grainCoord, u_seed + 100.0),
filmGrain(grainCoord, u_seed + 200.0)
);
}
// Apply blend mode
vec3 blended = applyBlend(color.rgb, grain, u_blendMode);
// Mix based on intensity
color.rgb = mix(color.rgb, blended, u_intensity);
gl_FragColor = color;
}
`;
export const CHROMATIC_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_intensity;
uniform float u_angle;
uniform bool u_radial;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
#define SAMPLES 8
// Attempt to approximate spectral colors across samples
vec3 spectralWeight(float t) {
// t goes from -1 (red end) to +1 (blue end)
// Approximate prism dispersion: red shifts one way, blue the other
float r = smoothstep(0.0, 0.7, -t + 0.5);
float g = 1.0 - abs(t) * 0.8;
float b = smoothstep(0.0, 0.7, t + 0.5);
return vec3(r, g, b);
}
void main() {
vec2 center = vec2(0.5);
vec2 dir;
if (u_radial) {
dir = normalize(v_texCoord - center + 0.0001) * u_intensity;
} else {
float angle = u_angle * PI / 180.0;
dir = vec2(cos(angle), sin(angle)) * u_intensity;
}
// Multi-sample spectral dispersion
vec3 color = vec3(0.0);
vec3 totalWeight = vec3(0.0);
for (int i = 0; i < SAMPLES; i++) {
float t = (float(i) / float(SAMPLES - 1)) * 2.0 - 1.0; // -1 to +1
vec2 offset = dir * t;
vec3 sampleColor = texture2D(u_image, v_texCoord + offset).rgb;
vec3 w = spectralWeight(t);
color += sampleColor * w;
totalWeight += w;
}
color /= totalWeight;
float a = texture2D(u_image, v_texCoord).a;
gl_FragColor = vec4(color, a);
}
`;
export const VIGNETTE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_intensity;
uniform float u_size;
uniform float u_softness;
uniform vec3 u_color;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
vec2 center = vec2(0.5);
float dist = distance(v_texCoord, center);
// Calculate vignette
float radius = u_size;
float soft = u_softness * 0.5 + 0.001;
float vignette = smoothstep(radius, radius - soft, dist);
// Apply vignette
color.rgb = mix(u_color, color.rgb, mix(1.0, vignette, u_intensity));
gl_FragColor = color;
}
`;
export const SCANLINES_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_spacing;
uniform float u_thickness;
uniform float u_opacity;
uniform bool u_horizontal;
uniform float u_offset;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
vec2 pos = v_texCoord * u_resolution;
float coord = u_horizontal ? pos.y : pos.x;
coord += u_offset;
float line = mod(coord, u_spacing);
float scanline = step(u_thickness, line);
color.rgb *= mix(1.0 - u_opacity, 1.0, scanline);
gl_FragColor = color;
}
`;
export const PIXELATE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_size;
uniform bool u_maintainAspect;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 pixelSize;
if (u_maintainAspect) {
// Square pixels: equal block size in device pixels on both axes
pixelSize = vec2(u_size) / u_resolution;
} else {
// Stretch to fill: same number of divisions (in UV) on each axis
float minDim = min(u_resolution.x, u_resolution.y);
float ps = u_size / minDim;
pixelSize = vec2(ps);
}
vec2 coord = floor(v_texCoord / pixelSize + 0.5) * pixelSize;
gl_FragColor = texture2D(u_image, coord);
}
`;
export const BLUR_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_radius;
uniform int u_type; // 0 = gaussian, 1 = radial, 2 = motion
uniform float u_angle;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
vec4 gaussianBlur(vec2 uv, float radius) {
vec4 color = vec4(0.0);
vec2 texelSize = 1.0 / u_resolution;
float total = 0.0;
float sigma = max(radius * 0.5, 0.5);
float invSigma2 = -0.5 / (sigma * sigma);
for (int x = -7; x <= 7; x++) {
for (int y = -7; y <= 7; y++) {
float dist2 = float(x*x + y*y);
if (dist2 > radius * radius) continue;
float weight = exp(dist2 * invSigma2);
vec2 offset = vec2(float(x), float(y)) * texelSize;
color += texture2D(u_image, uv + offset) * weight;
total += weight;
}
}
return color / total;
}
vec4 radialBlur(vec2 uv, float radius) {
vec4 color = vec4(0.0);
vec2 center = vec2(0.5);
vec2 dir = uv - center;
float total = 0.0;
const float samples = 20.0;
for (float i = 0.0; i < 20.0; i++) {
float t = i / samples * radius * 0.01;
float weight = 1.0 - i / samples;
color += texture2D(u_image, uv - dir * t) * weight;
total += weight;
}
return color / total;
}
vec4 motionBlur(vec2 uv, float radius, float angle) {
vec4 color = vec4(0.0);
vec2 dir = vec2(cos(angle * PI / 180.0), sin(angle * PI / 180.0));
vec2 texelSize = 1.0 / u_resolution;
float total = 0.0;
float sigma = max(radius * 0.5, 0.5);
float invSigma2 = -0.5 / (sigma * sigma);
for (float i = -7.0; i <= 7.0; i++) {
float dist2 = i * i;
if (dist2 > radius * radius) continue;
float weight = exp(dist2 * invSigma2);
vec2 offset = dir * texelSize * i * radius / 7.0;
color += texture2D(u_image, uv + offset) * weight;
total += weight;
}
return color / total;
}
void main() {
if (u_type == 0) {
gl_FragColor = gaussianBlur(v_texCoord, u_radius);
} else if (u_type == 1) {
gl_FragColor = radialBlur(v_texCoord, u_radius);
} else {
gl_FragColor = motionBlur(v_texCoord, u_radius, u_angle);
}
}
`;
export const DISTORTION_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform int u_type; // 0 = wave, 1 = swirl, 2 = bulge
uniform float u_amplitude;
uniform float u_frequency;
uniform vec2 u_center;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
vec2 waveDistortion(vec2 uv) {
vec2 offset;
offset.x = sin(uv.y * u_frequency * PI * 2.0) * u_amplitude / u_resolution.x;
offset.y = sin(uv.x * u_frequency * PI * 2.0) * u_amplitude / u_resolution.y;
return uv + offset;
}
vec2 swirlDistortion(vec2 uv) {
vec2 centered = uv - u_center;
float dist = length(centered);
float angle = atan(centered.y, centered.x);
float swirl = u_amplitude * 0.1 * (1.0 - dist);
swirl = max(0.0, swirl);
angle += swirl;
return u_center + vec2(cos(angle), sin(angle)) * dist;
}
vec2 bulgeDistortion(vec2 uv) {
vec2 centered = uv - u_center;
float dist = length(centered);
float radius = 0.5;
if (dist < radius) {
float percent = dist / radius;
float bulge = sin(percent * PI * 0.5);
float amount = 1.0 + u_amplitude * 0.01 * (1.0 - bulge);
centered *= amount;
}
return u_center + centered;
}
void main() {
vec2 distorted;
if (u_type == 0) {
distorted = waveDistortion(v_texCoord);
} else if (u_type == 1) {
distorted = swirlDistortion(v_texCoord);
} else {
distorted = bulgeDistortion(v_texCoord);
}
// Clamp to valid texture coordinates
distorted = clamp(distorted, 0.0, 1.0);
gl_FragColor = texture2D(u_image, distorted);
}
`;
export const POSTERIZE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform int u_levels;
uniform bool u_preserveHue;
varying vec2 v_texCoord;
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
float lvl = float(u_levels);
if (u_preserveHue) {
vec3 hsv = rgb2hsv(color.rgb);
hsv.z = floor(hsv.z * lvl + 0.5) / lvl;
hsv.y = floor(hsv.y * lvl + 0.5) / lvl;
color.rgb = hsv2rgb(hsv);
} else {
color.rgb = floor(color.rgb * lvl + 0.5) / lvl;
}
gl_FragColor = color;
}
`;
export const EDGE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_strength;
uniform float u_threshold;
uniform bool u_invert;
uniform bool u_colorize;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 texel = 1.0 / u_resolution;
vec4 color = texture2D(u_image, v_texCoord);
float tl = dot(texture2D(u_image, v_texCoord + vec2(-1.0, -1.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float t = dot(texture2D(u_image, v_texCoord + vec2( 0.0, -1.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float tr = dot(texture2D(u_image, v_texCoord + vec2( 1.0, -1.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float l = dot(texture2D(u_image, v_texCoord + vec2(-1.0, 0.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float r = dot(texture2D(u_image, v_texCoord + vec2( 1.0, 0.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float bl = dot(texture2D(u_image, v_texCoord + vec2(-1.0, 1.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float b = dot(texture2D(u_image, v_texCoord + vec2( 0.0, 1.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float br = dot(texture2D(u_image, v_texCoord + vec2( 1.0, 1.0) * texel).rgb, vec3(0.299, 0.587, 0.114));
float gx = -tl - 2.0*l - bl + tr + 2.0*r + br;
float gy = -tl - 2.0*t - tr + bl + 2.0*b + br;
float edge = sqrt(gx*gx + gy*gy) * u_strength;
edge = smoothstep(u_threshold, u_threshold + 0.1, edge);
if (u_invert) edge = 1.0 - edge;
vec3 result;
if (u_colorize) {
result = color.rgb * edge;
} else {
result = vec3(edge);
}
gl_FragColor = vec4(result, color.a);
}
`;
export const GRAIN_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_intensity;
uniform float u_size;
uniform float u_luminanceResponse;
uniform bool u_colored;
uniform float u_seed;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float grain(vec2 uv, float seed) {
return hash(uv + seed) * 2.0 - 1.0;
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
float response = mix(1.0, 1.0 - luma, u_luminanceResponse);
vec2 grainUV = v_texCoord * u_resolution / u_size;
vec3 grainValue;
if (u_colored) {
grainValue = vec3(
grain(grainUV, u_seed),
grain(grainUV, u_seed + 100.0),
grain(grainUV, u_seed + 200.0)
);
} else {
float g = grain(grainUV, u_seed);
grainValue = vec3(g);
}
color.rgb += grainValue * u_intensity * response;
color.rgb = clamp(color.rgb, 0.0, 1.0);
gl_FragColor = color;
}
`;
export const COLOR_GRADE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_saturation;
uniform float u_temperature;
uniform float u_tint;
uniform vec3 u_shadows;
uniform vec3 u_highlights;
uniform float u_shadowInfluence;
uniform float u_highlightInfluence;
varying vec2 v_texCoord;
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
// Brightness: multiplicative feels more natural than additive
color.rgb *= 1.0 + u_brightness;
// Contrast: pivot around midpoint
color.rgb = (color.rgb - 0.5) * (1.0 + u_contrast) + 0.5;
// Saturation
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(vec3(luma), color.rgb, 1.0 + u_saturation);
// Temperature: shift red-blue balance
color.r += u_temperature * 0.1;
color.b -= u_temperature * 0.1;
// Tint: shift green-magenta balance
color.g += u_tint * 0.1;
// Recalculate luma after adjustments for accurate shadow/highlight masks
luma = dot(clamp(color.rgb, 0.0, 1.0), vec3(0.299, 0.587, 0.114));
// Shadow tinting: mix toward shadow color in dark areas
float shadowMask = 1.0 - smoothstep(0.0, 0.5, luma);
color.rgb = mix(color.rgb, mix(color.rgb, u_shadows * luma * 2.0, 0.5), shadowMask * u_shadowInfluence);
// Highlight tinting: mix toward highlight color in bright areas
float highlightMask = smoothstep(0.5, 1.0, luma);
color.rgb = mix(color.rgb, mix(color.rgb, u_highlights * luma, 0.5), highlightMask * u_highlightInfluence);
color.rgb = clamp(color.rgb, 0.0, 1.0);
gl_FragColor = color;
}
`;
export const DITHER_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform int u_method;
uniform int u_levels;
uniform float u_threshold;
uniform vec2 u_resolution;
uniform float u_scale;
varying vec2 v_texCoord;
float bayer2(vec2 pos) {
pos = floor(pos);
float x = mod(pos.x, 2.0);
float y = mod(pos.y, 2.0);
return mod(x + y * 2.0, 4.0) / 4.0 + 1.0 / 8.0;
}
float bayer4(vec2 pos) {
pos = floor(pos);
return bayer2(pos * 0.5) * 0.25 + bayer2(pos);
}
float bayer8(vec2 pos) {
pos = floor(pos);
return bayer4(pos * 0.5) * 0.25 + bayer2(pos);
}
float getBayerThreshold(vec2 pos, int method) {
if (method == 0) return bayer2(pos) - 0.5;
if (method == 1) return bayer4(pos) - 0.5;
return bayer8(pos) - 0.5;
}
vec3 quantize(vec3 color, int levels) {
float lvl = float(levels - 1);
return floor(color * lvl + 0.5) / lvl;
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
vec2 pixelPos = v_texCoord * u_resolution / u_scale;
float threshold = getBayerThreshold(pixelPos, u_method);
float spread = u_threshold / float(u_levels);
vec3 dithered = color.rgb + threshold * spread;
vec3 result = quantize(dithered, u_levels);
gl_FragColor = vec4(clamp(result, 0.0, 1.0), color.a);
}
`;
export const HALFTONE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_gridSize;
uniform float u_dotScale;
uniform float u_angle;
uniform int u_shape;
uniform float u_softness;
uniform float u_contrast;
uniform bool u_invert;
uniform int u_colorMode;
uniform vec3 u_dotColor;
uniform vec3 u_backgroundColor;
uniform float u_mixOriginal;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
#define SHAPE_CIRCLE 0
#define SHAPE_SQUARE 1
#define SHAPE_DIAMOND 2
#define SHAPE_LINE 3
#define SHAPE_ELLIPSE 4
#define SHAPE_CROSS 5
#define SHAPE_RING 6
vec2 rotate2D(vec2 p, float angle) {
float s = sin(angle);
float c = cos(angle);
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// Shape distance functions (return 0 at center, 1 at edge)
float sdCircle(vec2 p) {
return length(p) * 2.0;
}
float sdSquare(vec2 p) {
vec2 d = abs(p);
return max(d.x, d.y) * 2.0;
}
float sdDiamond(vec2 p) {
vec2 d = abs(p);
return (d.x + d.y);
}
float sdLine(vec2 p) {
return abs(p.y) * 2.0;
}
float sdEllipse(vec2 p) {
return length(p * vec2(1.0, 1.6)) * 2.0;
}
float sdCross(vec2 p) {
vec2 d = abs(p);
return min(d.x, d.y) * 4.0;
}
float sdRing(vec2 p) {
float d = length(p) * 2.0;
return abs(d - 0.7) * 3.0;
}
float getShape(vec2 p, int shape) {
if (shape == SHAPE_CIRCLE) return sdCircle(p);
if (shape == SHAPE_SQUARE) return sdSquare(p);
if (shape == SHAPE_DIAMOND) return sdDiamond(p);
if (shape == SHAPE_LINE) return sdLine(p);
if (shape == SHAPE_ELLIPSE) return sdEllipse(p);
if (shape == SHAPE_CROSS) return sdCross(p);
if (shape == SHAPE_RING) return sdRing(p);
return sdCircle(p);
}
// CMYK conversion
vec4 rgb2cmyk(vec3 rgb) {
float k = 1.0 - max(max(rgb.r, rgb.g), rgb.b);
float invK = 1.0 / (1.0 - k + 0.0001);
float c = (1.0 - rgb.r - k) * invK;
float m = (1.0 - rgb.g - k) * invK;
float y = (1.0 - rgb.b - k) * invK;
return vec4(c, m, y, k);
}
vec3 cmyk2rgb(vec4 cmyk) {
float invK = 1.0 - cmyk.w;
return vec3(
(1.0 - cmyk.x) * invK,
(1.0 - cmyk.y) * invK,
(1.0 - cmyk.z) * invK
);
}
float halftonePattern(vec2 pos, float intensity, float angle, int shape, float softness, float dotScale) {
// Rotate the coordinate space
vec2 rotated = rotate2D(pos, angle);
// Get position within cell (-0.5 to 0.5)
vec2 cell = fract(rotated) - 0.5;
// Calculate shape distance
float shapeDist = getShape(cell, shape);
// The dot size is based on intensity - darker areas = bigger dots
float dotSize = sqrt(intensity) * dotScale;
// Antialiased edge
float aa = softness * 0.15 + 0.02;
float pattern = smoothstep(dotSize + aa, dotSize - aa, shapeDist);
return pattern;
}
void main() {
vec4 originalColor = texture2D(u_image, v_texCoord);
// Calculate grid coordinates
float aspectRatio = u_resolution.x / u_resolution.y;
vec2 gridCoord = v_texCoord * vec2(u_resolution.x / u_gridSize, u_resolution.y / u_gridSize);
// Sample color at cell center for more accurate color representation
vec2 cellSize = u_gridSize / u_resolution;
vec2 cellCenter = (floor(v_texCoord / cellSize) + 0.5) * cellSize;
vec4 sampledColor = texture2D(u_image, cellCenter);
// Apply contrast
vec3 adjusted = sampledColor.rgb;
adjusted = (adjusted - 0.5) * (1.0 + u_contrast) + 0.5;
adjusted = clamp(adjusted, 0.0, 1.0);
float angleRad = u_angle * PI / 180.0;
vec3 result;
if (u_colorMode == 0) {
// Original colors mode - use image colors
float r = halftonePattern(gridCoord, adjusted.r, angleRad, u_shape, u_softness, u_dotScale);
float g = halftonePattern(gridCoord, adjusted.g, angleRad + 0.52, u_shape, u_softness, u_dotScale);
float b = halftonePattern(gridCoord, adjusted.b, angleRad + 1.05, u_shape, u_softness, u_dotScale);
if (u_invert) {
result = vec3(1.0 - r, 1.0 - g, 1.0 - b);
} else {
result = vec3(r, g, b);
}
} else if (u_colorMode == 1) {
// Monochrome mode
float luma = dot(adjusted, vec3(0.299, 0.587, 0.114));
float pattern = halftonePattern(gridCoord, luma, angleRad, u_shape, u_softness, u_dotScale);
if (u_invert) pattern = 1.0 - pattern;
result = mix(u_backgroundColor, u_dotColor, pattern);
} else if (u_colorMode == 2) {
// Duotone mode - single pattern with custom colors
float luma = dot(adjusted, vec3(0.299, 0.587, 0.114));
float pattern = halftonePattern(gridCoord, luma, angleRad, u_shape, u_softness, u_dotScale);
if (u_invert) pattern = 1.0 - pattern;
result = mix(u_backgroundColor, u_dotColor, pattern);
} else {
// CMYK mode
vec4 cmyk = rgb2cmyk(adjusted);
// Traditional CMYK halftone angles
float cAngle = angleRad + 0.26; // ~15°
float mAngle = angleRad + 1.31; // ~75°
float yAngle = angleRad; // 0°
float kAngle = angleRad + 0.79; // ~45°
float c = halftonePattern(gridCoord, cmyk.x, cAngle, u_shape, u_softness, u_dotScale);
float m = halftonePattern(gridCoord, cmyk.y, mAngle, u_shape, u_softness, u_dotScale);
float y = halftonePattern(gridCoord, cmyk.z, yAngle, u_shape, u_softness, u_dotScale);
float k = halftonePattern(gridCoord, cmyk.w, kAngle, u_shape, u_softness, u_dotScale);
if (u_invert) {
c = 1.0 - c;
m = 1.0 - m;
y = 1.0 - y;
k = 1.0 - k;
}
result = cmyk2rgb(vec4(c, m, y, k));
}
// Mix with original color if desired
result = mix(result, originalColor.rgb, u_mixOriginal);
gl_FragColor = vec4(result, originalColor.a);
}
`;
export const ASCII_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform sampler2D u_charAtlas;
uniform float u_cellSize;
uniform bool u_color;
uniform bool u_invert;
uniform int u_charCount;
uniform vec2 u_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_gamma;
uniform float u_charBrightness;
uniform vec3 u_charColor;
uniform vec3 u_backgroundColor;
uniform float u_backgroundBlend;
uniform float u_edgeEnhance;
uniform float u_cellGap;
varying vec2 v_texCoord;
float getEdge(vec2 uv, vec2 texelSize) {
float tl = dot(texture2D(u_image, uv + vec2(-1.0, -1.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float t = dot(texture2D(u_image, uv + vec2( 0.0, -1.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float tr = dot(texture2D(u_image, uv + vec2( 1.0, -1.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float l = dot(texture2D(u_image, uv + vec2(-1.0, 0.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float r = dot(texture2D(u_image, uv + vec2( 1.0, 0.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float bl = dot(texture2D(u_image, uv + vec2(-1.0, 1.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float b = dot(texture2D(u_image, uv + vec2( 0.0, 1.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float br = dot(texture2D(u_image, uv + vec2( 1.0, 1.0) * texelSize).rgb, vec3(0.299, 0.587, 0.114));
float gx = -tl - 2.0*l - bl + tr + 2.0*r + br;
float gy = -tl - 2.0*t - tr + bl + 2.0*b + br;
return sqrt(gx*gx + gy*gy);
}
void main() {
vec2 cellCount = floor(u_resolution / u_cellSize);
vec2 cellIndex = floor(v_texCoord * cellCount);
vec2 cellUV = fract(v_texCoord * cellCount);
// Cell gap - discard pixels in gap region
if (u_cellGap > 0.0) {
float halfGap = u_cellGap * 0.5;
if (cellUV.x < halfGap || cellUV.x > 1.0 - halfGap ||
cellUV.y < halfGap || cellUV.y > 1.0 - halfGap) {
gl_FragColor = vec4(u_backgroundColor, 1.0);
return;
}
// Remap cellUV to fill remaining space
cellUV = (cellUV - halfGap) / (1.0 - u_cellGap);
}
vec2 sampleUV = (cellIndex + 0.5) / cellCount;
vec4 sampleColor = texture2D(u_image, sampleUV);
vec3 adjustedColor = sampleColor.rgb;
adjustedColor = (adjustedColor - 0.5) * u_contrast + 0.5;
adjustedColor = adjustedColor * u_brightness;
adjustedColor = clamp(adjustedColor, 0.0, 1.0);
float luma = dot(adjustedColor, vec3(0.299, 0.587, 0.114));
// Apply gamma curve for tone mapping
luma = pow(luma, 1.0 / u_gamma);
if (u_edgeEnhance > 0.0) {
vec2 texelSize = 1.0 / u_resolution;
float edge = getEdge(sampleUV, texelSize);
luma = mix(luma, luma + edge * 0.5, u_edgeEnhance);
luma = clamp(luma, 0.0, 1.0);
}
if (u_invert) luma = 1.0 - luma;
float charIdx = floor(luma * float(u_charCount - 1) + 0.5);
charIdx = clamp(charIdx, 0.0, float(u_charCount - 1));
vec2 atlasUV = vec2(
(charIdx + cellUV.x) / float(u_charCount),
1.0 - cellUV.y
);
vec4 charSample = texture2D(u_charAtlas, atlasUV);
float charAlpha = clamp(charSample.r * u_charBrightness, 0.0, 1.0);
// Character color: use original image color or custom char color
vec3 charColor;
if (u_color) {
charColor = adjustedColor * charAlpha;
} else {
charColor = u_charColor * charAlpha;
}
// Background: blend between backgroundColor and original image
vec3 bgColor = mix(u_backgroundColor, sampleColor.rgb, u_backgroundBlend);
// Composite: character on top of background
vec3 finalColor = mix(bgColor, charColor, charAlpha);
gl_FragColor = vec4(finalColor, sampleColor.a);
}
`;
export const OVERLAY_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform sampler2D u_overlay;
uniform float u_intensity;
uniform float u_scale;
uniform float u_rotation;
uniform int u_blendMode;
uniform vec2 u_resolution;
uniform bool u_hasOverlay;
varying vec2 v_texCoord;
#define PI 3.14159265359
vec2 rotate2D(vec2 p, float angle) {
float s = sin(angle);
float c = cos(angle);
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
}
vec3 blendOverlay(vec3 base, vec3 blend) {
return mix(
2.0 * base * blend,
1.0 - 2.0 * (1.0 - base) * (1.0 - blend),
step(0.5, base)
);
}
vec3 blendSoftLight(vec3 base, vec3 blend) {
return mix(
base - (1.0 - 2.0 * blend) * base * (1.0 - base),
base + (2.0 * blend - 1.0) * (sqrt(base) - base),
step(0.5, blend)
);
}
vec3 blendMultiply(vec3 base, vec3 blend) {
return base * blend;
}
vec3 blendScreen(vec3 base, vec3 blend) {
return 1.0 - (1.0 - base) * (1.0 - blend);
}
vec3 applyBlend(vec3 base, vec3 blend, int mode) {
if (mode == 0) return blendOverlay(base, blend);
if (mode == 1) return blendSoftLight(base, blend);
if (mode == 2) return blendMultiply(base, blend);
if (mode == 3) return blendScreen(base, blend);
return base;
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
if (u_hasOverlay) {
// Calculate overlay coordinates with rotation and scale
vec2 center = vec2(0.5);
vec2 overlayCoord = v_texCoord - center;
overlayCoord = rotate2D(overlayCoord, u_rotation * PI / 180.0);
overlayCoord = overlayCoord / u_scale + center;
overlayCoord = fract(overlayCoord); // Tile the texture
vec4 overlay = texture2D(u_overlay, overlayCoord);
vec3 blended = applyBlend(color.rgb, overlay.rgb, u_blendMode);
color.rgb = mix(color.rgb, blended, u_intensity * overlay.a);
}
gl_FragColor = color;
}
`;
export const GLITCH_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_intensity;
uniform float u_sliceCount;
uniform float u_rgbShift;
uniform float u_angle;
uniform float u_seed;
uniform float u_blockSize;
uniform bool u_colorShift;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
float hash(float n) {
return fract(sin(n) * 43758.5453123);
}
float hash2(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
void main() {
vec2 uv = v_texCoord;
// Slice-based displacement
float sliceY = floor(uv.y * u_sliceCount);
float sliceRand = hash(sliceY + u_seed);
float sliceActive = step(1.0 - u_intensity, sliceRand);
// Calculate displacement for this slice
float displacement = (hash(sliceY * 2.0 + u_seed) - 0.5) * 2.0 * u_intensity * 0.1;
displacement *= sliceActive;
// Block-based glitch
vec2 blockPos = floor(uv / u_blockSize);
float blockRand = hash2(blockPos + vec2(u_seed));
float blockActive = step(1.0 - u_intensity * 0.3, blockRand);
vec2 blockDisp = vec2(
(hash2(blockPos + vec2(u_seed, 0.0)) - 0.5) * 0.1,
(hash2(blockPos + vec2(0.0, u_seed)) - 0.5) * 0.05
) * blockActive * u_intensity;
// Apply displacement based on angle
float angleRad = u_angle * PI / 180.0;
vec2 dir = vec2(cos(angleRad), sin(angleRad));
vec2 displaced = uv + dir * displacement + blockDisp;
// RGB channel separation
vec2 rgbOffset = dir * u_rgbShift * u_intensity;
float r, g, b, a;
if (u_colorShift) {
r = texture2D(u_image, displaced + rgbOffset).r;
g = texture2D(u_image, displaced).g;
b = texture2D(u_image, displaced - rgbOffset).b;
} else {
vec3 col = texture2D(u_image, displaced).rgb;
r = col.r;
g = col.g;
b = col.b;
}
a = texture2D(u_image, displaced).a;
// Occasional color corruption
float corrupt = step(1.0 - u_intensity * 0.1, hash2(blockPos + vec2(u_seed * 2.0)));
if (corrupt > 0.5) {
float swap = hash(sliceY + u_seed * 3.0);
if (swap < 0.33) {
float temp = r; r = g; g = b; b = temp;
} else if (swap < 0.66) {
float temp = r; r = b; b = temp;
}
}
gl_FragColor = vec4(r, g, b, a);
}
`;
export const CRT_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_curvature;
uniform float u_scanlineIntensity;
uniform float u_scanlineCount;
uniform float u_vignetteIntensity;
uniform float u_brightness;
uniform float u_rgbOffset;
uniform float u_flickerIntensity;
uniform float u_staticNoise;
uniform float u_seed;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
vec2 curveUV(vec2 uv, float amount) {
uv = uv * 2.0 - 1.0;
vec2 offset = abs(uv.yx) / vec2(6.0, 4.0);
uv = uv + uv * offset * offset * amount;
uv = uv * 0.5 + 0.5;
return uv;
}
void main() {
// Apply screen curvature
vec2 uv = curveUV(v_texCoord, u_curvature);
// Check if we're outside the curved screen - use transparent
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
return;
}
// Sample center for alpha
vec4 centerSample = texture2D(u_image, uv);
// RGB phosphor offset (chromatic aberration)
vec2 rgbOff = vec2(u_rgbOffset, 0.0);
float r = texture2D(u_image, uv + rgbOff).r;
float g = texture2D(u_image, uv).g;
float b = texture2D(u_image, uv - rgbOff).b;
vec3 color = vec3(r, g, b);
// Scanlines
float scanline = sin(uv.y * u_scanlineCount * PI * 2.0) * 0.5 + 0.5;
scanline = pow(scanline, 1.5);
color *= 1.0 - u_scanlineIntensity * (1.0 - scanline);
// RGB phosphor pattern (subtle vertical stripes)
float phosphor = mod(floor(uv.x * u_resolution.x), 3.0);
vec3 phosphorMask = vec3(
phosphor == 0.0 ? 1.0 : 0.7,
phosphor == 1.0 ? 1.0 : 0.7,
phosphor == 2.0 ? 1.0 : 0.7
);
color *= mix(vec3(1.0), phosphorMask, u_scanlineIntensity * 0.3);
// Vignette
vec2 vigUV = uv * (1.0 - uv.yx);
float vig = vigUV.x * vigUV.y * 15.0;
vig = pow(vig, u_vignetteIntensity * 0.5);
color *= vig;
// Flicker
float flicker = 1.0 + (hash(vec2(u_seed, 0.0)) - 0.5) * u_flickerIntensity;
color *= flicker;
// Static noise
float noise = (hash(uv * u_resolution + u_seed) - 0.5) * u_staticNoise;
color += noise;
// Brightness adjustment
color *= u_brightness;
// Preserve original alpha for transparency support
gl_FragColor = vec4(clamp(color, 0.0, 1.0), centerSample.a);
}
`;
export const DUOTONE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform vec3 u_shadowColor;
uniform vec3 u_highlightColor;
uniform float u_contrast;
uniform float u_intensity;
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
// Calculate luminance
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
// Apply contrast adjustment
luma = (luma - 0.5) * u_contrast + 0.5;
luma = clamp(luma, 0.0, 1.0);
// Map to duotone gradient
vec3 duotone = mix(u_shadowColor, u_highlightColor, luma);
// Blend with original based on intensity
vec3 result = mix(color.rgb, duotone, u_intensity);
gl_FragColor = vec4(result, color.a);
}
`;
export const KUWAHARA_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform int u_radius;
uniform float u_sharpness;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
vec2 texelSize = 1.0 / u_resolution;
int r = u_radius;
// We divide the kernel into 4 overlapping quadrants
// and find the one with minimum variance
vec3 sum[4];
vec3 sumSq[4];
int count[4];
for (int i = 0; i < 4; i++) {
sum[i] = vec3(0.0);
sumSq[i] = vec3(0.0);
count[i] = 0;
}
// Sample the four quadrants
for (int dy = -5; dy <= 5; dy++) {
for (int dx = -5; dx <= 5; dx++) {
if (abs(float(dx)) > float(r) || abs(float(dy)) > float(r)) continue;
vec2 offset = vec2(float(dx), float(dy)) * texelSize;
vec3 sample = texture2D(u_image, v_texCoord + offset).rgb;
// Determine which quadrant(s) this sample belongs to
// Using overlapping regions for smoother results
if (dx <= 0 && dy <= 0) { sum[0] += sample; sumSq[0] += sample * sample; count[0]++; }
if (dx >= 0 && dy <= 0) { sum[1] += sample; sumSq[1] += sample * sample; count[1]++; }
if (dx <= 0 && dy >= 0) { sum[2] += sample; sumSq[2] += sample * sample; count[2]++; }
if (dx >= 0 && dy >= 0) { sum[3] += sample; sumSq[3] += sample * sample; count[3]++; }
}
}
// Find the quadrant with minimum variance
float minVariance = 1e10;
vec3 result = vec3(0.0);
for (int i = 0; i < 4; i++) {
if (count[i] > 0) {
vec3 mean = sum[i] / float(count[i]);
vec3 variance = sumSq[i] / float(count[i]) - mean * mean;
float totalVariance = variance.r + variance.g + variance.b;
if (totalVariance < minVariance) {
minVariance = totalVariance;
result = mean;
}
}
}
// Blend with original based on sharpness
vec4 original = texture2D(u_image, v_texCoord);
result = mix(original.rgb, result, 1.0 - u_sharpness * 0.5);
gl_FragColor = vec4(result, original.a);
}
`;
export const BARREL_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_amount;
uniform vec2 u_center;
uniform float u_zoom;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
// Center the coordinates
vec2 uv = v_texCoord - u_center;
// Correct for aspect ratio
float aspect = u_resolution.x / u_resolution.y;
uv.x *= aspect;
// Calculate distance from center
float r = length(uv);
float r2 = r * r;
// Apply barrel/pincushion distortion
// Positive amount = barrel (bulge outward)
// Negative amount = pincushion (pinch inward)
float distortion = 1.0 + r2 * u_amount;
// Apply distortion and zoom
vec2 distorted = uv * distortion / u_zoom;
// Restore aspect ratio and center
distorted.x /= aspect;
distorted += u_center;
// Sample with boundary check - use transparent for out-of-bounds
if (distorted.x < 0.0 || distorted.x > 1.0 || distorted.y < 0.0 || distorted.y > 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
} else {
gl_FragColor = texture2D(u_image, distorted);
}
}
`;
export const RIPPLE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_amplitude;
uniform float u_wavelength;
uniform float u_speed;
uniform vec2 u_center;
uniform float u_damping;
uniform float u_time;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
void main() {
vec2 uv = v_texCoord;
// Calculate distance from center
vec2 toCenter = uv - u_center;
float aspect = u_resolution.x / u_resolution.y;
toCenter.x *= aspect;
float dist = length(toCenter);
// Calculate ripple phase
float phase = dist * u_wavelength - u_time * u_speed;
// Calculate displacement with damping
float damping = exp(-dist * u_damping * 5.0);
float displacement = sin(phase * PI * 2.0) * u_amplitude * damping;
// Displace perpendicular to the direction from center
vec2 direction = normalize(toCenter + 0.0001);
direction.x /= aspect;
vec2 displaced = uv + direction * displacement;
// Clamp to valid range
displaced = clamp(displaced, 0.0, 1.0);
gl_FragColor = texture2D(u_image, displaced);
}
`;
export const DISPLACEMENT_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform sampler2D u_displacementMap;
uniform float u_scaleX;
uniform float u_scaleY;
uniform bool u_useRedGreen;
uniform bool u_hasDisplacementMap;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
// Procedural noise for when no map is provided
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 4; i++) {
value += amplitude * noise(p);
p *= 2.0;
amplitude *= 0.5;
}
return value;
}
void main() {
vec2 displacement;
if (u_hasDisplacementMap) {
vec4 dispSample = texture2D(u_displacementMap, v_texCoord);
if (u_useRedGreen) {
displacement = (dispSample.rg - 0.5) * 2.0;
} else {
float luma = dot(dispSample.rgb, vec3(0.299, 0.587, 0.114));
displacement = vec2(luma - 0.5) * 2.0;
}
} else {
// Procedural displacement
vec2 noiseCoord = v_texCoord * 8.0;
displacement = vec2(
fbm(noiseCoord) - 0.5,
fbm(noiseCoord + vec2(100.0)) - 0.5
);
}
vec2 displaced = v_texCoord + displacement * vec2(u_scaleX, u_scaleY);
displaced = clamp(displaced, 0.0, 1.0);
gl_FragColor = texture2D(u_image, displaced);
}
`;
export const LIGHT_LEAK_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform float u_position;
uniform float u_angle;
uniform float u_size;
uniform float u_intensity;
uniform float u_softness;
uniform int u_blendMode;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
vec3 blendScreen(vec3 base, vec3 blend) {
return 1.0 - (1.0 - base) * (1.0 - blend);
}
vec3 blendAdd(vec3 base, vec3 blend) {
return min(base + blend, vec3(1.0));
}
vec3 blendMultiply(vec3 base, vec3 blend) {
return base * blend;
}
vec3 blendOverlay(vec3 base, vec3 blend) {
return mix(
2.0 * base * blend,
1.0 - 2.0 * (1.0 - base) * (1.0 - blend),
step(0.5, base)
);
}
vec3 blendSoftLight(vec3 base, vec3 blend) {
return mix(
base - (1.0 - 2.0 * blend) * base * (1.0 - base),
base + (2.0 * blend - 1.0) * (sqrt(base) - base),
step(0.5, blend)
);
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
// Create directional gradient
float angleRad = u_angle * PI / 180.0;
vec2 dir = vec2(cos(angleRad), sin(angleRad));
// Calculate position along the gradient direction
vec2 uv = v_texCoord - 0.5;
float aspect = u_resolution.x / u_resolution.y;
uv.x *= aspect;
float projection = dot(uv, dir);
float offset = (projection + u_position - 0.5) / u_size;
// Create soft gradient
float t = 1.0 - smoothstep(0.0, u_softness, abs(offset));
t = pow(t, 2.0 - u_softness);
// Interpolate between colors
float colorT = smoothstep(-0.5, 0.5, offset);
vec3 leakColor = mix(u_color1, u_color2, colorT);
// Apply intensity
vec3 leak = leakColor * t * u_intensity;
// Blend with original (mode order matches renderer _blendModeToInt)
vec3 result;
if (u_blendMode == 0) result = blendOverlay(color.rgb, leak);
else if (u_blendMode == 1) result = blendSoftLight(color.rgb, leak);
else if (u_blendMode == 2) result = blendMultiply(color.rgb, leak);
else result = blendScreen(color.rgb, leak);
// Mix based on leak presence
result = mix(color.rgb, result, t);
gl_FragColor = vec4(result, color.a);
}
`;
export const BLOOM_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_radius;
uniform float u_intensity;
uniform float u_threshold;
uniform float u_softThreshold;
uniform int u_blendMode;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
vec3 blendScreen(vec3 base, vec3 blend) {
return 1.0 - (1.0 - base) * (1.0 - blend);
}
vec3 blendSoftLight(vec3 base, vec3 blend) {
return mix(
base - (1.0 - 2.0 * blend) * base * (1.0 - base),
base + (2.0 * blend - 1.0) * (sqrt(base) - base),
step(0.5, blend)
);
}
vec3 blendOverlay(vec3 base, vec3 blend) {
return mix(
2.0 * base * blend,
1.0 - 2.0 * (1.0 - base) * (1.0 - blend),
step(0.5, base)
);
}
vec3 blendMultiply(vec3 base, vec3 blend) {
return base * blend;
}
// Extract bright pixels with soft knee
vec3 extractBright(vec3 color, float threshold, float softKnee) {
float brightness = max(max(color.r, color.g), color.b);
float knee = threshold * softKnee;
float soft = brightness - threshold + knee;
soft = clamp(soft, 0.0, 2.0 * knee);
soft = soft * soft / (4.0 * knee + 0.00001);
float contribution = max(soft, brightness - threshold);
contribution /= max(brightness, 0.00001);
return color * max(contribution, 0.0);
}
void main() {
vec4 originalColor = texture2D(u_image, v_texCoord);
vec2 texelSize = 1.0 / u_resolution;
// Extract and blur bright areas
vec3 bloom = vec3(0.0);
float totalWeight = 0.0;
float r = u_radius;
// Two-pass Gaussian approximation in a single pass
// Use a 13x13 kernel for better performance while maintaining quality
for (int y = -6; y <= 6; y++) {
for (int x = -6; x <= 6; x++) {
float fx = float(x);
float fy = float(y);
float dist = sqrt(fx * fx + fy * fy);
// Skip samples outside radius
if (dist > r) continue;
// Gaussian weight
float sigma = max(r * 0.4, 0.5);
float weight = exp(-(dist * dist) / (2.0 * sigma * sigma));
vec2 offset = vec2(fx, fy) * texelSize;
vec3 sampleColor = texture2D(u_image, v_texCoord + offset).rgb;
// Extract only the bright parts of each sample
vec3 brightSample = extractBright(sampleColor, u_threshold, u_softThreshold);
bloom += brightSample * weight;
totalWeight += weight;
}
}
if (totalWeight > 0.0) {
bloom /= totalWeight;
}
// Apply intensity
bloom *= u_intensity;
// Blend bloom with original image (mode order matches renderer _blendModeToInt)
vec3 result;
if (u_blendMode == 0) {
// Overlay
result = blendOverlay(originalColor.rgb, bloom);
} else if (u_blendMode == 1) {
// Soft Light
result = blendSoftLight(originalColor.rgb, bloom);
} else if (u_blendMode == 2) {
// Multiply
result = blendMultiply(originalColor.rgb, bloom);
} else {
// Screen (default - bright and natural)
result = blendScreen(originalColor.rgb, bloom);
}
gl_FragColor = vec4(clamp(result, 0.0, 1.0), originalColor.a);
}
`;
export const RADIAL_BLUR_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_intensity;
uniform vec2 u_center;
uniform int u_samples;
uniform bool u_zoom;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
void main() {
vec2 uv = v_texCoord;
vec2 center = u_center;
vec4 color = vec4(0.0);
float total = 0.0;
if (u_zoom) {
// Zoom blur - samples along ray from center
vec2 dir = uv - center;
for (int i = 0; i < 64; i++) {
if (i >= u_samples) break;
float t = float(i) / float(u_samples - 1);
float scale = 1.0 - u_intensity * t;
vec2 sampleUV = center + dir * scale;
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
float weight = 1.0 - t * 0.5;
color += texture2D(u_image, sampleUV) * weight;
total += weight;
}
}
} else {
// Spin blur - samples along circular arc
vec2 dir = uv - center;
float dist = length(dir);
float baseAngle = atan(dir.y, dir.x);
for (int i = 0; i < 64; i++) {
if (i >= u_samples) break;
float t = (float(i) / float(u_samples - 1) - 0.5) * 2.0;
float angle = baseAngle + t * u_intensity * dist;
vec2 sampleUV = center + vec2(cos(angle), sin(angle)) * dist;
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
float weight = 1.0 - abs(t) * 0.3;
color += texture2D(u_image, sampleUV) * weight;
total += weight;
}
}
}
if (total > 0.0) {
color /= total;
} else {
color = texture2D(u_image, uv);
}
gl_FragColor = color;
}
`;
export const MOSAIC_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_cellSize;
uniform float u_irregularity;
uniform float u_edgeThickness;
uniform vec3 u_edgeColor;
uniform float u_colorVariation;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
// Simple hash functions for Voronoi
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453);
}
float hash1(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
void main() {
vec2 uv = v_texCoord;
vec2 pixelCoord = uv * u_resolution;
// Calculate cell coordinates
float cellPx = u_cellSize;
vec2 cellCoord = pixelCoord / cellPx;
vec2 cellBase = floor(cellCoord);
// Find closest Voronoi cell center
float minDist = 10.0;
float secondDist = 10.0;
vec2 closestCell = cellBase;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 neighbor = cellBase + vec2(float(x), float(y));
vec2 point = neighbor + mix(vec2(0.5), hash2(neighbor), u_irregularity);
float dist = length(cellCoord - point);
if (dist < minDist) {
secondDist = minDist;
minDist = dist;
closestCell = neighbor;
} else if (dist < secondDist) {
secondDist = dist;
}
}
}
// Sample color from cell center
vec2 cellCenter = (closestCell + 0.5) * cellPx / u_resolution;
cellCenter = clamp(cellCenter, 0.0, 1.0);
vec4 cellSample = texture2D(u_image, cellCenter);
vec3 cellColor = cellSample.rgb;
// Add slight color variation per cell
float variation = (hash1(closestCell) - 0.5) * u_colorVariation;
cellColor = clamp(cellColor + variation, 0.0, 1.0);
// Calculate edge
float edge = secondDist - minDist;
float edgeMask = smoothstep(0.0, u_edgeThickness, edge);
// Mix cell color with edge color
vec3 result = mix(u_edgeColor, cellColor, edgeMask);
// Preserve original alpha for transparency support
gl_FragColor = vec4(result, cellSample.a);
}
`;
export const TILT_SHIFT_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_focusPosition;
uniform float u_focusWidth;
uniform float u_blurRadius;
uniform float u_angle;
uniform float u_gradientSmooth;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
#define PI 3.14159265359
void main() {
vec2 uv = v_texCoord;
// Rotate coordinates for angled tilt shift
float angleRad = u_angle * PI / 180.0;
vec2 center = vec2(0.5);
vec2 rotatedUV = uv - center;
float cosA = cos(angleRad);
float sinA = sin(angleRad);
float rotatedY = -rotatedUV.x * sinA + rotatedUV.y * cosA;
rotatedY += 0.5;
// Calculate blur amount based on distance from focus band
float distFromFocus = abs(rotatedY - u_focusPosition);
float focusHalf = u_focusWidth * 0.5;
float blurAmount = smoothstep(focusHalf, focusHalf + u_gradientSmooth, distFromFocus);
// Apply Gaussian blur based on blur amount
vec3 color = vec3(0.0);
float total = 0.0;
float radius = u_blurRadius * blurAmount;
if (radius < 0.5) {
// No blur needed
gl_FragColor = texture2D(u_image, uv);
return;
}
vec2 texelSize = 1.0 / u_resolution;
// Optimized blur - sample in a circular pattern
for (int y = -8; y <= 8; y++) {
for (int x = -8; x <= 8; x++) {
float fx = float(x);
float fy = float(y);
float dist = sqrt(fx * fx + fy * fy);
if (dist > radius) continue;
float sigma = radius * 0.4;
float weight = exp(-(dist * dist) / (2.0 * sigma * sigma));
vec2 offset = vec2(fx, fy) * texelSize;
color += texture2D(u_image, uv + offset).rgb * weight;
total += weight;
}
}
if (total > 0.0) {
color /= total;
}
// Preserve original alpha for transparency support
vec4 centerSample = texture2D(u_image, uv);
gl_FragColor = vec4(color, centerSample.a);
}
`;
export const EXPOSURE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_exposure;
uniform float u_highlights;
uniform float u_shadows;
uniform float u_blacks;
uniform float u_whites;
varying vec2 v_texCoord;
// Attempt to recover highlights and shadows
vec3 adjustTones(vec3 color, float highlights, float shadows, float blacks, float whites) {
// Calculate luminance
float luma = dot(color, vec3(0.299, 0.587, 0.114));
// Highlights adjustment (affects bright areas)
float highlightMask = smoothstep(0.5, 1.0, luma);
color = mix(color, color * (1.0 + highlights), highlightMask);
// Shadows adjustment (affects dark areas)
float shadowMask = 1.0 - smoothstep(0.0, 0.5, luma);
color = mix(color, color * (1.0 + shadows * 2.0), shadowMask);
// Blacks - crush or lift the darkest values
color = max(color + blacks * 0.1, 0.0);
// Whites - clip or extend the brightest values
color = min(color + whites * 0.1, 1.0 + whites * 0.1);
return color;
}
void main() {
vec4 color = texture2D(u_image, v_texCoord);
// Apply exposure (EV stops)
vec3 result = color.rgb * pow(2.0, u_exposure);
// Apply tonal adjustments
result = adjustTones(result, u_highlights, u_shadows, u_blacks, u_whites);
gl_FragColor = vec4(clamp(result, 0.0, 1.0), color.a);
}
`;
export const VIBRANCE_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform float u_vibrance;
uniform float u_saturation;
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
// Calculate saturation info
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
float maxChannel = max(max(color.r, color.g), color.b);
float minChannel = min(min(color.r, color.g), color.b);
float saturation = (maxChannel - minChannel) / (maxChannel + 0.001);
// Vibrance: boost saturation more for less-saturated pixels
// This protects already-saturated colors (like skin tones)
float vibranceAmount = u_vibrance * (1.0 - saturation);
vibranceAmount = max(vibranceAmount, -saturation); // Don't oversaturate
vec3 result = mix(vec3(luma), color.rgb, 1.0 + vibranceAmount);
// Standard saturation on top
result = mix(vec3(luma), result, 1.0 + u_saturation);
gl_FragColor = vec4(clamp(result, 0.0, 1.0), color.a);
}
`;
export const DOT_DITHER_FRAGMENT_SHADER = `
precision highp float;
uniform sampler2D u_image;
uniform vec2 u_resolution;
uniform float u_threshold;
uniform float u_scale;
uniform bool u_animated;
uniform float u_time;
uniform float u_animationSpeed;
uniform bool u_invert;
varying vec2 v_texCoord;
// Checkerboard noise to decorrelate pattern between tiles
float stepnoise0(vec2 p, float size) {
vec2 pp = floor(p / size) * size;
float r = fract(sin(dot(pp, vec2(1.0, -7.131))) * 43758.5453);
return r;
}
// Stippling mask - creates the dot pattern
float mask(vec2 p, float time) {
const float SEED1 = 1.705;
const float DMUL = 8.12235325;
// Add per-tile random offset to break up regularity
float tileNoise = stepnoise0(p, 5.5);
p += (tileNoise - 0.5) * DMUL;
// Animation: shift the pattern over time
if (time > 0.0) {
p += vec2(sin(time * 0.7), cos(time * 0.9)) * 2.0;
}
// Create stippling pattern
float f = fract(p.x * SEED1 + p.y / (SEED1 + 0.15555));
f *= 1.03; // Avoid zero-stipple in plain white
// Tone mapping for proper dot distribution
return (pow(f, 150.0) + 1.3 * f) / 2.3;
}
void main() {
vec2 fragCoord = v_texCoord * u_resolution;
vec4 originalColor = texture2D(u_image, v_texCoord);
// Get luminance
float luminance = dot(originalColor.rgb, vec3(0.2126, 0.7152, 0.0722));
// Apply threshold adjustment
luminance = luminance + (u_threshold - 0.5);
luminance = clamp(luminance, 0.0, 1.0);
// Scale controls dot density - larger scale = bigger dots / less dense
// We floor to create pixel-sized dots that scale together
float dotSize = max(1.0, floor(u_scale));
vec2 scaledCoord = floor(fragCoord / dotSize);
// Get animation time
float time = u_animated ? u_time * u_animationSpeed : 0.0;
// Get stippling mask value
float maskVal = mask(scaledCoord, time);
// Compare luminance against mask to get final bit
float finalBit = step(maskVal, luminance);
if (u_invert) {
finalBit = 1.0 - finalBit;
}
gl_FragColor = vec4(vec3(finalBit), originalColor.a);
}
`;