Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/app/(shaders)/warp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const WarpWithControls = () => {

const [params, setParams] = useControls(() => {
return {
contour: { value: defaults.contour, min: 0, max: 1, order: 10 },
antialiasing: { value: defaults.antialiasing, order: 11 },
proportion: { value: defaults.proportion, min: 0, max: 1, order: 100 },
softness: { value: defaults.softness, min: 0, max: 1, order: 101 },
distortion: { value: defaults.distortion, min: 0, max: 1, order: 102 },
Expand Down
7 changes: 7 additions & 0 deletions docs/src/shader-defs/warp-def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ export const warpDef: ShaderDef = {
defaultValue: defaultParams.shapeScale,
description: 'Zoom level of the base pattern',
},
{
name: 'antialiasing',
type: 'boolean',
defaultValue: defaultParams.antialiasing,
description: 'Fixed 4x4 supersampling; needed for crisp low-softness bands under strong distortion/swirl',
options: ['true', 'false'],
},
...animatedCommonParams,
],
};
16 changes: 16 additions & 0 deletions packages/shaders-react/src/shaders/warp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const defaultPreset: WarpPreset = {
swirlIterations: 10,
shapeScale: 0.1,
shape: 'checks',
contour: 0,
antialiasing: false
},
};

Expand All @@ -51,6 +53,8 @@ export const presetCauldron: WarpPreset = {
swirlIterations: 7,
shapeScale: 0.6,
shape: 'edge',
contour: 0,
antialiasing: false
},
};

Expand All @@ -71,6 +75,8 @@ export const presetInk: WarpPreset = {
swirlIterations: 10,
shapeScale: 0.28,
shape: 'checks',
contour: 0,
antialiasing: true
},
};

Expand All @@ -90,6 +96,8 @@ export const presetKelp: WarpPreset = {
swirlIterations: 3,
shapeScale: 1,
shape: 'stripes',
contour: 0,
antialiasing: false
},
};

Expand All @@ -110,6 +118,8 @@ export const presetNectar: WarpPreset = {
swirlIterations: 10,
shapeScale: 0.75,
shape: 'edge',
contour: 0,
antialiasing: false
},
};

Expand All @@ -129,6 +139,8 @@ export const presetPassion: WarpPreset = {
swirlIterations: 6,
shapeScale: 0.25,
shape: 'checks',
contour: 0,
antialiasing: false
},
};

Expand All @@ -153,6 +165,8 @@ export const Warp: React.FC<WarpProps> = memo(function WarpImpl({
swirlIterations = defaultPreset.params.swirlIterations,
shapeScale = defaultPreset.params.shapeScale,
shape = defaultPreset.params.shape,
contour = defaultPreset.params.contour,
antialiasing = defaultPreset.params.antialiasing,

// Sizing props
fit = defaultPreset.params.fit,
Expand All @@ -177,6 +191,8 @@ export const Warp: React.FC<WarpProps> = memo(function WarpImpl({
u_swirlIterations: swirlIterations,
u_shapeScale: shapeScale,
u_shape: WarpPatterns[shape],
u_contour: contour,
u_antialiasing: antialiasing,
u_noiseTexture: getShaderNoiseTexture(),

// Sizing uniforms
Expand Down
152 changes: 110 additions & 42 deletions packages/shaders/src/shaders/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const warpMeta = {
* - u_distortion (float): Strength of noise-based distortion (0 to 1)
* - u_swirl (float): Strength of the swirl distortion (0 to 1)
* - u_swirlIterations (float): Number of layered swirl passes, effective with swirl > 0 (0 to 20)
* - u_contour (float): Strength of the contour band traced around the canvas element borders (0 to 1)
* - u_antialiasing (bool): Enables fixed 4x4 supersampling; needed for crisp low-softness bands under strong distortion/swirl
* - u_resolution (vec2): Canvas resolution in pixels, used to keep the contour locked to the canvas borders
* - u_pixelRatio (float): Device pixel ratio, used to keep the contour thickness consistent across displays
* - u_noiseTexture (sampler2D): Pre-computed randomizer source texture
*
* Vertex shader outputs (used in fragment shader):
Expand All @@ -46,14 +50,14 @@ export const warpMeta = {

// language=GLSL
export const warpFragmentShader: string = `#version 300 es
precision mediump float;
precision lowp float;

uniform float u_time;
uniform float u_scale;

uniform sampler2D u_noiseTexture;

uniform vec4 u_colors[${ warpMeta.maxColorCount }];
uniform vec4 u_colors[${warpMeta.maxColorCount}];
uniform float u_colorsCount;
uniform float u_proportion;
uniform float u_softness;
Expand All @@ -62,13 +66,17 @@ uniform float u_shapeScale;
uniform float u_distortion;
uniform float u_swirl;
uniform float u_swirlIterations;
uniform float u_contour;
uniform bool u_antialiasing;
uniform mediump vec2 u_resolution;
uniform mediump float u_pixelRatio;

in vec2 v_patternUV;

out vec4 fragColor;

${ declarePI }
${ rotation2 }
${declarePI}
${rotation2}
float randomG(vec2 p) {
vec2 uv = floor(p) / 100. + .5;
return texture(u_noiseTexture, fract(uv)).g;
Expand All @@ -86,59 +94,43 @@ float valueNoise(vec2 st) {
return mix(x1, x2, u.y);
}


void main() {
vec2 uv = v_patternUV;
uv *= .5;

const float firstFrameOffset = 118.;
float t = 0.0625 * (u_time + firstFrameOffset);

float n1 = valueNoise(uv * 1. + t);
float n2 = valueNoise(uv * 2. - t);
float angle = n1 * TWO_PI;
uv.x += 4. * u_distortion * n2 * cos(angle);
uv.y += 4. * u_distortion * n2 * sin(angle);

float swirl = u_swirl;
for (int i = 1; i <= 20; i++) {
if (i >= int(u_swirlIterations)) break;
float iFloat = float(i);
// swirl *= (1. - smoothstep(.0, .25, length(fwidth(uv))));
uv.x += swirl / iFloat * cos(t + iFloat * 1.5 * uv.y);
uv.y += swirl / iFloat * cos(t + iFloat * 1. * uv.x);
}

// The scalar base pattern (checks / stripes / edge) evaluated at a warped UV.
float patternShape(vec2 uv) {
float proportion = clamp(u_proportion, 0., 1.);

float shape = 0.;
float proportionOffset = .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5);
float shape;
if (u_shape < .5) {
vec2 checksShape_uv = uv * (.5 + 3.5 * u_shapeScale);
shape = .5 + .5 * sin(checksShape_uv.x) * cos(checksShape_uv.y);
shape += .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5);
vec2 p = uv * (.5 + 3.5 * u_shapeScale);
shape = .5 + .5 * sin(p.x) * cos(p.y);
shape += proportionOffset;
} else if (u_shape < 1.5) {
vec2 stripesShape_uv = uv * (2. * u_shapeScale);
float f = fract(stripesShape_uv.y);
float f = fract(uv.y * (2. * u_shapeScale));
shape = smoothstep(.0, .55, f) * (1.0 - smoothstep(.45, 1., f));
shape += .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5);
shape += proportionOffset;
} else {
float shapeScaling = 5. * (1. - u_shapeScale);
float e0 = 0.45 - shapeScaling;
float e1 = 0.55 + shapeScaling;
shape = smoothstep(min(e0, e1), max(e0, e1), 1.0 - uv.y + 0.3 * (proportion - 0.5));
}
return shape;
}

// Map the scalar pattern onto the color bands. u_softness sets band sharpness; aaW is
// a screen-space floor that antialiases individual band edges (needed when a pixel
// takes only 1 sample). Multi-crossing AA is handled by supersampling in main.
vec4 shadeBands(float shape, float aaW) {
float mixer = shape * (u_colorsCount - 1.);
vec4 gradient = u_colors[0];
gradient.rgb *= gradient.a;
float aa = fwidth(shape);
for (int i = 1; i < ${ warpMeta.maxColorCount }; i++) {
for (int i = 1; i < ${warpMeta.maxColorCount}; i++) {
if (i >= int(u_colorsCount)) break;
float m = clamp(mixer - float(i - 1), 0.0, 1.0);

float localMixerStart = floor(m);
float softness = .5 * u_softness + fwidth(m);
float smoothed = smoothstep(max(0., .5 - softness - aa), min(1., .5 + softness + aa), m - localMixerStart);
float w = .5 * u_softness + aaW;
float e = m - localMixerStart;
float smoothed = w > 0. ? smoothstep(max(0., .5 - w), min(1., .5 + w), e) : step(.5, e);
float stepped = localMixerStart + smoothed;

m = mix(stepped, m, u_softness);
Expand All @@ -147,11 +139,83 @@ void main() {
c.rgb *= c.a;
gradient = mix(gradient, c, m);
}
return gradient;
}


// Contour band mask, locked to the <canvas> borders. Smooth in screen space, so it is
// a reliable per-pixel signal (unlike derivatives of the folded warp).
float contourEdge(vec2 fragCoord) {
const float MAX_THICKNESS = .5;
vec2 borderUV = fragCoord / u_resolution;
vec2 mask = min(borderUV, 1. - borderUV);
vec2 pixel_thickness = min(400. * u_pixelRatio / u_resolution, vec2(MAX_THICKNESS));
float maskX = pow(smoothstep(0.0, pixel_thickness.x, mask.x), .2);
float maskY = pow(smoothstep(0.0, pixel_thickness.y, mask.y), .2);
return clamp(1. - maskX * maskY, 0., 1.);
}

vec2 warpUV(vec2 uv, vec2 fragCoord, float t) {
float edge = contourEdge(fragCoord);

float n1 = valueNoise(uv * 1. + t);
float n2 = valueNoise(uv * 2. - t);
float angle = n1 * TWO_PI;

float edgeFadeW = u_contour * edge;
float edgeFade = u_contour * pow(edge, 5.);
uv -= vec2(.5);
uv = rotate(uv, -edgeFade * angle);
uv += vec2(.5);

uv.x += 4. * u_distortion * n2 * cos(angle) * (1. - edgeFadeW);
uv.y += 4. * u_distortion * n2 * sin(angle) * (1. - edgeFadeW);

float swirl = u_swirl * (1. - edgeFadeW);
for (int i = 1; i <= 20; i++) {
if (i >= int(u_swirlIterations)) break;
float iFloat = float(i);
uv.x += swirl / iFloat * cos(t + iFloat * 1.5 * uv.y);
uv.y += swirl / iFloat * cos(t + iFloat * 1. * uv.x);
}
return uv;
}


void main() {
vec2 uv = v_patternUV * .5;

const float firstFrameOffset = 118.;
float t = 0.0625 * (u_time + firstFrameOffset);

vec4 acc;
if (u_antialiasing) {
vec2 duvdx = dFdx(uv);
vec2 duvdy = dFdy(uv);
const int AA_ROOT = 4;
acc = vec4(0.);
for (int i = 0; i < AA_ROOT * AA_ROOT; i++) {
int iy = i / AA_ROOT;
int ix = i - iy * AA_ROOT;
vec2 cell = (vec2(float(ix), float(iy)) + .5) / float(AA_ROOT) - .5;
// rotate ~26.57deg so the grid never resonates with the checks/stripes axes
vec2 o = vec2(cell.x * .8944 - cell.y * .4472, cell.x * .4472 + cell.y * .8944);
vec2 sBase = uv + o.x * duvdx + o.y * duvdy;
acc += shadeBands(patternShape(warpUV(sBase, gl_FragCoord.xy + o, t)), 0.);
}
acc /= float(AA_ROOT * AA_ROOT);
} else {
// Single sample + a cheap analytic floor that antialiases lone band edges.
vec2 wc = warpUV(uv, gl_FragCoord.xy, t);
float shapeC = patternShape(wc);
float aaW = min(fwidth(shapeC * (u_colorsCount - 1.)), .5);
acc = shadeBands(shapeC, aaW);
}

vec3 color = gradient.rgb;
float opacity = gradient.a;
vec3 color = acc.rgb;
float opacity = acc.a;

${ colorBandingFix }
${colorBandingFix}

fragColor = vec4(color, opacity);
}
Expand All @@ -167,6 +231,8 @@ export interface WarpUniforms extends ShaderSizingUniforms {
u_distortion: number;
u_swirl: number;
u_swirlIterations: number;
u_contour: number;
u_antialiasing: boolean;
u_noiseTexture?: HTMLImageElement;
}

Expand All @@ -180,6 +246,8 @@ export interface WarpParams extends ShaderSizingParams, ShaderMotionParams {
distortion?: number;
swirl?: number;
swirlIterations?: number;
contour?: number;
antialiasing?: boolean;
}

export const WarpPatterns = {
Expand Down
Loading