From bb08a49ff9f86ed1a75371c73b0ad121c49570df Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 31 Oct 2025 13:58:09 +0000 Subject: [PATCH 001/186] default fit cover => contain --- .../shaders-react/src/shaders/paper-texture.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 81cc82edf..df7a283dd 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -29,8 +29,8 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - fit: 'cover', - scale: 0.6, + fit: 'contain', + scale: 0.8, speed: 0, frame: 0, colorFront: '#9fadbc', @@ -53,10 +53,10 @@ export const abstractPreset: PaperTexturePreset = { name: 'Abstract', params: { ...defaultObjectSizing, - fit: 'cover', + fit: 'contain', speed: 0, frame: 0, - scale: 0.6, + scale: 0.8, colorFront: '#00eeff', colorBack: '#ff0a81', contrast: 0.85, @@ -77,10 +77,10 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - fit: 'cover', + fit: 'contain', speed: 0, frame: 0, - scale: 0.6, + scale: 0.8, colorFront: '#c7b89e', colorBack: '#999180', contrast: 0.4, @@ -103,7 +103,7 @@ export const detailsPreset: PaperTexturePreset = { ...defaultObjectSizing, speed: 0, frame: 0, - fit: 'cover', + fit: 'contain', scale: 3, colorFront: '#00000000', colorBack: '#00000000', From ccf26501fa34c21d578c8bcb2c3a23037e8c9a5b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 31 Oct 2025 14:33:10 +0000 Subject: [PATCH 002/186] blending param added + default blending improved --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + .../src/shaders/paper-texture.tsx | 6 ++++++ packages/shaders/src/shaders/paper-texture.ts | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index a8ec94dcf..c9685d31c 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -79,6 +79,7 @@ const PaperTextureWithControls = () => { foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 207 }, drops: { value: defaults.drops, min: 0, max: 1, order: 209 }, fade: { value: defaults.fade, min: 0, max: 1, order: 208 }, + blending: { value: defaults.blending, min: 0, max: 1, order: 0 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index df7a283dd..df34f9974 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -29,6 +29,7 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, + blending: 0, fit: 'contain', scale: 0.8, speed: 0, @@ -53,6 +54,7 @@ export const abstractPreset: PaperTexturePreset = { name: 'Abstract', params: { ...defaultObjectSizing, + blending: 0, fit: 'contain', speed: 0, frame: 0, @@ -77,6 +79,7 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, + blending: 0, fit: 'contain', speed: 0, frame: 0, @@ -101,6 +104,7 @@ export const detailsPreset: PaperTexturePreset = { name: 'Details', params: { ...defaultObjectSizing, + blending: 0, speed: 0, frame: 0, fit: 'contain', @@ -142,6 +146,7 @@ export const PaperTexture: React.FC = memo(function PaperText folds = defaultPreset.params.folds, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, + blending = defaultPreset.params.blending, // Reworked props fiberScale, @@ -183,6 +188,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_fade: fade, u_drops: drops, u_seed: seed, + u_blending: blending, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0b341b718..5cec8fcf2 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -44,6 +44,7 @@ uniform float u_foldCount; uniform float u_drops; uniform float u_seed; uniform float u_fade; +uniform float u_blending; uniform sampler2D u_noiseTexture; @@ -177,6 +178,13 @@ float lst(float edge0, float edge1, float x) { return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } +vec3 blendMultiply(vec3 base, vec3 blend) { + return base*blend; +} +vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { + return (blendMultiply(base, blend) * opacity + base * (1.0 - opacity)); +} + void main() { vec2 imageUV = v_imageUV; @@ -240,7 +248,7 @@ void main() { imageUV += .02 * normalImage; float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); - image.rgb += .6 * pow(u_contrast, .4) * (res - .7); + image.rgb += .6 * pow(u_contrast, .4) * (.3 - res); frame *= image.a; @@ -253,7 +261,11 @@ void main() { color -= .007 * drops; - color.rgb = mix(color, image.rgb, frame); + float blending = 1. - u_blending; + float blendOpacity = (.5 + .5 * res); + vec3 pic = blendMultiply(color, image.rgb + .3 * blending * length(normalImage), .5 * res + .5 * u_blending); + pic = mix(pic, image.rgb, blending); + color = mix(color, pic, frame); fragColor = vec4(color, opacity); } @@ -275,6 +287,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_crumpleSize: number; u_drops: number; u_seed: number; + u_blending: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -292,4 +305,5 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara crumpleSize?: number; drops?: number; seed?: number; + blending?: number; } From 453d71772daa6f2a266302ededdf05eaa9b4bfd3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 31 Oct 2025 16:35:06 +0000 Subject: [PATCH 003/186] params order swap --- docs/src/app/(shaders)/paper-texture/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index c9685d31c..21a40e169 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -69,7 +69,7 @@ const PaperTextureWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - contrast: { value: defaults.contrast, min: 0, max: 1, order: 200 }, + contrast: { value: defaults.contrast, min: 0, max: 1, order: 199 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 202 }, fiberSize: { value: defaults.fiberSize, min: 0.01, max: 1, order: 203 }, @@ -79,7 +79,7 @@ const PaperTextureWithControls = () => { foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 207 }, drops: { value: defaults.drops, min: 0, max: 1, order: 209 }, fade: { value: defaults.fade, min: 0, max: 1, order: 208 }, - blending: { value: defaults.blending, min: 0, max: 1, order: 0 }, + blending: { value: defaults.blending, min: 0, max: 1, order: 200 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, From 9fc23a4bc3981a34d1d3b6488953ed60e5133b97 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 31 Oct 2025 16:54:03 +0000 Subject: [PATCH 004/186] opacity added --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + packages/shaders-react/src/shaders/paper-texture.tsx | 6 ++++++ packages/shaders/src/shaders/paper-texture.ts | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 21a40e169..498405147 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -70,6 +70,7 @@ const PaperTextureWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, contrast: { value: defaults.contrast, min: 0, max: 1, order: 199 }, + opacity: { value: defaults.opacity, min: 0, max: 1, order: 199 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 202 }, fiberSize: { value: defaults.fiberSize, min: 0.01, max: 1, order: 203 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index df34f9974..0bc9f4823 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -37,6 +37,7 @@ export const defaultPreset: PaperTexturePreset = { colorFront: '#9fadbc', colorBack: '#ffffff', contrast: 0.3, + opacity: 1, roughness: 0.4, fiber: 0.3, fiberSize: 0.2, @@ -62,6 +63,7 @@ export const abstractPreset: PaperTexturePreset = { colorFront: '#00eeff', colorBack: '#ff0a81', contrast: 0.85, + opacity: 1, roughness: 0, fiber: 0.1, fiberSize: 0.2, @@ -87,6 +89,7 @@ export const cardboardPreset: PaperTexturePreset = { colorFront: '#c7b89e', colorBack: '#999180', contrast: 0.4, + opacity: 1, roughness: 0, fiber: 0.35, fiberSize: 0.14, @@ -112,6 +115,7 @@ export const detailsPreset: PaperTexturePreset = { colorFront: '#00000000', colorBack: '#00000000', contrast: 0, + opacity: 1, roughness: 1, fiber: 0.27, fiberSize: 0.22, @@ -140,6 +144,7 @@ export const PaperTexture: React.FC = memo(function PaperText colorBack = defaultPreset.params.colorBack, image = '', contrast = defaultPreset.params.contrast, + opacity = defaultPreset.params.opacity, roughness = defaultPreset.params.roughness, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, @@ -178,6 +183,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_colorFront: getShaderColorFromString(colorFront), u_colorBack: getShaderColorFromString(colorBack), u_contrast: contrast, + u_opacity: opacity, u_roughness: roughness, u_fiber: fiber, u_fiberSize: fiberSize, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5cec8fcf2..9f3c584e3 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -34,6 +34,7 @@ uniform sampler2D u_image; uniform float u_imageAspectRatio; uniform float u_contrast; +uniform float u_opacity; uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; @@ -265,7 +266,7 @@ void main() { float blendOpacity = (.5 + .5 * res); vec3 pic = blendMultiply(color, image.rgb + .3 * blending * length(normalImage), .5 * res + .5 * u_blending); pic = mix(pic, image.rgb, blending); - color = mix(color, pic, frame); + color = mix(color, pic, frame * u_opacity); fragColor = vec4(color, opacity); } @@ -277,6 +278,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_colorBack: [number, number, number, number]; u_contrast: number; + u_opacity: number; u_roughness: number; u_fiber: number; u_fiberSize: number; @@ -295,6 +297,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara colorFront?: string; colorBack?: string; contrast?: number; + opacity?: number; roughness?: number; fiber?: number; fiberSize?: number; From d24cfa360e4bb2993c7f3a924a4da51e26a8dc46 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 31 Oct 2025 17:51:01 +0000 Subject: [PATCH 005/186] roughness to use save UV as other pattern components --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9f3c584e3..4b9d41f56 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -192,7 +192,7 @@ void main() { vec2 patternUV = v_imageUV - .5; patternUV = 5. * (patternUV * vec2(u_imageAspectRatio, 1.)); - vec2 roughnessUv = 1.5 * (gl_FragCoord.xy - .5 * u_resolution) / u_pixelRatio; + vec2 roughnessUv = 120. * patternUV; float roughness = roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.)); vec2 crumplesUV = fract(patternUV * .02 / u_crumpleSize - u_seed) * 32.; From 9211b7e062fd14a201b447222746032b9745c250 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 3 Nov 2025 11:10:42 +0000 Subject: [PATCH 006/186] new u_distortion control --- docs/src/app/(shaders)/paper-texture/page.tsx | 5 +++-- packages/shaders-react/src/shaders/paper-texture.tsx | 6 ++++++ packages/shaders/src/shaders/paper-texture.ts | 7 +++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 498405147..629dfa1d4 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -69,8 +69,9 @@ const PaperTextureWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - contrast: { value: defaults.contrast, min: 0, max: 1, order: 199 }, - opacity: { value: defaults.opacity, min: 0, max: 1, order: 199 }, + contrast: { value: defaults.contrast, min: 0, max: 1, order: 197 }, + opacity: { value: defaults.opacity, min: 0, max: 1, order: 198 }, + distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 202 }, fiberSize: { value: defaults.fiberSize, min: 0.01, max: 1, order: 203 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 0bc9f4823..00019214d 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -30,6 +30,7 @@ export const defaultPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 0, + distortion: 0.5, fit: 'contain', scale: 0.8, speed: 0, @@ -56,6 +57,7 @@ export const abstractPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 0, + distortion: 0.5, fit: 'contain', speed: 0, frame: 0, @@ -82,6 +84,7 @@ export const cardboardPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 0, + distortion: 0.5, fit: 'contain', speed: 0, frame: 0, @@ -108,6 +111,7 @@ export const detailsPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 0, + distortion: 0.5, speed: 0, frame: 0, fit: 'contain', @@ -152,6 +156,7 @@ export const PaperTexture: React.FC = memo(function PaperText drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, + distortion = defaultPreset.params.distortion, // Reworked props fiberScale, @@ -195,6 +200,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_drops: drops, u_seed: seed, u_blending: blending, + u_distortion: distortion, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 4b9d41f56..978873999 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -46,6 +46,7 @@ uniform float u_drops; uniform float u_seed; uniform float u_fade; uniform float u_blending; +uniform float u_distortion; uniform sampler2D u_noiseTexture; @@ -235,7 +236,7 @@ void main() { normal.xy += u_roughness * 1.5 * roughness; normal.xy += fiber; - normalImage += u_roughness * .75 * roughness; + normalImage += u_roughness * .3 * roughness; normalImage += .2 * fiber; vec3 lightPos = vec3(1., 2., 1.); @@ -246,7 +247,7 @@ void main() { vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; - imageUV += .02 * normalImage; + imageUV += .1 * u_distortion * normalImage; float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); image.rgb += .6 * pow(u_contrast, .4) * (.3 - res); @@ -290,6 +291,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_drops: number; u_seed: number; u_blending: number; + u_distortion: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -309,4 +311,5 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara drops?: number; seed?: number; blending?: number; + distortion?: number; } From fec0d3d0ebf41ab2ef43c361bc60ad262e5608a0 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 16:45:37 +0100 Subject: [PATCH 007/186] opacity removes, 3 x sizes adjusted --- docs/src/app/(shaders)/paper-texture/page.tsx | 22 +++++++++---------- .../src/shaders/paper-texture.tsx | 16 +++++++------- packages/shaders/src/shaders/paper-texture.ts | 14 ++++++------ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 629dfa1d4..0d246d7f6 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -70,19 +70,19 @@ const PaperTextureWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, contrast: { value: defaults.contrast, min: 0, max: 1, order: 197 }, - opacity: { value: defaults.opacity, min: 0, max: 1, order: 198 }, + blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, - fiber: { value: defaults.fiber, min: 0, max: 1, order: 202 }, - fiberSize: { value: defaults.fiberSize, min: 0.01, max: 1, order: 203 }, - crumples: { value: defaults.crumples, min: 0, max: 1, order: 204 }, - crumpleSize: { value: defaults.crumpleSize, min: 0.01, max: 1, order: 205 }, - folds: { value: defaults.folds, min: 0, max: 1, order: 206 }, - foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 207 }, - drops: { value: defaults.drops, min: 0, max: 1, order: 209 }, - fade: { value: defaults.fade, min: 0, max: 1, order: 208 }, - blending: { value: defaults.blending, min: 0, max: 1, order: 200 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, + roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, + fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, + fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, + crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, + crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, + folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, + foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 231 }, + drops: { value: defaults.drops, min: 0, max: 1, order: 240 }, + fade: { value: defaults.fade, min: 0, max: 1, order: 250 }, + seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 251 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 04ab9ef46..a3ea86e00 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,12 +38,12 @@ export const defaultPreset: PaperTexturePreset = { colorFront: '#9fadbc', colorBack: '#ffffff', contrast: 0.3, - opacity: 1, roughness: 0.4, + roughnessSize: 0.5, fiber: 0.3, - fiberSize: 0.2, + fiberSize: 0.5, crumples: 0.3, - crumpleSize: 0.35, + crumpleSize: 0.5, folds: 0.65, foldCount: 5, fade: 0, @@ -65,8 +65,8 @@ export const abstractPreset: PaperTexturePreset = { colorFront: '#00eeff', colorBack: '#ff0a81', contrast: 0.85, - opacity: 1, roughness: 0, + roughnessSize: 0.5, fiber: 0.1, fiberSize: 0.2, crumples: 0, @@ -92,8 +92,8 @@ export const cardboardPreset: PaperTexturePreset = { colorFront: '#c7b89e', colorBack: '#999180', contrast: 0.4, - opacity: 1, roughness: 0, + roughnessSize: 0.5, fiber: 0.35, fiberSize: 0.14, crumples: 0.7, @@ -119,8 +119,8 @@ export const detailsPreset: PaperTexturePreset = { colorFront: '#00000000', colorBack: '#00000000', contrast: 0, - opacity: 1, roughness: 1, + roughnessSize: 0.5, fiber: 0.27, fiberSize: 0.22, crumples: 1, @@ -148,8 +148,8 @@ export const PaperTexture: React.FC = memo(function PaperText colorBack = defaultPreset.params.colorBack, image = '', contrast = defaultPreset.params.contrast, - opacity = defaultPreset.params.opacity, roughness = defaultPreset.params.roughness, + roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, @@ -188,8 +188,8 @@ export const PaperTexture: React.FC = memo(function PaperText u_colorFront: getShaderColorFromString(colorFront), u_colorBack: getShaderColorFromString(colorBack), u_contrast: contrast, - u_opacity: opacity, u_roughness: roughness, + u_roughnessSize: roughnessSize, u_fiber: fiber, u_fiberSize: fiberSize, u_crumples: crumples, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 978873999..8f096d2b2 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -34,7 +34,7 @@ uniform sampler2D u_image; uniform float u_imageAspectRatio; uniform float u_contrast; -uniform float u_opacity; +uniform float u_roughnessSize; uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; @@ -193,13 +193,13 @@ void main() { vec2 patternUV = v_imageUV - .5; patternUV = 5. * (patternUV * vec2(u_imageAspectRatio, 1.)); - vec2 roughnessUv = 120. * patternUV; + vec2 roughnessUv = mix(330., 130., u_roughnessSize) * patternUV; float roughness = roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.)); - vec2 crumplesUV = fract(patternUV * .02 / u_crumpleSize - u_seed) * 32.; + vec2 crumplesUV = fract(mix(.45, .02, pow(u_crumpleSize, .3)) * patternUV - u_seed) * 32.; float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.05, 0.)) - crumplesShape(crumplesUV)); - vec2 fiberUV = 2. / u_fiberSize * patternUV; + vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; float fiber = fiberNoise(fiberUV, vec2(0.)); fiber = .5 * u_fiber * (fiber - 1.); @@ -267,7 +267,7 @@ void main() { float blendOpacity = (.5 + .5 * res); vec3 pic = blendMultiply(color, image.rgb + .3 * blending * length(normalImage), .5 * res + .5 * u_blending); pic = mix(pic, image.rgb, blending); - color = mix(color, pic, frame * u_opacity); + color = mix(color, pic, frame); fragColor = vec4(color, opacity); } @@ -279,8 +279,8 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_colorBack: [number, number, number, number]; u_contrast: number; - u_opacity: number; u_roughness: number; + u_roughnessSize: number; u_fiber: number; u_fiberSize: number; u_crumples: number; @@ -299,8 +299,8 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara colorFront?: string; colorBack?: string; contrast?: number; - opacity?: number; roughness?: number; + roughnessSize?: number; fiber?: number; fiberSize?: number; crumples?: number; From 9142700cb35aa3ea6cadbaeb3d7ed4e23af6f594 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 16:59:56 +0100 Subject: [PATCH 008/186] glsl clean-up --- packages/shaders/src/shader-utils.ts | 43 +-------- packages/shaders/src/shaders/paper-texture.ts | 88 +++++++++++++------ 2 files changed, 64 insertions(+), 67 deletions(-) diff --git a/packages/shaders/src/shader-utils.ts b/packages/shaders/src/shader-utils.ts index c816fc716..124aea9cb 100644 --- a/packages/shaders/src/shader-utils.ts +++ b/packages/shaders/src/shader-utils.ts @@ -88,45 +88,4 @@ float snoise(vec2 v) { g.yz = a0.yz * x12.xz + h.yz * x12.yw; return 130.0 * dot(m, g); } -`; - -// language=GLSL -export const fiberNoise = ` -float fiberRandom(vec2 p) { - vec2 uv = floor(p) / 100.; - return texture(u_noiseTexture, fract(uv)).b; -} - -float fiberValueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = fiberRandom(i); - float b = fiberRandom(i + vec2(1.0, 0.0)); - float c = fiberRandom(i + vec2(0.0, 1.0)); - float d = fiberRandom(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} - -float fiberNoiseFbm(in vec2 n, vec2 seedOffset) { - float total = 0.0, amplitude = 1.; - for (int i = 0; i < 4; i++) { - n = rotate(n, .7); - total += fiberValueNoise(n + seedOffset) * amplitude; - n *= 2.; - amplitude *= 0.6; - } - return total; -} - -float fiberNoise(vec2 uv, vec2 seedOffset) { - float epsilon = 0.001; - float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset); - float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset); - float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset); - float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset); - return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon); -} -`; +`; \ No newline at end of file diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 8f096d2b2..f34097869 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { rotation2, declarePI, fiberNoise, textureRandomizerR } from '../shader-utils.js'; +import { rotation2, declarePI, textureRandomizerR, textureRandomizerGB } from '../shader-utils.js'; /** * Mimicking paper texture with a combination of noises @@ -103,20 +103,57 @@ float roughness(vec2 p) { vec4 w = vec4(floor(p), ceil(p)); vec2 f = fract(p); o += mix( - mix(randomG(w.xy), randomG(w.xw), f.y), - mix(randomG(w.zy), randomG(w.zw), f.y), - f.x); + mix(randomG(w.xy), randomG(w.xw), f.y), + mix(randomG(w.zy), randomG(w.zw), f.y), + f.x); o += .2 / exp(2. * abs(sin(.2 * p.x + .5 * p.y))); - } + } return o / 3.; } -${fiberNoise} +float fiberRandom(vec2 p) { + vec2 uv = floor(p) / 100.; + return texture(u_noiseTexture, fract(uv)).b; +} + +float fiberValueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = fiberRandom(i); + float b = fiberRandom(i + vec2(1.0, 0.0)); + float c = fiberRandom(i + vec2(0.0, 1.0)); + float d = fiberRandom(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} + +float fiberNoiseFbm(vec2 n, vec2 seedOffset) { + float total = 0.0, amplitude = 1.; + for (int i = 0; i < 4; i++) { + n = rotate(n, .7); + total += fiberValueNoise(n + seedOffset) * amplitude; + n *= 2.; + amplitude *= 0.6; + } + return total; +} + +float fiberNoise(vec2 uv, vec2 seedOffset) { + float epsilon = 0.001; + float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset); + float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset); + float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset); + float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset); + return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon); +} vec2 randomGB(vec2 p) { vec2 uv = floor(p) / 50. + .5; return texture(u_noiseTexture, fract(uv)).gb; } + float crumpledNoise(vec2 t, float pw) { vec2 p = floor(t); float wsum = 0.; @@ -135,28 +172,28 @@ float crumpledNoise(vec2 t, float pw) { } return pow(wsum != 0.0 ? cl / wsum : 0.0, .5) * 2.; } + float crumplesShape(vec2 uv) { return crumpledNoise(uv * .25, 16.) * crumpledNoise(uv * .5, 2.); } - vec2 folds(vec2 uv) { - vec3 pp = vec3(0.); - float l = 9.; - for (float i = 0.; i < 15.; i++) { - if (i >= u_foldCount) break; - vec2 rand = randomGB(vec2(i, i * u_seed)); - float an = rand.x * TWO_PI; - vec2 p = vec2(cos(an), sin(an)) * rand.y; - float dist = distance(uv, p); - l = min(l, dist); - - if (l == dist) { - pp.xy = (uv - p.xy); - pp.z = dist; - } + vec3 pp = vec3(0.); + float l = 9.; + for (float i = 0.; i < 15.; i++) { + if (i >= u_foldCount) break; + vec2 rand = randomGB(vec2(i, i * u_seed)); + float an = rand.x * TWO_PI; + vec2 p = vec2(cos(an), sin(an)) * rand.y; + float dist = distance(uv, p); + l = min(l, dist); + + if (l == dist) { + pp.xy = (uv - p.xy); + pp.z = dist; } - return mix(pp.xy, vec2(0.), pow(pp.z, .25)); + } + return mix(pp.xy, vec2(0.), pow(pp.z, .25)); } float drops(vec2 uv) { @@ -180,10 +217,11 @@ float lst(float edge0, float edge1, float x) { return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } -vec3 blendMultiply(vec3 base, vec3 blend) { +vec3 blendMultiply(vec3 base, vec3 blend) { return base*blend; } -vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { + +vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { return (blendMultiply(base, blend) * opacity + base * (1.0 - opacity)); } @@ -191,7 +229,7 @@ void main() { vec2 imageUV = v_imageUV; vec2 patternUV = v_imageUV - .5; - patternUV = 5. * (patternUV * vec2(u_imageAspectRatio, 1.)); + patternUV *= 5. * vec2(u_imageAspectRatio, 1.); vec2 roughnessUv = mix(330., 130., u_roughnessSize) * patternUV; float roughness = roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.)); From f135ceec65f04137f1c804caaca01a9fa1ee2e82 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 17:07:08 +0100 Subject: [PATCH 009/186] tweaking blending --- packages/shaders/src/shaders/paper-texture.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index f34097869..3a10b92df 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -301,10 +301,9 @@ void main() { color -= .007 * drops; - float blending = 1. - u_blending; float blendOpacity = (.5 + .5 * res); - vec3 pic = blendMultiply(color, image.rgb + .3 * blending * length(normalImage), .5 * res + .5 * u_blending); - pic = mix(pic, image.rgb, blending); + vec3 pic = blendMultiply(color, image.rgb + .2 * 1. - u_blending * length(normalImage), .5 * res + .2 * u_blending); + pic = mix(image.rgb, pic, .5 * u_blending); color = mix(color, pic, frame); fragColor = vec4(color, opacity); From 43cc2cdeb67bc0c8981cec224a806e131a1c719f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 17:18:38 +0100 Subject: [PATCH 010/186] weights readjustment --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- packages/shaders/src/shaders/paper-texture.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 0d246d7f6..69e2a5db8 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -71,7 +71,7 @@ const PaperTextureWithControls = () => { colorFront: { value: toHsla(defaults.colorFront), order: 101 }, contrast: { value: defaults.contrast, min: 0, max: 1, order: 197 }, blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, - distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, + distortion: { value: defaults.distortion, min: 0, max: 1, order: 199 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 3a10b92df..0c6ff4dca 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -232,7 +232,7 @@ void main() { patternUV *= 5. * vec2(u_imageAspectRatio, 1.); vec2 roughnessUv = mix(330., 130., u_roughnessSize) * patternUV; - float roughness = roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.)); + float roughness = u_roughness * (roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.))); vec2 crumplesUV = fract(mix(.45, .02, pow(u_crumpleSize, .3)) * patternUV - u_seed) * 32.; float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.05, 0.)) - crumplesShape(crumplesUV)); @@ -262,23 +262,23 @@ void main() { fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); - normal.xy += u_folds * min(5. * u_contrast, 1.) * 4. * max(vec2(0.), w + w2); - normalImage.xy += u_folds * 2. * w; + normal += u_folds * min(5. * u_contrast, 1.) * 4. * max(vec2(0.), w + w2); + normalImage += u_folds * w; - normal.xy += crumples; - normalImage.xy += 1.5 * crumples; + normal += crumples; + normalImage += .4 * crumples; - normal.xy += 3. * drops; - normalImage.xy += .2 * drops; + normal += 2. * drops; + normalImage += .2 * drops; - normal.xy += u_roughness * 1.5 * roughness; - normal.xy += fiber; - - normalImage += u_roughness * .3 * roughness; + normal += 1.5 * roughness; + normalImage += .1 * roughness; + + normal += fiber; normalImage += .2 * fiber; vec3 lightPos = vec3(1., 2., 1.); - float res = dot(normalize(vec3(normal, 9.5 - 9. * pow(u_contrast, .1))), normalize(lightPos)); + float res = dot(normalize(vec3(normal, 7.5 - 7. * pow(u_contrast, .1))), normalize(lightPos)); vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; @@ -288,7 +288,7 @@ void main() { imageUV += .1 * u_distortion * normalImage; float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); - image.rgb += .6 * pow(u_contrast, .4) * (.3 - res); + image.rgb += .4 * pow(u_contrast, .4) * (.3 - res); frame *= image.a; From feae82ca92422c43d1cd8872bb34383fe7b2acc0 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 19 Nov 2025 17:34:55 +0100 Subject: [PATCH 011/186] fiber to distort the image less --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0c6ff4dca..e16158066 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -275,7 +275,7 @@ void main() { normalImage += .1 * roughness; normal += fiber; - normalImage += .2 * fiber; + normalImage += .1 * fiber; vec3 lightPos = vec3(1., 2., 1.); float res = dot(normalize(vec3(normal, 7.5 - 7. * pow(u_contrast, .1))), normalize(lightPos)); From ab9744553f4b8d8ab794d98badcef5257c749095 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Fri, 21 Nov 2025 16:05:48 +0100 Subject: [PATCH 012/186] saving progress on straight folds --- .../src/shaders/paper-texture.tsx | 2 +- packages/shaders/src/shaders/paper-texture.ts | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index a3ea86e00..44a7cd48a 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -29,7 +29,7 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - blending: 0, + blending: 0.75, distortion: 0.5, fit: 'contain', scale: 0.8, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index e16158066..5a042218c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -235,7 +235,7 @@ void main() { float roughness = u_roughness * (roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.))); vec2 crumplesUV = fract(mix(.45, .02, pow(u_crumpleSize, .3)) * patternUV - u_seed) * 32.; - float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.05, 0.)) - crumplesShape(crumplesUV)); + float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.02, 0.)) - crumplesShape(crumplesUV)); vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; float fiber = fiberNoise(fiberUV, vec2(0.)); @@ -280,6 +280,41 @@ void main() { vec3 lightPos = vec3(1., 2., 1.); float res = dot(normalize(vec3(normal, 7.5 - 7. * pow(u_contrast, .1))), normalize(lightPos)); + { + const vec3 LIGHT_DIR = normalize(vec3(-0.4, 0.7, 0.5)); + + vec2 gridUV = (patternUV * .12) + .5; + float foldX = .5; + + float dx = gridUV.x - foldX; + + float foldWidth = .3; + + float foldAmount = 1.0 - smoothstep(0.0, foldWidth, abs(dx)); + vec3 n = vec3(0.0, 0.0, 1.0); + float maxAngle = radians(65.); + float angle = sign(dx) * maxAngle * foldAmount; + + mat3 rotY = mat3( + cos(angle), 0.0, sin(angle), + 0.0, 1.0, 0.0, + -sin(angle), 0.0, cos(angle) + ); + n = rotY * n; + + float diffuse = max(dot(n, LIGHT_DIR), 0.0); + float lighting = .3 + 0.8 * diffuse; + + float crease = smoothstep(0., foldWidth * .5, abs(dx)); + float creaseDark = mix(.7, 1., crease); + + lighting *= creaseDark; + lighting = (1. - lighting); + + res += lighting; + } + + vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; From f1aab32774d75f047446ad9bec5970bbc65fc242 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 22 Nov 2025 00:55:43 +0100 Subject: [PATCH 013/186] grids! --- docs/src/app/(shaders)/paper-texture/page.tsx | 9 ++- .../src/shaders/paper-texture.tsx | 20 ++++- packages/shaders/src/shaders/paper-texture.ts | 78 ++++++++++--------- 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 69e2a5db8..31924d701 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -72,6 +72,7 @@ const PaperTextureWithControls = () => { contrast: { value: defaults.contrast, min: 0, max: 1, order: 197 }, blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 199 }, + seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, @@ -80,9 +81,11 @@ const PaperTextureWithControls = () => { crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 231 }, - drops: { value: defaults.drops, min: 0, max: 1, order: 240 }, - fade: { value: defaults.fade, min: 0, max: 1, order: 250 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 251 }, + grid: { value: defaults.grid, min: 0, max: 1, order: 240 }, + gridShape: { value: defaults.gridShape, min: 0, max: 1, order: 241 }, + gridCount: { value: defaults.gridCount, min: 1, max: 20, order: 242 }, + drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, + fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 44a7cd48a..87c52f770 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -29,7 +29,7 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - blending: 0.75, + blending: 0, distortion: 0.5, fit: 'contain', scale: 0.8, @@ -46,6 +46,9 @@ export const defaultPreset: PaperTexturePreset = { crumpleSize: 0.5, folds: 0.65, foldCount: 5, + grid: 0.5, + gridShape: 0.5, + gridCount: 1, fade: 0, drops: 0.2, seed: 5.8, @@ -73,6 +76,9 @@ export const abstractPreset: PaperTexturePreset = { crumpleSize: 0.3, folds: 1, foldCount: 3, + grid: 0.5, + gridShape: 0.5, + gridCount: 10, fade: 0, drops: 0.2, seed: 2.2, @@ -100,6 +106,9 @@ export const cardboardPreset: PaperTexturePreset = { crumpleSize: 0.1, folds: 0, foldCount: 1, + grid: 0.5, + gridShape: 0.5, + gridCount: 1, fade: 0, drops: 0.1, seed: 1.6, @@ -127,6 +136,9 @@ export const detailsPreset: PaperTexturePreset = { crumpleSize: 0.5, folds: 1, foldCount: 15, + grid: 0.5, + gridShape: 0.5, + gridCount: 1, fade: 0, drops: 0, seed: 6, @@ -153,6 +165,9 @@ export const PaperTexture: React.FC = memo(function PaperText fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, + grid = defaultPreset.params.grid, + gridShape = defaultPreset.params.gridShape, + gridCount = defaultPreset.params.gridCount, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, @@ -196,6 +211,9 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumpleSize: crumpleSize, u_foldCount: foldCount, u_folds: folds, + u_grid: grid, + u_gridShape: gridShape, + u_gridCount: gridCount, u_fade: fade, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5a042218c..d88c464a1 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -42,6 +42,9 @@ uniform float u_crumples; uniform float u_crumpleSize; uniform float u_folds; uniform float u_foldCount; +uniform float u_grid; +uniform float u_gridShape; +uniform float u_gridCount; uniform float u_drops; uniform float u_seed; uniform float u_fade; @@ -262,39 +265,24 @@ void main() { fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); - normal += u_folds * min(5. * u_contrast, 1.) * 4. * max(vec2(0.), w + w2); - normalImage += u_folds * w; - - normal += crumples; - normalImage += .4 * crumples; - - normal += 2. * drops; - normalImage += .2 * drops; - - normal += 1.5 * roughness; - normalImage += .1 * roughness; - - normal += fiber; - normalImage += .1 * fiber; - - vec3 lightPos = vec3(1., 2., 1.); - float res = dot(normalize(vec3(normal, 7.5 - 7. * pow(u_contrast, .1))), normalize(lightPos)); - + vec3 lightPos = vec3(.5, 1., .5); + float grid = 0.; { - const vec3 LIGHT_DIR = normalize(vec3(-0.4, 0.7, 0.5)); - - vec2 gridUV = (patternUV * .12) + .5; + vec2 gridUV = patternUV; + gridUV.x = fract(gridUV.x * .1 * u_gridCount + .5); float foldX = .5; float dx = gridUV.x - foldX; - float foldWidth = .3; + drops *= (.2 + .8 * step(0., dx)); - float foldAmount = 1.0 - smoothstep(0.0, foldWidth, abs(dx)); - vec3 n = vec3(0.0, 0.0, 1.0); - float maxAngle = radians(65.); - float angle = sign(dx) * maxAngle * foldAmount; + float foldWidth = u_gridShape; + + float foldAmount = 1. - smoothstep(0.0, foldWidth, abs(dx)); + float angle = sign(dx) * 1.1345 * foldAmount; + float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); + vec3 n = vec3(0.0, 0.0, 1.0); mat3 rotY = mat3( cos(angle), 0.0, sin(angle), 0.0, 1.0, 0.0, @@ -302,19 +290,31 @@ void main() { ); n = rotY * n; - float diffuse = max(dot(n, LIGHT_DIR), 0.0); - float lighting = .3 + 0.8 * diffuse; - - float crease = smoothstep(0., foldWidth * .5, abs(dx)); - float creaseDark = mix(.7, 1., crease); - - lighting *= creaseDark; - lighting = (1. - lighting); - - res += lighting; + grid = max(dot(n, lightPos), 0.); + grid *= creaseDark; + grid = .5 * u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); } + normal += 4. * u_folds * min(5. * u_contrast, 1.) * max(vec2(0.), w + w2); + normalImage += u_folds * w; + + normal += crumples; + normalImage += .4 * crumples; + + normal += 2. * drops; + normalImage += .2 * drops; + + normal += 1.5 * roughness; + normalImage += .1 * roughness; + + normal += fiber; + normalImage += .1 * fiber; + + normal += grid; + normalImage += .2 * grid; + float res = dot(normalize(vec3(normal, 7.5 - 7. * pow(u_contrast, .1))), lightPos); + vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; @@ -358,6 +358,9 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_crumples: number; u_foldCount: number; u_folds: number; + u_grid: number; + u_gridShape: number; + u_gridCount: number; u_fade: number; u_crumpleSize: number; u_drops: number; @@ -378,6 +381,9 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara crumples?: number; foldCount?: number; folds?: number; + grid?: number; + gridShape?: number; + gridCount?: number; fade?: number; crumpleSize?: number; drops?: number; From e6b428845efaed245f5325a1c6c1f7aeb3caddb4 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 22 Nov 2025 17:06:25 +0100 Subject: [PATCH 014/186] both shader and presets adjustments --- .../src/shaders/paper-texture.tsx | 128 +++++++++--------- packages/shaders/src/shaders/paper-texture.ts | 10 +- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 87c52f770..a34f7858c 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -40,48 +40,48 @@ export const defaultPreset: PaperTexturePreset = { contrast: 0.3, roughness: 0.4, roughnessSize: 0.5, - fiber: 0.3, + fiber: 0.2, fiberSize: 0.5, crumples: 0.3, crumpleSize: 0.5, folds: 0.65, - foldCount: 5, - grid: 0.5, - gridShape: 0.5, - gridCount: 1, + foldCount: 7, + grid: 0, + gridShape: 0.35, + gridCount: 3, fade: 0, drops: 0.2, seed: 5.8, }, }; -export const abstractPreset: PaperTexturePreset = { - name: 'Abstract', +export const fiberPreset: PaperTexturePreset = { + name: 'Fiber', params: { ...defaultObjectSizing, - blending: 0, - distortion: 0.5, - fit: 'contain', - speed: 0, - frame: 0, - scale: 0.8, - colorFront: '#00eeff', - colorBack: '#ff0a81', - contrast: 0.85, - roughness: 0, - roughnessSize: 0.5, - fiber: 0.1, - fiberSize: 0.2, - crumples: 0, - crumpleSize: 0.3, - folds: 1, + colorBack: "#b6ff94", + colorFront: "#5f4563", + contrast: 0.05, + blending: 0.19, + distortion: 1.00, + seed: 365, + roughness: 0.63, + roughnessSize: 0.50, + fiber: 0.76, + fiberSize: 0.61, + crumples: 0.00, + crumpleSize: 0.30, + folds: 0.00, foldCount: 3, - grid: 0.5, - gridShape: 0.5, + grid: 0.00, + gridShape: 0.50, gridCount: 10, - fade: 0, - drops: 0.2, - seed: 2.2, + drops: 0.20, + fade: 1, + scale: 0.80, + fit: "contain", + speed: 0, + frame: 0 }, }; @@ -89,67 +89,67 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - blending: 0, + blending: 0.5, distortion: 0.5, fit: 'contain', speed: 0, frame: 0, scale: 0.8, - colorFront: '#c7b89e', - colorBack: '#999180', - contrast: 0.4, - roughness: 0, - roughnessSize: 0.5, + colorBack: "#dcc593", + colorFront: "#685327", + contrast: 0.27, + roughness: 0.1, + roughnessSize: 0.50, fiber: 0.35, fiberSize: 0.14, - crumples: 0.7, + crumples: 0., crumpleSize: 0.1, folds: 0, foldCount: 1, - grid: 0.5, - gridShape: 0.5, - gridCount: 1, + grid: 0.50, + gridShape: 0.28, + gridCount: 20, fade: 0, drops: 0.1, seed: 1.6, }, }; -export const detailsPreset: PaperTexturePreset = { - name: 'Details', +export const flatPreset: PaperTexturePreset = { + name: 'Flat', params: { ...defaultObjectSizing, - blending: 0, - distortion: 0.5, - speed: 0, - frame: 0, + colorBack: '#fff3e0', + colorFront: '#9c9c9c', + contrast: 0.79, + blending: 0.19, + distortion: 0.33, + seed: 415, + roughness: 0.31, + roughnessSize: 1.0, + fiber: 0, + fiberSize: 0.61, + crumples: 0.7, + crumpleSize: 0.2, + folds: 0, + foldCount: 6, + grid: 0.0, + gridShape: 0.30, + gridCount: 20.0, + drops: 0.20, + fade: 0.75, + scale: 0.8, fit: 'contain', - scale: 3, - colorFront: '#00000000', - colorBack: '#00000000', - contrast: 0, - roughness: 1, - roughnessSize: 0.5, - fiber: 0.27, - fiberSize: 0.22, - crumples: 1, - crumpleSize: 0.5, - folds: 1, - foldCount: 15, - grid: 0.5, - gridShape: 0.5, - gridCount: 1, - fade: 0, - drops: 0, - seed: 6, + speed: 0, + frame: 0 }, }; export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, + flatPreset, cardboardPreset, - abstractPreset, - detailsPreset, + fiberPreset, ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index d88c464a1..c8bd8df3e 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -196,7 +196,7 @@ vec2 folds(vec2 uv) { pp.z = dist; } } - return mix(pp.xy, vec2(0.), pow(pp.z, .25)); + return mix(pp.xy, vec2(0.), pow(pp.z, .15)); } float drops(vec2 uv) { @@ -247,10 +247,10 @@ void main() { vec2 normal = vec2(0.); vec2 normalImage = vec2(0.); - vec2 foldsUV = patternUV * .12; + vec2 foldsUV = patternUV * .18; foldsUV = rotate(foldsUV, 4. * u_seed); vec2 w = folds(foldsUV); - foldsUV = rotate(foldsUV + .007 * cos(u_seed), .01 * sin(u_seed)); + foldsUV = rotate(foldsUV + .005 * cos(u_seed), .01 * sin(u_seed)); vec2 w2 = folds(foldsUV); float drops = u_drops * drops(patternUV * 2.); @@ -295,7 +295,7 @@ void main() { grid = .5 * u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); } - normal += 4. * u_folds * min(5. * u_contrast, 1.) * max(vec2(0.), w + w2); + normal -= 3. * u_folds * (w + w2); normalImage += u_folds * w; normal += crumples; @@ -313,7 +313,7 @@ void main() { normal += grid; normalImage += .2 * grid; - float res = dot(normalize(vec3(normal, 7.5 - 7. * pow(u_contrast, .1))), lightPos); + float res = dot(normalize(vec3(normal, mix(7.5, .5, pow(u_contrast, .1)))), lightPos); vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; From 09646b3244b9b867d94755639c17dfd992b787ee Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 23 Nov 2025 20:11:53 +0100 Subject: [PATCH 015/186] various adjustments --- .../src/shaders/paper-texture.tsx | 48 +++++++++++++++---- packages/shaders/src/shaders/paper-texture.ts | 4 +- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index a34f7858c..4c648d698 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -59,21 +59,21 @@ export const fiberPreset: PaperTexturePreset = { name: 'Fiber', params: { ...defaultObjectSizing, - colorBack: "#b6ff94", - colorFront: "#5f4563", + colorBack: "#eaffe0", + colorFront: "#c19ac6", contrast: 0.05, - blending: 0.19, + blending: 0.8, distortion: 1.00, seed: 365, roughness: 0.63, roughnessSize: 0.50, fiber: 0.76, fiberSize: 0.61, - crumples: 0.00, + crumples: 0, crumpleSize: 0.30, - folds: 0.00, + folds: 0, foldCount: 3, - grid: 0.00, + grid: 0, gridShape: 0.50, gridCount: 10, drops: 0.20, @@ -106,7 +106,7 @@ export const cardboardPreset: PaperTexturePreset = { crumpleSize: 0.1, folds: 0, foldCount: 1, - grid: 0.50, + grid: 0.25, gridShape: 0.28, gridCount: 20, fade: 0, @@ -115,6 +115,37 @@ export const cardboardPreset: PaperTexturePreset = { }, }; +export const spreadPreset: PaperTexturePreset = { + name: 'Spread', + params: { + ...defaultObjectSizing, + colorBack: "#ffffff", + colorFront: "#c0c4c0", + contrast: 0.47, + blending: 0.36, + distortion: 0.50, + seed: 280, + roughness: 0.4, + roughnessSize: 0.4, + fiber: 0.24, + fiberSize: 0.5, + crumples: 0.2, + crumpleSize: 0.42, + folds: 0.3, + foldCount: 4, + grid: 0.7, + gridShape: 0.48, + gridCount: 1, + drops: 0, + fade: 0.68, + scale: 0.8, + fit: "contain", + speed: 0, + frame: 0 + }, +}; + + export const flatPreset: PaperTexturePreset = { name: 'Flat', params: { @@ -147,9 +178,10 @@ export const flatPreset: PaperTexturePreset = { export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, - flatPreset, + spreadPreset, cardboardPreset, fiberPreset, + flatPreset ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index c8bd8df3e..02d5b50bd 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -292,13 +292,13 @@ void main() { grid = max(dot(n, lightPos), 0.); grid *= creaseDark; - grid = .5 * u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); + grid = u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); } normal -= 3. * u_folds * (w + w2); normalImage += u_folds * w; - normal += crumples; + normal += 1.5 * crumples; normalImage += .4 * crumples; normal += 2. * drops; From 78277dfb4f9bb87d3cbcba043e98d860658700be Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 23 Nov 2025 20:43:22 +0100 Subject: [PATCH 016/186] various adjustments --- packages/shaders-react/src/shaders/paper-texture.tsx | 8 ++++---- packages/shaders/src/shaders/paper-texture.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 4c648d698..65d5ee911 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -62,9 +62,9 @@ export const fiberPreset: PaperTexturePreset = { colorBack: "#eaffe0", colorFront: "#c19ac6", contrast: 0.05, - blending: 0.8, - distortion: 1.00, - seed: 365, + blending: 0.4, + distortion: 0.4, + seed: 180, roughness: 0.63, roughnessSize: 0.50, fiber: 0.76, @@ -76,7 +76,7 @@ export const fiberPreset: PaperTexturePreset = { grid: 0, gridShape: 0.50, gridCount: 10, - drops: 0.20, + drops: 1, fade: 1, scale: 0.80, fit: "contain", diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 02d5b50bd..e869a1219 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -301,7 +301,7 @@ void main() { normal += 1.5 * crumples; normalImage += .4 * crumples; - normal += 2. * drops; + normal += 2.5 * drops; normalImage += .2 * drops; normal += 1.5 * roughness; From 2fa0f07f3d3ab11eb28cdf9447187a427d4bf929 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 23 Nov 2025 23:22:13 +0100 Subject: [PATCH 017/186] various adjustments --- packages/shaders-react/src/shaders/paper-texture.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 65d5ee911..10a49f4aa 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -45,13 +45,13 @@ export const defaultPreset: PaperTexturePreset = { crumples: 0.3, crumpleSize: 0.5, folds: 0.65, - foldCount: 7, + foldCount: 12, grid: 0, gridShape: 0.35, gridCount: 3, fade: 0, drops: 0.2, - seed: 5.8, + seed: 43, }, }; From 83c4b8350307d501bc3a0499f50f133cd77bd745 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 23 Nov 2025 23:38:57 +0100 Subject: [PATCH 018/186] various adjustments --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 10a49f4aa..e280bc8b5 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -35,7 +35,7 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#9fadbc', + colorFront: '#bbbfc4', colorBack: '#ffffff', contrast: 0.3, roughness: 0.4, From 3ba5c58f5cbbab298384a1b47ebd2a8f3f4855cb Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 9 Dec 2025 17:59:42 +0400 Subject: [PATCH 019/186] JSDcs update --- packages/shaders/src/shaders/paper-texture.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index e6427ad29..1f088ecc3 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { rotation2, declarePI, textureRandomizerR, textureRandomizerGB } from '../shader-utils.js'; +import { rotation2, declarePI, textureRandomizerR } from '../shader-utils.js'; /** * A static texture built from multiple noise layers, usable for realistic paper and cardboard surfaces. @@ -14,16 +14,22 @@ import { rotation2, declarePI, textureRandomizerR, textureRandomizerGB } from '. * - u_colorFront (vec4): Foreground color in RGBA * - u_colorBack (vec4): Background color in RGBA * - u_contrast (float): Blending behavior, sharper vs smoother color transitions (0 to 1) - * - u_roughness (float): Pixel noise, related to canvas and not scalable (0 to 1) + * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) + * - u_roughnessSize (float): Scale of the roughness pattern (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) * - u_fiberSize (float): Curly-shaped noise scale (0 to 1) * - u_crumples (float): Cell-based crumple pattern intensity (0 to 1) * - u_crumpleSize (float): Cell-based crumple pattern scale (0 to 1) * - u_folds (float): Depth of the folds (0 to 1) * - u_foldCount (float): Number of folds (1 to 15) - * - u_fade (float): Big-scale noise mask applied to the pattern (0 to 1) - * - u_drops (float): Visibility of speckle pattern (0 to 1) + * - u_grid (float): Intensity of the grid / crease pattern (0 to 1) + * - u_gridShape (float): Shape/width of the grid creases (0 to 1) + * - u_gridCount (float): Number/density of grid creases (0 to N) + * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds, crumples and dots (0 to 1000) + * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) + * - u_blending (float): Amount of image-to-paper blending, 0 for original color, 1 for mix with colorBack (0 to 1) + * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): From 935d26063a7de88036d5b18555095eb1fcbcd35f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 9 Dec 2025 18:01:27 +0400 Subject: [PATCH 020/186] removing defs (to be taken from JSDocs once the list is approved) --- docs/src/shader-defs/paper-texture-def.ts | 89 ----------------------- 1 file changed, 89 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 63369b230..36dfec274 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -28,95 +28,6 @@ export const paperTextureDef: ShaderDef = { isColor: true, description: 'Foreground color', }, - { - name: 'contrast', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.contrast, - description: 'Blending behavior (sharper vs. smoother color transitions)', - }, - { - name: 'roughness', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.roughness, - description: 'Pixel noise, related to canvas (not scalable)', - }, - { - name: 'fiber', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.fiber, - description: 'Curly-shaped noise', - }, - { - name: 'fiberSize', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.fiberSize, - description: 'Curly-shaped noise scale', - }, - { - name: 'crumples', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.crumples, - description: 'Cell-based crumple pattern', - }, - { - name: 'crumpleSize', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.crumpleSize, - description: 'Cell-based crumple pattern scale', - }, - { - name: 'folds', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.folds, - description: 'Depth of the folds', - }, - { - name: 'foldCount', - type: 'number', - min: 1, - max: 15, - step: 1, - defaultValue: defaultParams.foldCount, - description: 'Number of folds (15 max)', - }, - { - name: 'fade', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.fade, - description: 'Big-scale noise mask applied to the pattern', - }, - { - name: 'drops', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.drops, - description: 'The visibility of speckle pattern', - }, - { - name: 'seed', - type: 'number', - min: 0, - max: 1000, - defaultValue: defaultParams.seed, - description: 'Seed applied to folds, crumples and dots', - }, ...staticImageCommonParams, ], }; From ede418a86ab210bb611895ae111e00312a4f9c17 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 9 Dec 2025 18:07:14 +0400 Subject: [PATCH 021/186] post-merge fix --- packages/shaders/src/shaders/paper-texture.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 1f088ecc3..46899e838 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -383,15 +383,21 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_colorBack: [number, number, number, number]; u_contrast: number; u_roughness: number; + u_roughnessSize: number; u_fiber: number; u_fiberSize: number; u_crumples: number; u_foldCount: number; u_folds: number; + u_grid: number; + u_gridShape: number; + u_gridCount: number; u_fade: number; u_crumpleSize: number; u_drops: number; u_seed: number; + u_blending: number; + u_distortion: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -400,13 +406,19 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara colorBack?: string; contrast?: number; roughness?: number; + roughnessSize?: number; fiber?: number; fiberSize?: number; crumples?: number; foldCount?: number; folds?: number; + grid?: number; + gridShape?: number; + gridCount?: number; fade?: number; crumpleSize?: number; drops?: number; seed?: number; + blending?: number; + distortion?: number; } From aa83ad4dbdfd3499b054fd4183d9694b7e0b9de1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 17 Dec 2025 17:44:11 +0400 Subject: [PATCH 022/186] post-merging tweaks --- packages/shaders/src/shaders/paper-texture.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index fa4cf0b9e..7949a97aa 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -40,8 +40,6 @@ import { rotation2, declarePI, textureRandomizerR } from '../shader-utils.js'; * - u_pixelRatio (float): Device pixel ratio * - u_originX (float): Reference point for positioning world width in the canvas (0 to 1) * - u_originY (float): Reference point for positioning world height in the canvas (0 to 1) - * - u_worldWidth (float): Virtual width of the graphic before it's scaled to fit the canvas - * - u_worldHeight (float): Virtual height of the graphic before it's scaled to fit the canvas * - u_fit (float): How to fit the rendered shader into the canvas dimensions (0 = none, 1 = contain, 2 = cover) * - u_scale (float): Overall zoom level of the graphics (0.01 to 4) * - u_rotation (float): Overall rotation angle of the graphics in degrees (0 to 360) @@ -85,7 +83,6 @@ uniform float u_distortion; uniform sampler2D u_noiseTexture; in vec2 v_imageUV; - out vec4 fragColor; float getUvFrame(vec2 uv) { From 4193916bef55e851272bac9baa2c8ea2760de945 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 19:40:25 +0400 Subject: [PATCH 023/186] profiler added --- .../src/shaders/paper-texture.tsx | 10 +- packages/shaders/src/shader-mount.ts | 207 ++++++++++++++++++ 2 files changed, 212 insertions(+), 5 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e280bc8b5..dde2068d4 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -33,7 +33,7 @@ export const defaultPreset: PaperTexturePreset = { distortion: 0.5, fit: 'contain', scale: 0.8, - speed: 0, + speed: 1, frame: 0, colorFront: '#bbbfc4', colorBack: '#ffffff', @@ -80,7 +80,7 @@ export const fiberPreset: PaperTexturePreset = { fade: 1, scale: 0.80, fit: "contain", - speed: 0, + speed: 1, frame: 0 }, }; @@ -92,7 +92,7 @@ export const cardboardPreset: PaperTexturePreset = { blending: 0.5, distortion: 0.5, fit: 'contain', - speed: 0, + speed: 1, frame: 0, scale: 0.8, colorBack: "#dcc593", @@ -140,7 +140,7 @@ export const spreadPreset: PaperTexturePreset = { fade: 0.68, scale: 0.8, fit: "contain", - speed: 0, + speed: 1, frame: 0 }, }; @@ -171,7 +171,7 @@ export const flatPreset: PaperTexturePreset = { fade: 0.75, scale: 0.8, fit: 'contain', - speed: 0, + speed: 1, frame: 0 }, }; diff --git a/packages/shaders/src/shader-mount.ts b/packages/shaders/src/shader-mount.ts index b327d682b..2fbf1bd69 100644 --- a/packages/shaders/src/shader-mount.ts +++ b/packages/shaders/src/shader-mount.ts @@ -2,6 +2,23 @@ import { vertexShaderSource } from './vertex-shader.js'; const DEFAULT_MAX_PIXEL_COUNT: number = 1920 * 1080 * 4; +// Type definition for the WebGL timer query extension +interface EXT_disjoint_timer_query_webgl2 { + readonly TIME_ELAPSED_EXT: 0x88bf; + readonly GPU_DISJOINT_EXT: 0x8fbb; +} + +// Interface for performance statistics +interface PerformanceStats { + avgFrameTime: number; + avgGPUTime: number; + fps: number; + resolution: { width: number; height: number }; + renderScale: number; + pixelCount: number; + supportsTimerQueries: boolean; +} + export class ShaderMount { public parentElement: PaperShaderElement; public canvasElement: HTMLCanvasElement; @@ -36,6 +53,14 @@ export class ShaderMount { private uniformCache: Record = {}; private textureUnitMap: Map = new Map(); + // Performance measurement properties + private timerExt: EXT_disjoint_timer_query_webgl2 | null = null; + private performanceQueries: WebGLQuery[] = []; + private performanceResults: number[] = []; + private frameTimeHistory: number[] = []; + private lastPerformanceLog = 0; + private performanceLoggingEnabled = true; + constructor( /** The div you'd like to mount the shader to. The shader will match its size. */ parentElement: HTMLElement, @@ -93,6 +118,9 @@ export class ShaderMount { } this.gl = gl; + // Initialize performance measurement + this.initPerformanceMeasurement(); + this.initProgram(); this.setupPositionAttribute(); // Grab the locations of the uniforms in the fragment shader @@ -117,6 +145,163 @@ export class ShaderMount { document.addEventListener('visibilitychange', this.handleDocumentVisibilityChange); } + private initPerformanceMeasurement = (): void => { + // Try to get timer query extension + this.timerExt = this.gl.getExtension('EXT_disjoint_timer_query_webgl2'); + + if (!this.timerExt) { + console.warn('Timer queries not supported - using frame timing fallback'); + } else { + console.log('✓ GPU timer queries supported'); + } + }; + + // Enable/disable performance logging + public enablePerformanceLogging = (enabled: boolean = true): void => { + this.performanceLoggingEnabled = enabled; + if (enabled) { + console.log('🎮 Performance logging enabled'); + } + }; + + private measureGPUTime = (): WebGLQuery | null => { + if (!this.timerExt) { + return null; + } + + const query = this.gl.createQuery(); + if (!query) return null; + + this.gl.beginQuery(this.timerExt.TIME_ELAPSED_EXT, query); + return query; + }; + + private endGPUMeasurement = (query: WebGLQuery | null) => { + if (!query || !this.timerExt) return; + + this.gl.endQuery(this.timerExt.TIME_ELAPSED_EXT); + this.performanceQueries.push(query); + }; + + private checkPerformanceResults = () => { + if (!this.timerExt || this.performanceQueries.length === 0) return; + + // Check completed queries + const completedQueries: WebGLQuery[] = []; + + this.performanceQueries.forEach((query) => { + const available = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE); + const disjoint = this.gl.getParameter(this.timerExt!.GPU_DISJOINT_EXT); + + if (available && !disjoint) { + const timeElapsed = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT); + const timeInMs = timeElapsed / 1000000; // Convert to milliseconds + + this.performanceResults.push(timeInMs); + completedQueries.push(query); + + // Keep only last 100 results + if (this.performanceResults.length > 100) { + this.performanceResults.shift(); + } + } + }); + + // Remove completed queries + completedQueries.forEach((completedQuery) => { + const index = this.performanceQueries.indexOf(completedQuery); + if (index > -1) { + this.performanceQueries.splice(index, 1); + } + this.gl.deleteQuery(completedQuery); + }); + }; + + private logPerformanceStats = (frameTime: number) => { + if (!this.performanceLoggingEnabled) return; + + const now = performance.now(); + if (now - this.lastPerformanceLog < 2000) return; // Log every 2 seconds + + this.lastPerformanceLog = now; + + // Frame time stats + const avgFrameTime = this.frameTimeHistory.reduce((a, b) => a + b, 0) / this.frameTimeHistory.length; + const maxFrameTime = Math.max(...this.frameTimeHistory); + const minFrameTime = Math.min(...this.frameTimeHistory); + + console.group('🎮 Shader Performance Stats'); + console.log(`📊 Frame Time: ${frameTime.toFixed(2)}ms (avg: ${avgFrameTime.toFixed(2)}ms)`); + console.log( + `⚡ FPS: ${(1000 / avgFrameTime).toFixed(1)} (range: ${(1000 / maxFrameTime).toFixed(1)}-${(1000 / minFrameTime).toFixed(1)})` + ); + console.log( + `🖥️ Resolution: ${this.gl.canvas.width}x${this.gl.canvas.height} (${((this.gl.canvas.width * this.gl.canvas.height) / 1000000).toFixed(1)}MP)` + ); + console.log(`📏 Render Scale: ${this.renderScale.toFixed(2)}x`); + + // GPU timing stats (if available) + if (this.performanceResults.length > 0) { + const avgGPUTime = this.performanceResults.reduce((a, b) => a + b, 0) / this.performanceResults.length; + const maxGPUTime = Math.max(...this.performanceResults); + const minGPUTime = Math.min(...this.performanceResults); + + console.log( + `🎯 GPU Time: ${avgGPUTime.toFixed(2)}ms (range: ${minGPUTime.toFixed(2)}-${maxGPUTime.toFixed(2)}ms)` + ); + console.log(`⚙️ GPU Usage: ${((avgGPUTime / avgFrameTime) * 100).toFixed(1)}% of frame time`); + } else { + console.log('⚠️ GPU timing not available (timer queries not supported)'); + } + console.groupEnd(); + }; + + // Public method to get current performance stats + public getPerformanceStats = (): PerformanceStats => { + const avgFrameTime = + this.frameTimeHistory.length > 0 + ? this.frameTimeHistory.reduce((a, b) => a + b, 0) / this.frameTimeHistory.length + : 0; + + const avgGPUTime = + this.performanceResults.length > 0 + ? this.performanceResults.reduce((a, b) => a + b, 0) / this.performanceResults.length + : 0; + + return { + avgFrameTime, + avgGPUTime, + fps: avgFrameTime > 0 ? 1000 / avgFrameTime : 0, + resolution: { width: this.gl.canvas.width, height: this.gl.canvas.height }, + renderScale: this.renderScale, + pixelCount: this.gl.canvas.width * this.gl.canvas.height, + supportsTimerQueries: !!this.timerExt, + }; + }; + + // Method to stress test with multiple passes + public stressTest = (passes: number = 10): void => { + if (!this.program) return; + + console.log(`🔥 Starting stress test with ${passes} passes...`); + + const gpuQuery = this.measureGPUTime(); + const startTime = performance.now(); + + for (let i = 0; i < passes; i++) { + this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); + } + + this.endGPUMeasurement(gpuQuery); + + const endTime = performance.now(); + const totalTime = endTime - startTime; + + console.log( + `⏱️ Stress test completed: ${totalTime.toFixed(2)}ms total, ${(totalTime / passes).toFixed(2)}ms per pass` + ); + }; + private initProgram = () => { const program = createProgram(this.gl, vertexShaderSource, this.fragmentShader); if (!program) return; @@ -276,6 +461,13 @@ export class ShaderMount { return; } + // Calculate frame timing + const frameTime = currentTime - this.lastRenderTime; + this.frameTimeHistory.push(frameTime); + if (this.frameTimeHistory.length > 60) { + this.frameTimeHistory.shift(); // Keep last 60 frames + } + // Calculate the delta time const dt = currentTime - this.lastRenderTime; this.lastRenderTime = currentTime; @@ -300,8 +492,17 @@ export class ShaderMount { this.resolutionChanged = false; } + // === PERFORMANCE MEASUREMENT START === + const gpuQuery = this.measureGPUTime(); + + // Your main draw call - this is what we're measuring this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); + this.endGPUMeasurement(gpuQuery); + this.checkPerformanceResults(); + this.logPerformanceStats(frameTime); + // === PERFORMANCE MEASUREMENT END === + // Loop if we're animating if (this.currentSpeed !== 0) { this.requestRender(); @@ -542,6 +743,12 @@ export class ShaderMount { this.rafId = null; } + // Clean up performance queries + this.performanceQueries.forEach((query) => { + this.gl.deleteQuery(query); + }); + this.performanceQueries = []; + if (this.gl && this.program) { // Clean up all textures this.textures.forEach((texture) => { From 164e5bfbe29d49d953c07f8584e0dc9ff017b4e5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 19:54:20 +0400 Subject: [PATCH 024/186] fiber noise opt --- packages/shaders/src/shaders/paper-texture.ts | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 7949a97aa..b69af44c0 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -147,7 +147,7 @@ float fiberRandom(vec2 p) { return texture(u_noiseTexture, fract(uv)).b; } -float fiberValueNoise(vec2 st) { +vec3 fiberValueNoiseD(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); float a = fiberRandom(i); @@ -155,29 +155,39 @@ float fiberValueNoise(vec2 st) { float c = fiberRandom(i + vec2(0.0, 1.0)); float d = fiberRandom(i + vec2(1.0, 1.0)); vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); + vec2 du = 6.0 * f * (1.0 - f); + float value = mix(mix(a, b, u.x), mix(c, d, u.x), u.y); + float dx = du.x * mix(b - a, d - c, u.y); + float dy = du.y * mix(c - a, d - b, u.x); + return vec3(value, dx, dy); } -float fiberNoiseFbm(vec2 n, vec2 seedOffset) { - float total = 0.0, amplitude = 1.; +// Returns vec3(value, dValue/dx, dValue/dy) with gradients in original coordinate space +vec3 fiberNoiseFbmD(vec2 n, vec2 seedOffset) { + float total = 0.0; + vec2 totalGrad = vec2(0.0); + float amplitude = 1.0; + float angle = 0.7; + float scale = 1.0; + float rotAngle = 0.0; for (int i = 0; i < 4; i++) { - n = rotate(n, .7); - total += fiberValueNoise(n + seedOffset) * amplitude; - n *= 2.; + n = rotate(n, angle); + rotAngle += angle; + vec3 nd = fiberValueNoiseD(n + seedOffset); + total += nd.x * amplitude; + float rc = cos(rotAngle); + float rs = sin(rotAngle); + vec2 g = nd.yz; + totalGrad += amplitude * scale * vec2(rc * g.x + rs * g.y, -rs * g.x + rc * g.y); + n *= 2.0; + scale *= 2.0; amplitude *= 0.6; } - return total; + return vec3(total, totalGrad); } float fiberNoise(vec2 uv, vec2 seedOffset) { - float epsilon = 0.001; - float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset); - float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset); - float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset); - float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset); - return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon); + return length(fiberNoiseFbmD(uv, seedOffset).yz); } vec2 randomGB(vec2 p) { From 5b079b2379f39bd5dc8d542a96c7412cef6c7ca9 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 19:59:07 +0400 Subject: [PATCH 025/186] fiber seed remove --- packages/shaders/src/shaders/paper-texture.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b69af44c0..322a9d62e 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { rotation2, declarePI, textureRandomizerR } from '../shader-utils.js'; +import { rotation2, declarePI } from '../shader-utils.js'; /** * A static texture built from multiple noise layers, usable for realistic paper and cardboard surfaces. @@ -99,7 +99,10 @@ float getUvFrame(vec2 uv) { ${ declarePI } ${ rotation2 } -${ textureRandomizerR } +float randomR(vec2 p) { + vec2 uv = floor(p) / 100. + .5; + return texture(u_noiseTexture, fract(uv)).r; +} float valueNoise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); @@ -162,8 +165,7 @@ vec3 fiberValueNoiseD(vec2 st) { return vec3(value, dx, dy); } -// Returns vec3(value, dValue/dx, dValue/dy) with gradients in original coordinate space -vec3 fiberNoiseFbmD(vec2 n, vec2 seedOffset) { +vec3 fiberNoiseFbmD(vec2 n) { float total = 0.0; vec2 totalGrad = vec2(0.0); float amplitude = 1.0; @@ -173,7 +175,7 @@ vec3 fiberNoiseFbmD(vec2 n, vec2 seedOffset) { for (int i = 0; i < 4; i++) { n = rotate(n, angle); rotAngle += angle; - vec3 nd = fiberValueNoiseD(n + seedOffset); + vec3 nd = fiberValueNoiseD(n); total += nd.x * amplitude; float rc = cos(rotAngle); float rs = sin(rotAngle); @@ -186,8 +188,8 @@ vec3 fiberNoiseFbmD(vec2 n, vec2 seedOffset) { return vec3(total, totalGrad); } -float fiberNoise(vec2 uv, vec2 seedOffset) { - return length(fiberNoiseFbmD(uv, seedOffset).yz); +float fiberNoise(vec2 uv) { + return length(fiberNoiseFbmD(uv).yz); } vec2 randomGB(vec2 p) { @@ -279,7 +281,7 @@ void main() { float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.02, 0.)) - crumplesShape(crumplesUV)); vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; - float fiber = fiberNoise(fiberUV, vec2(0.)); + float fiber = fiberNoise(fiberUV); fiber = .5 * u_fiber * (fiber - 1.); vec2 normal = vec2(0.); From bfe17faa015d07d391ff04da2339e8b2880f8488 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 20:58:47 +0400 Subject: [PATCH 026/186] save res --- packages/shaders/src/shaders/paper-texture.ts | 142 ++++++++---------- 1 file changed, 63 insertions(+), 79 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 322a9d62e..e81a6f780 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -115,7 +115,7 @@ float valueNoise(vec2 st) { float x2 = mix(c, d, u.x); return mix(x1, x2, u.y); } -float fbm(vec2 n) { +float getFadeMask(vec2 n) { float total = 0.0, amplitude = .4; for (int i = 0; i < 3; i++) { total += valueNoise(n) * amplitude; @@ -125,71 +125,53 @@ float fbm(vec2 n) { return total; } - -float randomG(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).g; -} -float roughness(vec2 p) { - p *= .1; - float o = 0.; - for (float i = 0.; ++i < 4.; p *= 2.1) { - vec4 w = vec4(floor(p), ceil(p)); - vec2 f = fract(p); - o += mix( - mix(randomG(w.xy), randomG(w.xw), f.y), - mix(randomG(w.zy), randomG(w.zw), f.y), - f.x); - o += .2 / exp(2. * abs(sin(.2 * p.x + .5 * p.y))); - } - return o / 3.; -} - -float fiberRandom(vec2 p) { - vec2 uv = floor(p) / 100.; - return texture(u_noiseTexture, fract(uv)).b; -} - -vec3 fiberValueNoiseD(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = fiberRandom(i); - float b = fiberRandom(i + vec2(1.0, 0.0)); - float c = fiberRandom(i + vec2(0.0, 1.0)); - float d = fiberRandom(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - vec2 du = 6.0 * f * (1.0 - f); - float value = mix(mix(a, b, u.x), mix(c, d, u.x), u.y); - float dx = du.x * mix(b - a, d - c, u.y); - float dy = du.y * mix(c - a, d - b, u.x); - return vec3(value, dx, dy); -} - -vec3 fiberNoiseFbmD(vec2 n) { - float total = 0.0; - vec2 totalGrad = vec2(0.0); - float amplitude = 1.0; +vec2 getRoughnessFiber(vec2 pR, vec2 pF) { + float roughDx = 0.; + vec2 fiberGrad = vec2(0.); + pR *= .1; + float scaleR = .1; + float scaleF = 1.; + float amplitude = 1.; + float rotAngle = 0.; float angle = 0.7; - float scale = 1.0; - float rotAngle = 0.0; for (int i = 0; i < 4; i++) { - n = rotate(n, angle); + // Roughness: R channel at pR (only 3 iterations) + if (i < 3) { + vec2 ipR = floor(pR); + vec2 fpR = fract(pR); + vec4 uvR = fract(vec4(ipR, ipR + 1.) / 50. + .5); + float aR = texture(u_noiseTexture, uvR.xy).r; + float bR = texture(u_noiseTexture, uvR.xw).r; + float cR = texture(u_noiseTexture, uvR.zy).r; + float dR = texture(u_noiseTexture, uvR.zw).r; + roughDx += scaleR * mix(cR - aR, dR - bR, fpR.y); + float arg = .2 * pR.x + .5 * pR.y; + float s = sin(arg); + roughDx += scaleR * -.08 * exp(-2. * abs(s)) * sign(s) * cos(arg); + pR *= 2.1; + scaleR *= 2.1; + } + // Fiber: B channel at pF with rotation + pF = rotate(pF, angle); rotAngle += angle; - vec3 nd = fiberValueNoiseD(n); - total += nd.x * amplitude; - float rc = cos(rotAngle); - float rs = sin(rotAngle); - vec2 g = nd.yz; - totalGrad += amplitude * scale * vec2(rc * g.x + rs * g.y, -rs * g.x + rc * g.y); - n *= 2.0; - scale *= 2.0; + vec2 ipF = floor(pF); + vec2 fpF = fract(pF); + vec4 uvF = fract(vec4(ipF, ipF + 1.) / 50. + .5); + float aF = texture(u_noiseTexture, uvF.xy).b; + float bF = texture(u_noiseTexture, uvF.zy).b; + float cF = texture(u_noiseTexture, uvF.xw).b; + float dF = texture(u_noiseTexture, uvF.zw).b; + vec2 u = fpF * fpF * (3. - 2. * fpF); + vec2 du = 6. * fpF * (1. - fpF); + float dxF = du.x * mix(bF - aF, dF - cF, u.y); + float dyF = du.y * mix(cF - aF, dF - bF, u.x); + float rc = cos(rotAngle), rs = sin(rotAngle); + fiberGrad += amplitude * scaleF * vec2(rc * dxF + rs * dyF, -rs * dxF + rc * dyF); + pF *= 2.; + scaleF *= 2.; amplitude *= 0.6; } - return vec3(total, totalGrad); -} - -float fiberNoise(vec2 uv) { - return length(fiberNoiseFbmD(uv).yz); + return vec2(roughDx / 1.5, length(fiberGrad)); } vec2 randomGB(vec2 p) { @@ -216,7 +198,7 @@ float crumpledNoise(vec2 t, float pw) { return pow(wsum != 0.0 ? cl / wsum : 0.0, .5) * 2.; } -float crumplesShape(vec2 uv) { +float getCrumples(vec2 uv) { return crumpledNoise(uv * .25, 16.) * crumpledNoise(uv * .5, 2.); } @@ -239,7 +221,7 @@ vec2 folds(vec2 uv) { return mix(pp.xy, vec2(0.), pow(pp.z, .15)); } -float drops(vec2 uv) { +float getDrops(vec2 uv) { vec2 iDropsUV = floor(uv); vec2 fDropsUV = fract(uv); float dropsMinDist = 1.; @@ -274,32 +256,33 @@ void main() { vec2 patternUV = v_imageUV - .5; patternUV *= 5. * vec2(u_imageAspectRatio, 1.); - vec2 roughnessUv = mix(330., 130., u_roughnessSize) * patternUV; - float roughness = u_roughness * (roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.))); - + vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; + vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; + vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); + float roughness = u_roughness * (rf.x + .5); + float fiber = u_fiber * (rf.y - 1.); + vec2 crumplesUV = fract(mix(.45, .02, pow(u_crumpleSize, .3)) * patternUV - u_seed) * 32.; - float crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.02, 0.)) - crumplesShape(crumplesUV)); + float crumples = u_crumples * (getCrumples(crumplesUV + vec2(.02, 0.)) - getCrumples(crumplesUV)); + - vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; - float fiber = fiberNoise(fiberUV); - fiber = .5 * u_fiber * (fiber - 1.); vec2 normal = vec2(0.); vec2 normalImage = vec2(0.); vec2 foldsUV = patternUV * .18; foldsUV = rotate(foldsUV, 4. * u_seed); - vec2 w = folds(foldsUV); + vec2 folds1 = folds(foldsUV); foldsUV = rotate(foldsUV + .005 * cos(u_seed), .01 * sin(u_seed)); - vec2 w2 = folds(foldsUV); + vec2 folds2 = folds(foldsUV); - float drops = u_drops * drops(patternUV * 2.); + float drops = u_drops * getDrops(patternUV * 2.); - float fade = u_fade * fbm(.17 * patternUV + 10. * u_seed); + float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - w = mix(w, vec2(0.), fade); - w2 = mix(w2, vec2(0.), fade); + folds1 = mix(folds1, vec2(0.), fade); + folds2 = mix(folds2, vec2(0.), fade); crumples = mix(crumples, 0., fade); drops = mix(drops, 0., fade); fiber *= mix(1., .5, fade); @@ -335,8 +318,8 @@ void main() { grid = u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); } - normal -= 3. * u_folds * (w + w2); - normalImage += u_folds * w; + normal -= 3. * u_folds * (folds1 + folds2); + normalImage += u_folds * folds1; normal += 1.5 * crumples; normalImage += .4 * crumples; @@ -381,7 +364,8 @@ void main() { pic = mix(image.rgb, pic, .5 * u_blending); color = mix(color, pic, frame); - fragColor = vec4(color, opacity); +// fragColor = vec4(color, opacity); + fragColor = vec4(roughness, fiber, 0., 1.); } `; From 6ba8791fe93342e9e6527afcd256e1b9b537ce34 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 21:08:50 +0400 Subject: [PATCH 027/186] RF opt --- packages/shaders/src/shaders/paper-texture.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index e81a6f780..11f9fd871 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -132,10 +132,10 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { float scaleR = .1; float scaleF = 1.; float amplitude = 1.; - float rotAngle = 0.; - float angle = 0.7; - for (int i = 0; i < 4; i++) { - // Roughness: R channel at pR (only 3 iterations) + float c = 0.7648; // cos(0.7) + float s = 0.6442; // sin(0.7) + float rc = 1., rs = 0.; // accumulated rotation + for (int i = 0; i < 5; i++) { if (i < 3) { vec2 ipR = floor(pR); vec2 fpR = fract(pR); @@ -146,14 +146,15 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { float dR = texture(u_noiseTexture, uvR.zw).r; roughDx += scaleR * mix(cR - aR, dR - bR, fpR.y); float arg = .2 * pR.x + .5 * pR.y; - float s = sin(arg); - roughDx += scaleR * -.08 * exp(-2. * abs(s)) * sign(s) * cos(arg); + float sn = sin(arg); + roughDx += scaleR * -.08 * exp(-2. * abs(sn)) * sign(sn) * cos(arg); pR *= 2.1; scaleR *= 2.1; } - // Fiber: B channel at pF with rotation - pF = rotate(pF, angle); - rotAngle += angle; + pF = vec2(c * pF.x - s * pF.y, s * pF.x + c * pF.y); + float rc2 = rc * c - rs * s; + rs = rs * c + rc * s; + rc = rc2; vec2 ipF = floor(pF); vec2 fpF = fract(pF); vec4 uvF = fract(vec4(ipF, ipF + 1.) / 50. + .5); @@ -165,7 +166,6 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { vec2 du = 6. * fpF * (1. - fpF); float dxF = du.x * mix(bF - aF, dF - cF, u.y); float dyF = du.y * mix(cF - aF, dF - bF, u.x); - float rc = cos(rotAngle), rs = sin(rotAngle); fiberGrad += amplitude * scaleF * vec2(rc * dxF + rs * dyF, -rs * dxF + rc * dyF); pF *= 2.; scaleF *= 2.; @@ -265,8 +265,6 @@ void main() { vec2 crumplesUV = fract(mix(.45, .02, pow(u_crumpleSize, .3)) * patternUV - u_seed) * 32.; float crumples = u_crumples * (getCrumples(crumplesUV + vec2(.02, 0.)) - getCrumples(crumplesUV)); - - vec2 normal = vec2(0.); vec2 normalImage = vec2(0.); From 4fa8e7ef55d28ba91f586a4cbcd573a42613415a Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 23:08:15 +0400 Subject: [PATCH 028/186] getCrumples opt --- packages/shaders/src/shaders/paper-texture.ts | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 11f9fd871..d06440735 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -97,6 +97,14 @@ float getUvFrame(vec2 uv) { return left * right * bottom * top; } + +float lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +} +float sst(float edge0, float edge1, float x) { + return smoothstep(edge0, edge1, x); +} + ${ declarePI } ${ rotation2 } float randomR(vec2 p) { @@ -179,27 +187,39 @@ vec2 randomGB(vec2 p) { return texture(u_noiseTexture, fract(uv)).gb; } -float crumpledNoise(vec2 t, float pw) { - vec2 p = floor(t); - float wsum = 0.; - float cl = 0.; +float getCrumples(vec2 uv) { + vec2 t1 = uv * .25; + vec2 t2 = uv * .5; + vec2 p1 = floor(t1), p2 = floor(t2); + vec2 wsum1 = vec2(0.), cl1 = vec2(0.); + vec2 wsum2 = vec2(0.), cl2 = vec2(0.); for (int y = -1; y < 2; y += 1) { for (int x = -1; x < 2; x += 1) { vec2 b = vec2(float(x), float(y)); - vec2 q = b + p; - vec2 q2 = q - floor(q / 8.) * 8.; - vec2 c = q + randomGB(q2); - vec2 r = c - t; - float w = pow(smoothstep(0., 1., 1. - abs(r.x)), pw) * pow(smoothstep(0., 1., 1. - abs(r.y)), pw); - cl += (.5 + .5 * sin((q2.x + q2.y * 5.) * 8.)) * w; - wsum += w; + // Scale 1 + vec2 q1 = b + p1; + vec2 q1m = mod(q1, 8.); + vec2 c1 = q1 + randomGB(q1m); + float val1 = .5 + .5 * sin((q1m.x + q1m.y * 5.) * 8.); + vec2 r1 = c1 - t1; + float wy1 = pow(sst(0., 1., 1. - abs(r1.y)), 16.); + vec2 w1 = pow(smoothstep(0., 1., 1. - abs(vec2(r1.x, r1.x - .0125))), vec2(16.)) * wy1; + cl1 += val1 * w1; wsum1 += w1; + // Scale 2 + vec2 q2 = b + p2; + vec2 q2m = mod(q2, 8.); + vec2 c2 = q2 + randomGB(q2m); + float val2 = .5 + .5 * sin((q2m.x + q2m.y * 5.) * 8.); + vec2 r2 = c2 - t2; + float sy2 = sst(0., 1., 1. - abs(r2.y)); + float wy2 = sy2 * sy2; + vec2 sx2 = smoothstep(0., 1., 1. - abs(vec2(r2.x, r2.x - .025))); + vec2 w2 = sx2 * sx2 * wy2; + cl2 += val2 * w2; wsum2 += w2; } } - return pow(wsum != 0.0 ? cl / wsum : 0.0, .5) * 2.; -} - -float getCrumples(vec2 uv) { - return crumpledNoise(uv * .25, 16.) * crumpledNoise(uv * .5, 2.); + vec2 n = (cl1 / wsum1) * (cl2 / wsum2); + return 4. * (n.y - n.x); } vec2 folds(vec2 uv) { @@ -238,10 +258,6 @@ float getDrops(vec2 uv) { return 1. - smoothstep(.05, .09, pow(dropsMinDist, .5)); } -float lst(float edge0, float edge1, float x) { - return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); -} - vec3 blendMultiply(vec3 base, vec3 blend) { return base*blend; } @@ -262,8 +278,8 @@ void main() { float roughness = u_roughness * (rf.x + .5); float fiber = u_fiber * (rf.y - 1.); - vec2 crumplesUV = fract(mix(.45, .02, pow(u_crumpleSize, .3)) * patternUV - u_seed) * 32.; - float crumples = u_crumples * (getCrumples(crumplesUV + vec2(.02, 0.)) - getCrumples(crumplesUV)); + vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; + float crumples = .5 + getCrumples(crumplesUV); vec2 normal = vec2(0.); vec2 normalImage = vec2(0.); @@ -363,7 +379,7 @@ void main() { color = mix(color, pic, frame); // fragColor = vec4(color, opacity); - fragColor = vec4(roughness, fiber, 0., 1.); + fragColor = vec4(crumples, 0., 0., 1.); } `; From 2910fd3b9f40ad27ab414df9bf98bce7f53ba9e7 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 23:22:41 +0400 Subject: [PATCH 029/186] small rearrangement --- packages/shaders/src/shaders/paper-texture.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index d06440735..5a5cd1f0a 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -222,7 +222,25 @@ float getCrumples(vec2 uv) { return 4. * (n.y - n.x); } -vec2 folds(vec2 uv) { + +float getDrops(vec2 uv) { + vec2 iDropsUV = floor(uv); + vec2 fDropsUV = fract(uv); + float dropsMinDist = 1.; + for (int y = -1; y < 2; y += 1) { + for (int x = -1; x < 2; x += 1) { + vec2 neighbor = vec2(float(y), float(x)); + vec2 offset = randomGB(iDropsUV + neighbor); + offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); + vec2 pos = neighbor + offset - fDropsUV; + float dist = length(pos); + dropsMinDist = min(dropsMinDist, dropsMinDist*dist); + } + } + return 1. - lst(.05, .09, pow(dropsMinDist, .5)); +} + +vec2 getFolds(vec2 uv) { vec3 pp = vec3(0.); float l = 9.; for (float i = 0.; i < 15.; i++) { @@ -241,23 +259,6 @@ vec2 folds(vec2 uv) { return mix(pp.xy, vec2(0.), pow(pp.z, .15)); } -float getDrops(vec2 uv) { - vec2 iDropsUV = floor(uv); - vec2 fDropsUV = fract(uv); - float dropsMinDist = 1.; - for (int j = -1; j <= 1; j++) { - for (int i = -1; i <= 1; i++) { - vec2 neighbor = vec2(float(i), float(j)); - vec2 offset = randomGB(iDropsUV + neighbor); - offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); - vec2 pos = neighbor + offset - fDropsUV; - float dist = length(pos); - dropsMinDist = min(dropsMinDist, dropsMinDist*dist); - } - } - return 1. - smoothstep(.05, .09, pow(dropsMinDist, .5)); -} - vec3 blendMultiply(vec3 base, vec3 blend) { return base*blend; } @@ -281,16 +282,16 @@ void main() { vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; float crumples = .5 + getCrumples(crumplesUV); + float drops = getDrops(patternUV * 2.); + vec2 normal = vec2(0.); vec2 normalImage = vec2(0.); vec2 foldsUV = patternUV * .18; foldsUV = rotate(foldsUV, 4. * u_seed); - vec2 folds1 = folds(foldsUV); + vec2 folds1 = getFolds(foldsUV); foldsUV = rotate(foldsUV + .005 * cos(u_seed), .01 * sin(u_seed)); - vec2 folds2 = folds(foldsUV); - - float drops = u_drops * getDrops(patternUV * 2.); + vec2 folds2 = getFolds(foldsUV); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); @@ -379,7 +380,7 @@ void main() { color = mix(color, pic, frame); // fragColor = vec4(color, opacity); - fragColor = vec4(crumples, 0., 0., 1.); + fragColor = vec4(crumples, drops, 0., 1.); } `; From 67083c3a140e61220f52e1344c570cc4a5b74fd1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 23:38:24 +0400 Subject: [PATCH 030/186] folds opt1 --- packages/shaders/src/shaders/paper-texture.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5a5cd1f0a..b97dbed9d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -240,23 +240,21 @@ float getDrops(vec2 uv) { return 1. - lst(.05, .09, pow(dropsMinDist, .5)); } -vec2 getFolds(vec2 uv) { - vec3 pp = vec3(0.); +float getFolds(vec2 uv) { + vec2 pp = vec2(0.); float l = 9.; - for (float i = 0.; i < 15.; i++) { - if (i >= u_foldCount) break; - vec2 rand = randomGB(vec2(i, i * u_seed)); + for (int i = 0; i < 15; i++) { + if (float(i) >= u_foldCount) break; + vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; float dist = distance(uv, p); - l = min(l, dist); - - if (l == dist) { - pp.xy = (uv - p.xy); - pp.z = dist; + if (dist < l) { + l = dist; + pp = vec2(uv.x - p.x, dist); } } - return mix(pp.xy, vec2(0.), pow(pp.z, .15)); + return mix(pp.x, 0., pow(pp.y, .15)); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -289,15 +287,15 @@ void main() { vec2 foldsUV = patternUV * .18; foldsUV = rotate(foldsUV, 4. * u_seed); - vec2 folds1 = getFolds(foldsUV); - foldsUV = rotate(foldsUV + .005 * cos(u_seed), .01 * sin(u_seed)); - vec2 folds2 = getFolds(foldsUV); + float folds1 = getFolds(foldsUV); + foldsUV = rotate(foldsUV + .01 * cos(u_seed), .02 * sin(u_seed)); + float folds2 = getFolds(foldsUV); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - folds1 = mix(folds1, vec2(0.), fade); - folds2 = mix(folds2, vec2(0.), fade); + folds1 = mix(folds1, 0., fade); + folds2 = mix(folds2, 0., fade); crumples = mix(crumples, 0., fade); drops = mix(drops, 0., fade); fiber *= mix(1., .5, fade); @@ -380,7 +378,7 @@ void main() { color = mix(color, pic, frame); // fragColor = vec4(color, opacity); - fragColor = vec4(crumples, drops, 0., 1.); + fragColor = vec4(folds1 + folds2 + .5, 0., 0., 1.); } `; From 8d895ec89de8a35c6d99f3cefce1376165a3ecb4 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 23:40:43 +0400 Subject: [PATCH 031/186] folds opt2 --- packages/shaders/src/shaders/paper-texture.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b97dbed9d..1d68eb84b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -240,21 +240,29 @@ float getDrops(vec2 uv) { return 1. - lst(.05, .09, pow(dropsMinDist, .5)); } -float getFolds(vec2 uv) { - vec2 pp = vec2(0.); - float l = 9.; +vec2 getFolds(vec2 uv1, vec2 uv2) { + vec2 pp1 = vec2(0.), pp2 = vec2(0.); + float l1 = 9., l2 = 9.; for (int i = 0; i < 15; i++) { if (float(i) >= u_foldCount) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; - float dist = distance(uv, p); - if (dist < l) { - l = dist; - pp = vec2(uv.x - p.x, dist); + float dist1 = distance(uv1, p); + if (dist1 < l1) { + l1 = dist1; + pp1 = vec2(uv1.x - p.x, dist1); + } + float dist2 = distance(uv2, p); + if (dist2 < l2) { + l2 = dist2; + pp2 = vec2(uv2.x - p.x, dist2); } } - return mix(pp.x, 0., pow(pp.y, .15)); + return vec2( + mix(pp1.x, 0., pow(pp1.y, .15)), + mix(pp2.x, 0., pow(pp2.y, .15)) + ); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -285,11 +293,10 @@ void main() { vec2 normal = vec2(0.); vec2 normalImage = vec2(0.); - vec2 foldsUV = patternUV * .18; - foldsUV = rotate(foldsUV, 4. * u_seed); - float folds1 = getFolds(foldsUV); - foldsUV = rotate(foldsUV + .01 * cos(u_seed), .02 * sin(u_seed)); - float folds2 = getFolds(foldsUV); + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .02 * sin(u_seed)); + vec2 folds12 = getFolds(foldsUV1, foldsUV2); + float folds1 = folds12.x, folds2 = folds12.y; float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); From 34abca5bc51984b1f8bdead0e7a0291fee1e4427 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 13 Jan 2026 23:49:23 +0400 Subject: [PATCH 032/186] grid opt1 --- packages/shaders/src/shaders/paper-texture.ts | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 1d68eb84b..deaa8ebc4 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -265,6 +265,20 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { ); } +vec2 getGrid(vec2 uv, vec3 lightPos) { + float gridX = fract(uv.x * .1 * u_gridCount + .5); + float dx = gridX - .5; + float foldWidth = u_gridShape; + float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); + float angle = sign(dx) * 1.1345 * foldAmount; + float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); + vec3 n = vec3(-sin(angle), 0., cos(angle)); + float grid = max(dot(n, lightPos), 0.) * creaseDark; + grid = u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); + float dropsMask = .2 + .8 * step(0., dx); + return vec2(grid, dropsMask); +} + vec3 blendMultiply(vec3 base, vec3 blend) { return base*blend; } @@ -295,51 +309,24 @@ void main() { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .02 * sin(u_seed)); - vec2 folds12 = getFolds(foldsUV1, foldsUV2); - float folds1 = folds12.x, folds2 = folds12.y; + vec2 folds = getFolds(foldsUV1, foldsUV2); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - folds1 = mix(folds1, 0., fade); - folds2 = mix(folds2, 0., fade); + folds = mix(folds, vec2(0.), fade); crumples = mix(crumples, 0., fade); drops = mix(drops, 0., fade); fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); vec3 lightPos = vec3(.5, 1., .5); - float grid = 0.; - { - vec2 gridUV = patternUV; - gridUV.x = fract(gridUV.x * .1 * u_gridCount + .5); - float foldX = .5; - - float dx = gridUV.x - foldX; - - drops *= (.2 + .8 * step(0., dx)); - - float foldWidth = u_gridShape; - - float foldAmount = 1. - smoothstep(0.0, foldWidth, abs(dx)); - float angle = sign(dx) * 1.1345 * foldAmount; - float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); - - vec3 n = vec3(0.0, 0.0, 1.0); - mat3 rotY = mat3( - cos(angle), 0.0, sin(angle), - 0.0, 1.0, 0.0, - -sin(angle), 0.0, cos(angle) - ); - n = rotY * n; - - grid = max(dot(n, lightPos), 0.); - grid *= creaseDark; - grid = u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); - } + vec2 gridResult = getGrid(patternUV, lightPos); + float grid = gridResult.x; + drops *= gridResult.y; - normal -= 3. * u_folds * (folds1 + folds2); - normalImage += u_folds * folds1; + normal -= 3. * u_folds * (folds.x + folds.x); + normalImage += u_folds * folds.x; normal += 1.5 * crumples; normalImage += .4 * crumples; @@ -385,7 +372,7 @@ void main() { color = mix(color, pic, frame); // fragColor = vec4(color, opacity); - fragColor = vec4(folds1 + folds2 + .5, 0., 0., 1.); + fragColor = vec4(grid, 0., 0., 1.); } `; From 3a6fca39db7f31369e92490b014d39c0a74b8702 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 14 Jan 2026 00:18:10 +0400 Subject: [PATCH 033/186] pattern construct --- packages/shaders/src/shaders/paper-texture.ts | 71 +++++++------------ 1 file changed, 27 insertions(+), 44 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index deaa8ebc4..a4e3afbd1 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -276,7 +276,7 @@ vec2 getGrid(vec2 uv, vec3 lightPos) { float grid = max(dot(n, lightPos), 0.) * creaseDark; grid = u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); float dropsMask = .2 + .8 * step(0., dx); - return vec2(grid, dropsMask); + return vec2(.5 * grid, dropsMask); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -300,79 +300,62 @@ void main() { float fiber = u_fiber * (rf.y - 1.); vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; - float crumples = .5 + getCrumples(crumplesUV); + float crumples = u_crumples * (.5 + getCrumples(crumplesUV)); - float drops = getDrops(patternUV * 2.); - - vec2 normal = vec2(0.); - vec2 normalImage = vec2(0.); + float drops = u_drops * getDrops(patternUV * 2.); vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .02 * sin(u_seed)); - vec2 folds = getFolds(foldsUV1, foldsUV2); + vec2 folds = u_folds * getFolds(foldsUV1, foldsUV2); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - folds = mix(folds, vec2(0.), fade); - crumples = mix(crumples, 0., fade); - drops = mix(drops, 0., fade); - fiber *= mix(1., .5, fade); - roughness *= mix(1., .5, fade); - vec3 lightPos = vec3(.5, 1., .5); vec2 gridResult = getGrid(patternUV, lightPos); float grid = gridResult.x; drops *= gridResult.y; - - normal -= 3. * u_folds * (folds.x + folds.x); - normalImage += u_folds * folds.x; - - normal += 1.5 * crumples; - normalImage += .4 * crumples; - - normal += 2.5 * drops; - normalImage += .2 * drops; - normal += 1.5 * roughness; - normalImage += .1 * roughness; - - normal += fiber; - normalImage += .1 * fiber; - - normal += grid; - normalImage += .2 * grid; + folds = mix(folds, vec2(0.), fade); + crumples = mix(crumples, 0., fade); + drops = mix(drops, 0., fade); + fiber *= mix(1., .5, fade); + roughness *= mix(1., .5, fade); + grid *= mix(1., .0, fade); - float res = dot(normalize(vec3(normal, mix(7.5, .5, pow(u_contrast, .1)))), lightPos); + float pattern = roughness; + pattern += fiber; + pattern += crumples; + pattern += (folds.x + folds.y); + pattern += grid; + pattern += drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; - imageUV += .1 * u_distortion * normalImage; + imageUV += .1 * u_distortion * pattern; float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); - image.rgb += .4 * pow(u_contrast, .4) * (.3 - res); - + image.rgb += .4 * pow(u_contrast, .4) * (.3 - pattern); frame *= image.a; - vec3 color = fgColor * res; - float opacity = fgOpacity * res; + vec3 color = fgColor * pattern; + float opacity = fgOpacity * pattern; color += bgColor * (1. - opacity); opacity += bgOpacity * (1. - opacity); opacity = mix(opacity, 1., frame); - color -= .007 * drops; - - float blendOpacity = (.5 + .5 * res); - vec3 pic = blendMultiply(color, image.rgb + .2 * 1. - u_blending * length(normalImage), .5 * res + .2 * u_blending); - pic = mix(image.rgb, pic, .5 * u_blending); - color = mix(color, pic, frame); +// color -= .007 * drops; +// +// float blendOpacity = (.5 + .5 * pattern); +// vec3 pic = blendMultiply(color, image.rgb + .2 * 1. - u_blending * pattern, .5 * pattern + .2 * u_blending); +// pic = mix(image.rgb, pic, .5 * u_blending); +// color = mix(color, pic, frame); -// fragColor = vec4(color, opacity); - fragColor = vec4(grid, 0., 0., 1.); + fragColor = vec4(color, opacity); } `; From fbcc3f74bf1fc56e1336b5da7b3bcd1fab2b066f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 14 Jan 2026 02:06:39 +0400 Subject: [PATCH 034/186] grid progr --- packages/shaders/src/shaders/paper-texture.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index a4e3afbd1..3df82264d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -265,18 +265,16 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { ); } -vec2 getGrid(vec2 uv, vec3 lightPos) { +vec2 getGrid(vec2 uv) { float gridX = fract(uv.x * .1 * u_gridCount + .5); float dx = gridX - .5; float foldWidth = u_gridShape; float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); float angle = sign(dx) * 1.1345 * foldAmount; float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); - vec3 n = vec3(-sin(angle), 0., cos(angle)); - float grid = max(dot(n, lightPos), 0.) * creaseDark; - grid = u_grid * smoothstep(0., 1., grid) * smoothstep(0., .3, u_contrast); + float grid = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * creaseDark; float dropsMask = .2 + .8 * step(0., dx); - return vec2(.5 * grid, dropsMask); + return vec2(grid, dropsMask); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -311,8 +309,7 @@ void main() { float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - vec3 lightPos = vec3(.5, 1., .5); - vec2 gridResult = getGrid(patternUV, lightPos); + vec2 gridResult = u_grid * getGrid(patternUV); float grid = gridResult.x; drops *= gridResult.y; From f6589fd2a12c6c20a492ec33629ccbe8f5f6fa4e Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 14 Jan 2026 02:09:00 +0400 Subject: [PATCH 035/186] roughness small opt --- packages/shaders/src/shaders/paper-texture.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 3df82264d..1284e90de 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -153,9 +153,6 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { float cR = texture(u_noiseTexture, uvR.zy).r; float dR = texture(u_noiseTexture, uvR.zw).r; roughDx += scaleR * mix(cR - aR, dR - bR, fpR.y); - float arg = .2 * pR.x + .5 * pR.y; - float sn = sin(arg); - roughDx += scaleR * -.08 * exp(-2. * abs(sn)) * sign(sn) * cos(arg); pR *= 2.1; scaleR *= 2.1; } @@ -179,7 +176,7 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { scaleF *= 2.; amplitude *= 0.6; } - return vec2(roughDx / 1.5, length(fiberGrad)); + return vec2(roughDx, length(fiberGrad)); } vec2 randomGB(vec2 p) { From 0bd2060684161831e743dd1370ceb76a5aa70710 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 14 Jan 2026 02:33:31 +0400 Subject: [PATCH 036/186] texture tweaks --- packages/shaders/src/shaders/paper-texture.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 1284e90de..32160d3bd 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -270,7 +270,7 @@ vec2 getGrid(vec2 uv) { float angle = sign(dx) * 1.1345 * foldAmount; float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); float grid = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * creaseDark; - float dropsMask = .2 + .8 * step(0., dx); + float dropsMask = 1. - .8 * step(0., dx); return vec2(grid, dropsMask); } @@ -291,24 +291,24 @@ void main() { vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); - float roughness = u_roughness * (rf.x + .5); - float fiber = u_fiber * (rf.y - 1.); + float roughness = u_roughness * pow(1.3 * clamp(rf.x + .5, 0., 1.), 3.); + float fiber = u_fiber * clamp(rf.y - 1., 0., 1.); vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; - float crumples = u_crumples * (.5 + getCrumples(crumplesUV)); + float crumples = u_crumples * clamp(.2 + getCrumples(crumplesUV), 0., 1.); float drops = u_drops * getDrops(patternUV * 2.); vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .02 * sin(u_seed)); - vec2 folds = u_folds * getFolds(foldsUV1, foldsUV2); + vec2 folds = u_folds * clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - vec2 gridResult = u_grid * getGrid(patternUV); + vec2 gridResult = u_grid * clamp(getGrid(patternUV), 0., 1.); float grid = gridResult.x; - drops *= gridResult.y; + drops *= mix(1., gridResult.y, u_grid); folds = mix(folds, vec2(0.), fade); crumples = mix(crumples, 0., fade); @@ -342,8 +342,6 @@ void main() { opacity += bgOpacity * (1. - opacity); opacity = mix(opacity, 1., frame); -// color -= .007 * drops; -// // float blendOpacity = (.5 + .5 * pattern); // vec3 pic = blendMultiply(color, image.rgb + .2 * 1. - u_blending * pattern, .5 * pattern + .2 * u_blending); // pic = mix(image.rgb, pic, .5 * u_blending); From bf442505e5a3bb13d0b25e4b3a6ca2dac88ea5b5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 14 Jan 2026 02:43:04 +0400 Subject: [PATCH 037/186] normal preset back --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + packages/shaders-react/src/shaders/paper-texture.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 31924d701..733db6081 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -67,6 +67,7 @@ const PaperTextureWithControls = () => { ]) ); return { + speed: { value: defaults.speed, min: 0, max: 3, order: 0 }, colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, contrast: { value: defaults.contrast, min: 0, max: 1, order: 197 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index dde2068d4..e5855dd21 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -33,7 +33,7 @@ export const defaultPreset: PaperTexturePreset = { distortion: 0.5, fit: 'contain', scale: 0.8, - speed: 1, + speed: 0, frame: 0, colorFront: '#bbbfc4', colorBack: '#ffffff', @@ -44,14 +44,14 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0.3, crumpleSize: 0.5, - folds: 0.65, - foldCount: 12, + folds: 0.5, + foldCount: 6, grid: 0, gridShape: 0.35, gridCount: 3, fade: 0, drops: 0.2, - seed: 43, + seed: 808, }, }; @@ -124,7 +124,7 @@ export const spreadPreset: PaperTexturePreset = { contrast: 0.47, blending: 0.36, distortion: 0.50, - seed: 280, + seed: 448, roughness: 0.4, roughnessSize: 0.4, fiber: 0.24, @@ -134,7 +134,7 @@ export const spreadPreset: PaperTexturePreset = { folds: 0.3, foldCount: 4, grid: 0.7, - gridShape: 0.48, + gridShape: 0.15, gridCount: 1, drops: 0, fade: 0.68, From 154e9643259dbe6767ea75536c8a83cc13b69bf8 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 14 Jan 2026 13:20:00 +0400 Subject: [PATCH 038/186] folds tweaks --- packages/shaders-react/src/shaders/paper-texture.tsx | 8 ++++---- packages/shaders/src/shaders/paper-texture.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e5855dd21..c06b1aef2 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -35,7 +35,7 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#bbbfc4', + colorFront: '#d1d6dc', colorBack: '#ffffff', contrast: 0.3, roughness: 0.4, @@ -44,14 +44,14 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0.3, crumpleSize: 0.5, - folds: 0.5, - foldCount: 6, + folds: 0.4, + foldCount: 11, grid: 0, gridShape: 0.35, gridCount: 3, fade: 0, drops: 0.2, - seed: 808, + seed: 285, }, }; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 32160d3bd..bfc6a6c93 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -238,7 +238,7 @@ float getDrops(vec2 uv) { } vec2 getFolds(vec2 uv1, vec2 uv2) { - vec2 pp1 = vec2(0.), pp2 = vec2(0.); + vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; for (int i = 0; i < 15; i++) { if (float(i) >= u_foldCount) break; @@ -248,17 +248,17 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { float dist1 = distance(uv1, p); if (dist1 < l1) { l1 = dist1; - pp1 = vec2(uv1.x - p.x, dist1); + pp1 = vec3(uv1.x - p.x, dist1, rand.y); } float dist2 = distance(uv2, p); if (dist2 < l2) { l2 = dist2; - pp2 = vec2(uv2.x - p.x, dist2); + pp2 = vec3(uv2.x - p.x, dist2, rand.y); } } return vec2( - mix(pp1.x, 0., pow(pp1.y, .15)), - mix(pp2.x, 0., pow(pp2.y, .15)) + mix(pp1.x, .2 * pp1.z, pow(pp1.y, .15)), + mix(pp2.x, .2 * pp2.z, pow(pp2.y, .15)) ); } @@ -300,7 +300,7 @@ void main() { float drops = u_drops * getDrops(patternUV * 2.); vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .02 * sin(u_seed)); + vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .015 * sin(u_seed)); vec2 folds = u_folds * clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); From a498fc9c0189241bdddd78243417b7c148bd1691 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 18 Jan 2026 20:12:14 +0400 Subject: [PATCH 039/186] tweaks --- packages/shaders-react/src/shaders/paper-texture.tsx | 4 ++-- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index c06b1aef2..b8cb4dc40 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -35,7 +35,7 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#d1d6dc', + colorFront: '#c4cbd4', colorBack: '#ffffff', contrast: 0.3, roughness: 0.4, @@ -50,7 +50,7 @@ export const defaultPreset: PaperTexturePreset = { gridShape: 0.35, gridCount: 3, fade: 0, - drops: 0.2, + drops: 0.4, seed: 285, }, }; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index bfc6a6c93..6155a73ef 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -196,7 +196,7 @@ float getCrumples(vec2 uv) { // Scale 1 vec2 q1 = b + p1; vec2 q1m = mod(q1, 8.); - vec2 c1 = q1 + randomGB(q1m); + vec2 c1 = q1 + randomGB(q1m + u_seed); float val1 = .5 + .5 * sin((q1m.x + q1m.y * 5.) * 8.); vec2 r1 = c1 - t1; float wy1 = pow(sst(0., 1., 1. - abs(r1.y)), 16.); @@ -300,7 +300,7 @@ void main() { float drops = u_drops * getDrops(patternUV * 2.); vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = rotate(foldsUV1 + .01 * cos(u_seed), .015 * sin(u_seed)); + vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); vec2 folds = u_folds * clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); From ebbacaa09ee659815f1c9b35bbbceaa84a9ed16e Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 18 Jan 2026 20:29:06 +0400 Subject: [PATCH 040/186] gridShape to affect folds --- packages/shaders/src/shaders/paper-texture.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6155a73ef..9e030e2ba 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -256,9 +256,10 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { pp2 = vec3(uv2.x - p.x, dist2, rand.y); } } + float mult2 = mix(.22, .02, u_gridShape); return vec2( - mix(pp1.x, .2 * pp1.z, pow(pp1.y, .15)), - mix(pp2.x, .2 * pp2.z, pow(pp2.y, .15)) + mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), + mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)) ); } @@ -302,6 +303,7 @@ void main() { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); vec2 folds = u_folds * clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); +// folds = mix(folds, smoothstep(vec2(0.), vec2(1.), folds), u_gridShape); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); From 2dcba0bb1b8d3fbd8b167e77ecb1dc5681fdcb9a Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 18 Jan 2026 22:32:27 +0400 Subject: [PATCH 041/186] gridShape => foldsShape renaming --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- .../shaders-react/src/shaders/paper-texture.tsx | 14 +++++++------- packages/shaders/src/shaders/paper-texture.ts | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 733db6081..58c483165 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -83,7 +83,7 @@ const PaperTextureWithControls = () => { folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 231 }, grid: { value: defaults.grid, min: 0, max: 1, order: 240 }, - gridShape: { value: defaults.gridShape, min: 0, max: 1, order: 241 }, + foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 241 }, gridCount: { value: defaults.gridCount, min: 1, max: 20, order: 242 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index b8cb4dc40..0064b808c 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -47,7 +47,7 @@ export const defaultPreset: PaperTexturePreset = { folds: 0.4, foldCount: 11, grid: 0, - gridShape: 0.35, + foldsShape: 0.35, gridCount: 3, fade: 0, drops: 0.4, @@ -74,7 +74,7 @@ export const fiberPreset: PaperTexturePreset = { folds: 0, foldCount: 3, grid: 0, - gridShape: 0.50, + foldsShape: 0.50, gridCount: 10, drops: 1, fade: 1, @@ -107,7 +107,7 @@ export const cardboardPreset: PaperTexturePreset = { folds: 0, foldCount: 1, grid: 0.25, - gridShape: 0.28, + foldsShape: 0.28, gridCount: 20, fade: 0, drops: 0.1, @@ -134,7 +134,7 @@ export const spreadPreset: PaperTexturePreset = { folds: 0.3, foldCount: 4, grid: 0.7, - gridShape: 0.15, + foldsShape: 0.15, gridCount: 1, drops: 0, fade: 0.68, @@ -165,7 +165,7 @@ export const flatPreset: PaperTexturePreset = { folds: 0, foldCount: 6, grid: 0.0, - gridShape: 0.30, + foldsShape: 0.30, gridCount: 20.0, drops: 0.20, fade: 0.75, @@ -198,7 +198,7 @@ export const PaperTexture: React.FC = memo(function PaperText crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, grid = defaultPreset.params.grid, - gridShape = defaultPreset.params.gridShape, + foldsShape = defaultPreset.params.foldsShape, gridCount = defaultPreset.params.gridCount, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, @@ -244,7 +244,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_foldCount: foldCount, u_folds: folds, u_grid: grid, - u_gridShape: gridShape, + u_foldsShape: foldsShape, u_gridCount: gridCount, u_fade: fade, u_drops: drops, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9e030e2ba..9af56193b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -23,7 +23,7 @@ import { rotation2, declarePI } from '../shader-utils.js'; * - u_folds (float): Depth of the folds (0 to 1) * - u_foldCount (float): Number of folds (1 to 15) * - u_grid (float): Intensity of the grid / crease pattern (0 to 1) - * - u_gridShape (float): Shape/width of the grid creases (0 to 1) + * - u_foldsShape (float): Shape/width of the grid creases (0 to 1) * - u_gridCount (float): Number/density of grid creases (0 to N) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds, crumples and dots (0 to 1000) @@ -72,7 +72,7 @@ uniform float u_crumpleSize; uniform float u_folds; uniform float u_foldCount; uniform float u_grid; -uniform float u_gridShape; +uniform float u_foldsShape; uniform float u_gridCount; uniform float u_drops; uniform float u_seed; @@ -256,7 +256,7 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { pp2 = vec3(uv2.x - p.x, dist2, rand.y); } } - float mult2 = mix(.22, .02, u_gridShape); + float mult2 = mix(.22, .02, u_foldsShape); return vec2( mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)) @@ -266,7 +266,7 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { vec2 getGrid(vec2 uv) { float gridX = fract(uv.x * .1 * u_gridCount + .5); float dx = gridX - .5; - float foldWidth = u_gridShape; + float foldWidth = u_foldsShape; float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); float angle = sign(dx) * 1.1345 * foldAmount; float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); @@ -303,7 +303,7 @@ void main() { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); vec2 folds = u_folds * clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); -// folds = mix(folds, smoothstep(vec2(0.), vec2(1.), folds), u_gridShape); +// folds = mix(folds, smoothstep(vec2(0.), vec2(1.), folds), u_foldsShape); float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); @@ -367,7 +367,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_foldCount: number; u_folds: number; u_grid: number; - u_gridShape: number; + u_foldsShape: number; u_gridCount: number; u_fade: number; u_crumpleSize: number; @@ -390,7 +390,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara foldCount?: number; folds?: number; grid?: number; - gridShape?: number; + foldsShape?: number; gridCount?: number; fade?: number; crumpleSize?: number; From 7c4bbd7bba71f7ecd1dd0040b1ac8b45e4ac3ce2 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 18 Jan 2026 23:54:45 +0400 Subject: [PATCH 042/186] merging folds and grid --- docs/src/app/(shaders)/paper-texture/page.tsx | 13 +++-- .../src/shaders/paper-texture.tsx | 30 +++++------ packages/shaders/src/index.ts | 2 + packages/shaders/src/shaders/paper-texture.ts | 52 +++++++++---------- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 58c483165..db32393b6 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -5,7 +5,11 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { ShaderFit } from '@paper-design/shaders'; +import { + ShaderFit, + PaperTextureFoldTypes, + type PaperTextureFoldType +} from '@paper-design/shaders'; import { levaImageButton, levaDeleteImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; @@ -81,10 +85,9 @@ const PaperTextureWithControls = () => { crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, - foldCount: { value: defaults.foldCount, min: 1, max: 15, step: 1, order: 231 }, - grid: { value: defaults.grid, min: 0, max: 1, order: 240 }, - foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 241 }, - gridCount: { value: defaults.gridCount, min: 1, max: 20, order: 242 }, + foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, + foldCount: { value: defaults.foldCount, min: 1, max: 20, step: 1, order: 232 }, + foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 0064b808c..e22a8b622 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -6,6 +6,7 @@ import { getShaderColorFromString, getShaderNoiseTexture, paperTextureFragmentShader, + PaperTextureFoldTypes, ShaderFitOptions, type ImageShaderPreset, type PaperTextureParams, @@ -45,10 +46,9 @@ export const defaultPreset: PaperTexturePreset = { crumples: 0.3, crumpleSize: 0.5, folds: 0.4, + foldType: 'folds', foldCount: 11, - grid: 0, foldsShape: 0.35, - gridCount: 3, fade: 0, drops: 0.4, seed: 285, @@ -72,10 +72,9 @@ export const fiberPreset: PaperTexturePreset = { crumples: 0, crumpleSize: 0.30, folds: 0, + foldType: 'folds', foldCount: 3, - grid: 0, foldsShape: 0.50, - gridCount: 10, drops: 1, fade: 1, scale: 0.80, @@ -104,11 +103,10 @@ export const cardboardPreset: PaperTexturePreset = { fiberSize: 0.14, crumples: 0., crumpleSize: 0.1, - folds: 0, - foldCount: 1, - grid: 0.25, + folds: 0.25, + foldType: 'creases', + foldCount: 20, foldsShape: 0.28, - gridCount: 20, fade: 0, drops: 0.1, seed: 1.6, @@ -131,11 +129,10 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0.2, crumpleSize: 0.42, - folds: 0.3, - foldCount: 4, - grid: 0.7, + folds: 0.7, + foldType: 'creases', + foldCount: 1, foldsShape: 0.15, - gridCount: 1, drops: 0, fade: 0.68, scale: 0.8, @@ -163,10 +160,9 @@ export const flatPreset: PaperTexturePreset = { crumples: 0.7, crumpleSize: 0.2, folds: 0, + foldType: 'folds', foldCount: 6, - grid: 0.0, foldsShape: 0.30, - gridCount: 20.0, drops: 0.20, fade: 0.75, scale: 0.8, @@ -197,9 +193,8 @@ export const PaperTexture: React.FC = memo(function PaperText fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, - grid = defaultPreset.params.grid, + foldType = defaultPreset.params.foldType, foldsShape = defaultPreset.params.foldsShape, - gridCount = defaultPreset.params.gridCount, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, @@ -243,9 +238,8 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumpleSize: crumpleSize, u_foldCount: foldCount, u_folds: folds, - u_grid: grid, + u_foldType: PaperTextureFoldTypes[foldType], u_foldsShape: foldsShape, - u_gridCount: gridCount, u_fade: fade, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index 1fa838072..fca1f8569 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -156,6 +156,8 @@ export { /** A static texture built from multiple noise layers, usable for a realistic paper and cardboard surfaces or generating abstract patterns. Can be used as a image filter or as a texture */ export { paperTextureFragmentShader, + PaperTextureFoldTypes, + type PaperTextureFoldType, type PaperTextureParams, type PaperTextureUniforms, } from './shaders/paper-texture.js'; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9af56193b..9b5049845 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -21,10 +21,9 @@ import { rotation2, declarePI } from '../shader-utils.js'; * - u_crumples (float): Cell-based crumple pattern intensity (0 to 1) * - u_crumpleSize (float): Cell-based crumple pattern scale (0 to 1) * - u_folds (float): Depth of the folds (0 to 1) - * - u_foldCount (float): Number of folds (1 to 15) - * - u_grid (float): Intensity of the grid / crease pattern (0 to 1) - * - u_foldsShape (float): Shape/width of the grid creases (0 to 1) - * - u_gridCount (float): Number/density of grid creases (0 to N) + * - u_foldType (float): Type of folds pattern (0 = radial folds, 1 = creases) + * - u_foldCount (float): Number of folds or creases (1 to 20) + * - u_foldsShape (float): Shape/width of the folds (0 to 1) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds, crumples and dots (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) @@ -70,10 +69,9 @@ uniform float u_fiberSize; uniform float u_crumples; uniform float u_crumpleSize; uniform float u_folds; +uniform float u_foldType; uniform float u_foldCount; -uniform float u_grid; uniform float u_foldsShape; -uniform float u_gridCount; uniform float u_drops; uniform float u_seed; uniform float u_fade; @@ -264,7 +262,7 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { } vec2 getGrid(vec2 uv) { - float gridX = fract(uv.x * .1 * u_gridCount + .5); + float gridX = fract(uv.x * .1 * u_foldCount + .5); float dx = gridX - .5; float foldWidth = u_foldsShape; float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); @@ -300,30 +298,27 @@ void main() { float drops = u_drops * getDrops(patternUV * 2.); - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); - vec2 folds = u_folds * clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); -// folds = mix(folds, smoothstep(vec2(0.), vec2(1.), folds), u_foldsShape); - float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - vec2 gridResult = u_grid * clamp(getGrid(patternUV), 0., 1.); - float grid = gridResult.x; - drops *= mix(1., gridResult.y, u_grid); + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); + vec2 radialFolds = u_folds * clamp(4. * getFolds(foldsUV1, foldsUV2), 0., 1.); + vec2 creasesResult = u_folds * clamp(getGrid(patternUV), 0., 1.); + float creases = creasesResult.x; + drops *= mix(1., creasesResult.y, u_folds * u_foldType); - folds = mix(folds, vec2(0.), fade); + radialFolds = mix(radialFolds, vec2(0.), fade); + creases *= mix(1., .0, fade); crumples = mix(crumples, 0., fade); drops = mix(drops, 0., fade); fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); - grid *= mix(1., .0, fade); - + float pattern = roughness; pattern += fiber; pattern += crumples; - pattern += (folds.x + folds.y); - pattern += grid; + pattern += mix(radialFolds.x + radialFolds.y, creases, u_foldType); pattern += drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; @@ -364,11 +359,10 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_fiber: number; u_fiberSize: number; u_crumples: number; - u_foldCount: number; u_folds: number; - u_grid: number; + u_foldType: number; + u_foldCount: number; u_foldsShape: number; - u_gridCount: number; u_fade: number; u_crumpleSize: number; u_drops: number; @@ -387,11 +381,10 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara fiber?: number; fiberSize?: number; crumples?: number; - foldCount?: number; folds?: number; - grid?: number; + foldType?: PaperTextureFoldType; + foldCount?: number; foldsShape?: number; - gridCount?: number; fade?: number; crumpleSize?: number; drops?: number; @@ -399,3 +392,10 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara blending?: number; distortion?: number; } + +export type PaperTextureFoldType = 'folds' | 'creases'; + +export const PaperTextureFoldTypes: Record = { + folds: 0, + creases: 1, +}; From a9e462a25d1479e0e0692bf3463fe3fe0100220d Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 18 Jan 2026 23:59:05 +0400 Subject: [PATCH 043/186] branching folds selector --- packages/shaders/src/shaders/paper-texture.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9b5049845..753adfa7c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -301,15 +301,19 @@ void main() { float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); - vec2 radialFolds = u_folds * clamp(4. * getFolds(foldsUV1, foldsUV2), 0., 1.); - vec2 creasesResult = u_folds * clamp(getGrid(patternUV), 0., 1.); - float creases = creasesResult.x; - drops *= mix(1., creasesResult.y, u_folds * u_foldType); - - radialFolds = mix(radialFolds, vec2(0.), fade); - creases *= mix(1., .0, fade); + float foldsPattern = 0.; + if (u_foldType < .5) { + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); + vec2 radialFolds = u_folds * clamp(4. * getFolds(foldsUV1, foldsUV2), 0., 1.); + radialFolds = mix(radialFolds, vec2(0.), fade); + foldsPattern = radialFolds.x + radialFolds.y; + } else { + vec2 creasesResult = u_folds * clamp(getGrid(patternUV), 0., 1.); + foldsPattern = creasesResult.x * mix(1., .0, fade); + drops *= mix(1., creasesResult.y, u_folds); + } + crumples = mix(crumples, 0., fade); drops = mix(drops, 0., fade); fiber *= mix(1., .5, fade); @@ -318,7 +322,7 @@ void main() { float pattern = roughness; pattern += fiber; pattern += crumples; - pattern += mix(radialFolds.x + radialFolds.y, creases, u_foldType); + pattern += foldsPattern; pattern += drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; From e22646912bf521ce17ca89a01d1ee9c15e02bd34 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 19 Jan 2026 00:00:57 +0400 Subject: [PATCH 044/186] contrast removed + tweaks --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 - packages/shaders-react/src/shaders/paper-texture.tsx | 7 ------- packages/shaders/src/shaders/paper-texture.ts | 8 ++------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index db32393b6..81572760a 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -74,7 +74,6 @@ const PaperTextureWithControls = () => { speed: { value: defaults.speed, min: 0, max: 3, order: 0 }, colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - contrast: { value: defaults.contrast, min: 0, max: 1, order: 197 }, blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 199 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e22a8b622..a30ab85b8 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,7 +38,6 @@ export const defaultPreset: PaperTexturePreset = { frame: 0, colorFront: '#c4cbd4', colorBack: '#ffffff', - contrast: 0.3, roughness: 0.4, roughnessSize: 0.5, fiber: 0.2, @@ -61,7 +60,6 @@ export const fiberPreset: PaperTexturePreset = { ...defaultObjectSizing, colorBack: "#eaffe0", colorFront: "#c19ac6", - contrast: 0.05, blending: 0.4, distortion: 0.4, seed: 180, @@ -96,7 +94,6 @@ export const cardboardPreset: PaperTexturePreset = { scale: 0.8, colorBack: "#dcc593", colorFront: "#685327", - contrast: 0.27, roughness: 0.1, roughnessSize: 0.50, fiber: 0.35, @@ -119,7 +116,6 @@ export const spreadPreset: PaperTexturePreset = { ...defaultObjectSizing, colorBack: "#ffffff", colorFront: "#c0c4c0", - contrast: 0.47, blending: 0.36, distortion: 0.50, seed: 448, @@ -149,7 +145,6 @@ export const flatPreset: PaperTexturePreset = { ...defaultObjectSizing, colorBack: '#fff3e0', colorFront: '#9c9c9c', - contrast: 0.79, blending: 0.19, distortion: 0.33, seed: 415, @@ -187,7 +182,6 @@ export const PaperTexture: React.FC = memo(function PaperText colorFront = defaultPreset.params.colorFront, colorBack = defaultPreset.params.colorBack, image = '', - contrast = defaultPreset.params.contrast, roughness = defaultPreset.params.roughness, roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, @@ -229,7 +223,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_image: image, u_colorFront: getShaderColorFromString(colorFront), u_colorBack: getShaderColorFromString(colorBack), - u_contrast: contrast, u_roughness: roughness, u_roughnessSize: roughnessSize, u_fiber: fiber, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 753adfa7c..7ee17c75f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -13,7 +13,6 @@ import { rotation2, declarePI } from '../shader-utils.js'; * - u_imageAspectRatio (float): Aspect ratio of the source image * - u_colorFront (vec4): Foreground color in RGBA * - u_colorBack (vec4): Background color in RGBA - * - u_contrast (float): Blending behavior, sharper vs smoother color transitions (0 to 1) * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) * - u_roughnessSize (float): Scale of the roughness pattern (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) @@ -61,7 +60,6 @@ uniform vec4 u_colorBack; uniform sampler2D u_image; uniform float u_imageAspectRatio; -uniform float u_contrast; uniform float u_roughnessSize; uniform float u_roughness; uniform float u_fiber; @@ -305,7 +303,7 @@ void main() { if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); - vec2 radialFolds = u_folds * clamp(4. * getFolds(foldsUV1, foldsUV2), 0., 1.); + vec2 radialFolds = u_folds * clamp(3. * getFolds(foldsUV1, foldsUV2), 0., 1.); radialFolds = mix(radialFolds, vec2(0.), fade); foldsPattern = radialFolds.x + radialFolds.y; } else { @@ -333,7 +331,7 @@ void main() { imageUV += .1 * u_distortion * pattern; float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); - image.rgb += .4 * pow(u_contrast, .4) * (.3 - pattern); + image.rgb += .4 * (.3 - pattern); frame *= image.a; vec3 color = fgColor * pattern; @@ -357,7 +355,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_noiseTexture?: HTMLImageElement; u_colorFront: [number, number, number, number]; u_colorBack: [number, number, number, number]; - u_contrast: number; u_roughness: number; u_roughnessSize: number; u_fiber: number; @@ -379,7 +376,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara image?: HTMLImageElement | string; colorFront?: string; colorBack?: string; - contrast?: number; roughness?: number; roughnessSize?: number; fiber?: number; From c5a171c1f632ee32246be6c782f10ed7a3ff3dfa Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 19 Jan 2026 00:07:15 +0400 Subject: [PATCH 045/186] tweaks --- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 7ee17c75f..708280f98 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -286,10 +286,10 @@ void main() { patternUV *= 5. * vec2(u_imageAspectRatio, 1.); vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; - vec2 fiberUV = mix(25., 8., u_fiberSize) * patternUV; + vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); float roughness = u_roughness * pow(1.3 * clamp(rf.x + .5, 0., 1.), 3.); - float fiber = u_fiber * clamp(rf.y - 1., 0., 1.); + float fiber = u_fiber * clamp(2. * pow(rf.y - 1., 4.), 0., 1.); vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; float crumples = u_crumples * clamp(.2 + getCrumples(crumplesUV), 0., 1.); From 1f872554b8ef83d18ff99ed5c0253a4c1beff137 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 19 Jan 2026 17:31:28 +0400 Subject: [PATCH 046/186] saving image distortion options --- packages/shaders/src/shaders/paper-texture.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 708280f98..003e80071 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -322,16 +322,34 @@ void main() { pattern += crumples; pattern += foldsPattern; pattern += drops; + + float distortionPattern = .2 * roughness; + distortionPattern += .3 * fiber; + distortionPattern += crumples; + distortionPattern += (foldsPattern - .5); + distortionPattern += drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; - imageUV += .1 * u_distortion * pattern; +// float eps = .002; +// vec2 grad = vec2( +// distortionPattern - (roughness + fiber + crumples + foldsPattern + drops), +// distortionPattern + .5 * (roughness + fiber + crumples + foldsPattern + drops) +// ); +// imageUV += u_distortion * .08 * normalize(grad + .001) * distortionPattern; + +// imageUV.x += u_distortion * .03 * sin(distortionPattern * 10.); +// imageUV.y += u_distortion * .03 * cos(distortionPattern * 10.); + + vec2 dc = imageUV - .5; + float r2 = dot(dc, dc); + imageUV = .5 + dc * (1. - u_distortion * .3 * (distortionPattern - .5) * r2); + float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); - image.rgb += .4 * (.3 - pattern); frame *= image.a; vec3 color = fgColor * pattern; @@ -339,12 +357,10 @@ void main() { color += bgColor * (1. - opacity); opacity += bgOpacity * (1. - opacity); - opacity = mix(opacity, 1., frame); - -// float blendOpacity = (.5 + .5 * pattern); -// vec3 pic = blendMultiply(color, image.rgb + .2 * 1. - u_blending * pattern, .5 * pattern + .2 * u_blending); -// pic = mix(image.rgb, pic, .5 * u_blending); -// color = mix(color, pic, frame); + + vec3 pic = color; + pic = mix(image.rgb, pic, u_blending); + color = mix(color, pic, frame); fragColor = vec4(color, opacity); } From 7353c68ab8ae1e2867f2029d3749d138bd7ea03a Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 19 Jan 2026 18:05:36 +0400 Subject: [PATCH 047/186] saving blending (not final) --- packages/shaders/src/shaders/paper-texture.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 003e80071..5fe2c2750 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -272,11 +272,10 @@ vec2 getGrid(vec2 uv) { } vec3 blendMultiply(vec3 base, vec3 blend) { - return base*blend; + return base * blend; } - vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { - return (blendMultiply(base, blend) * opacity + base * (1.0 - opacity)); + return blendMultiply(base, blend) * opacity + base * (1. - opacity); } void main() { @@ -323,10 +322,10 @@ void main() { pattern += foldsPattern; pattern += drops; - float distortionPattern = .2 * roughness; - distortionPattern += .3 * fiber; + float distortionPattern = .3 * roughness; + distortionPattern += .4 * fiber; distortionPattern += crumples; - distortionPattern += (foldsPattern - .5); + distortionPattern -= foldsPattern; distortionPattern += drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; @@ -344,9 +343,9 @@ void main() { // imageUV.x += u_distortion * .03 * sin(distortionPattern * 10.); // imageUV.y += u_distortion * .03 * cos(distortionPattern * 10.); - vec2 dc = imageUV - .5; - float r2 = dot(dc, dc); - imageUV = .5 + dc * (1. - u_distortion * .3 * (distortionPattern - .5) * r2); + vec2 dc = imageUV - .5; + float r2 = dot(dc, dc); + imageUV = .5 + dc * (1. - u_distortion * .3 * (distortionPattern - .5) * r2); float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); @@ -357,9 +356,10 @@ void main() { color += bgColor * (1. - opacity); opacity += bgOpacity * (1. - opacity); + + float lum = dot(vec3(.2126, .7152, .0722), image.rgb); + vec3 pic = (1. + .3 * u_blending * (1. - lum)) * blendMultiply(image.rgb, pow(color, vec3(4. - 3. * lum)), u_blending); - vec3 pic = color; - pic = mix(image.rgb, pic, u_blending); color = mix(color, pic, frame); fragColor = vec4(color, opacity); From 904ff3a9d78dda6b80ee7b9c6e57bdb61b2c16cb Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 15:44:12 +0400 Subject: [PATCH 048/186] tweaks --- .../src/shaders/paper-texture.tsx | 16 ++-- packages/shaders/src/shaders/paper-texture.ts | 83 ++++++++++--------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index a30ab85b8..918ed776c 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -30,7 +30,7 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - blending: 0, + blending: 0.7, distortion: 0.5, fit: 'contain', scale: 0.8, @@ -44,13 +44,13 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0.3, crumpleSize: 0.5, - folds: 0.4, + folds: 0.75, foldType: 'folds', foldCount: 11, foldsShape: 0.35, fade: 0, drops: 0.4, - seed: 285, + seed: 74, }, }; @@ -86,7 +86,7 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - blending: 0.5, + blending: 0.35, distortion: 0.5, fit: 'contain', speed: 1, @@ -117,7 +117,7 @@ export const spreadPreset: PaperTexturePreset = { colorBack: "#ffffff", colorFront: "#c0c4c0", blending: 0.36, - distortion: 0.50, + distortion: 0.7, seed: 448, roughness: 0.4, roughnessSize: 0.4, @@ -125,10 +125,10 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0.2, crumpleSize: 0.42, - folds: 0.7, + folds: 1, foldType: 'creases', foldCount: 1, - foldsShape: 0.15, + foldsShape: 0.2, drops: 0, fade: 0.68, scale: 0.8, @@ -145,7 +145,7 @@ export const flatPreset: PaperTexturePreset = { ...defaultObjectSizing, colorBack: '#fff3e0', colorFront: '#9c9c9c', - blending: 0.19, + blending: 0.9, distortion: 0.33, seed: 415, roughness: 0.31, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5fe2c2750..43494f56f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -265,10 +265,9 @@ vec2 getGrid(vec2 uv) { float foldWidth = u_foldsShape; float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); float angle = sign(dx) * 1.1345 * foldAmount; - float creaseDark = mix(.9, 1., smoothstep(0., foldWidth * .5, abs(dx))); - float grid = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * creaseDark; - float dropsMask = 1. - .8 * step(0., dx); - return vec2(grid, dropsMask); + float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); + float grid = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., creaseDark); + return vec2(grid, creaseDark); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -284,68 +283,72 @@ void main() { vec2 patternUV = v_imageUV - .5; patternUV *= 5. * vec2(u_imageAspectRatio, 1.); + float pattern = 0.; + float distortionPatternRadial = 0.; + float distortionPatternLinear = 0.; + + float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); + fade = clamp(8. * fade * fade * fade, 0., 1.); + vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); - float roughness = u_roughness * pow(1.3 * clamp(rf.x + .5, 0., 1.), 3.); - float fiber = u_fiber * clamp(2. * pow(rf.y - 1., 4.), 0., 1.); - - vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; - float crumples = u_crumples * clamp(.2 + getCrumples(crumplesUV), 0., 1.); + float roughness = clamp(5. * rf.x, 0., 1.); + float fiber = clamp(2. * pow(rf.y - 1., 4.), 0., 1.); + fiber *= mix(1., .5, fade); + roughness *= mix(1., .5, fade); - float drops = u_drops * getDrops(patternUV * 2.); + fiber *= u_fiber; + pattern += fiber; + distortionPatternRadial += .06 * fiber; - float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); - fade = clamp(8. * fade * fade * fade, 0., 1.); + roughness *= u_roughness; + pattern += roughness; + distortionPatternRadial += .05 * roughness; + + vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; + float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); + crumples = mix(crumples, 0., fade); + + pattern += u_crumples * crumples; + distortionPatternLinear -= .08 * u_crumples * (crumples - .25); + + float drops = getDrops(patternUV * 2.); + drops = mix(drops, 0., fade); float foldsPattern = 0.; if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); - vec2 radialFolds = u_folds * clamp(3. * getFolds(foldsUV1, foldsUV2), 0., 1.); + vec2 radialFolds = clamp(3. * getFolds(foldsUV1, foldsUV2), 0., 1.); radialFolds = mix(radialFolds, vec2(0.), fade); foldsPattern = radialFolds.x + radialFolds.y; + + pattern += u_folds * foldsPattern; + distortionPatternRadial += .25 * u_folds * (foldsPattern - 1.); } else { - vec2 creasesResult = u_folds * clamp(getGrid(patternUV), 0., 1.); + vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); foldsPattern = creasesResult.x * mix(1., .0, fade); drops *= mix(1., creasesResult.y, u_folds); - } - crumples = mix(crumples, 0., fade); - drops = mix(drops, 0., fade); - fiber *= mix(1., .5, fade); - roughness *= mix(1., .5, fade); + pattern += u_folds * foldsPattern; + distortionPatternLinear += .03 * u_folds * (pow(creasesResult.y, .2) - .5); + } - float pattern = roughness; - pattern += fiber; - pattern += crumples; - pattern += foldsPattern; + drops *= u_drops; pattern += drops; - - float distortionPattern = .3 * roughness; - distortionPattern += .4 * fiber; - distortionPattern += crumples; - distortionPattern -= foldsPattern; - distortionPattern += drops; + distortionPatternLinear += .03 * drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; -// float eps = .002; -// vec2 grad = vec2( -// distortionPattern - (roughness + fiber + crumples + foldsPattern + drops), -// distortionPattern + .5 * (roughness + fiber + crumples + foldsPattern + drops) -// ); -// imageUV += u_distortion * .08 * normalize(grad + .001) * distortionPattern; - -// imageUV.x += u_distortion * .03 * sin(distortionPattern * 10.); -// imageUV.y += u_distortion * .03 * cos(distortionPattern * 10.); + imageUV.y += u_distortion * distortionPatternLinear; vec2 dc = imageUV - .5; float r2 = dot(dc, dc); - imageUV = .5 + dc * (1. - u_distortion * .3 * (distortionPattern - .5) * r2); + imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); @@ -358,7 +361,7 @@ void main() { opacity += bgOpacity * (1. - opacity); float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - vec3 pic = (1. + .3 * u_blending * (1. - lum)) * blendMultiply(image.rgb, pow(color, vec3(4. - 3. * lum)), u_blending); + vec3 pic = (1. + .6 * u_blending * (1. - lum)) * blendMultiply(image.rgb, pow(color, vec3(4. - 3. * lum)), u_blending); color = mix(color, pic, frame); From 07580765c3081dec397e6212d086e250b4e2798b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 15:50:58 +0400 Subject: [PATCH 049/186] simplification --- packages/shaders/src/shaders/paper-texture.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 43494f56f..db6c147e5 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -7,8 +7,6 @@ import { rotation2, declarePI } from '../shader-utils.js'; * Can be used as an image filter or as a standalone texture. * * Fragment shader uniforms: - * - u_resolution (vec2): Canvas resolution in pixels - * - u_pixelRatio (float): Device pixel ratio * - u_image (sampler2D): Optional source image texture * - u_imageAspectRatio (float): Aspect ratio of the source image * - u_colorFront (vec4): Foreground color in RGBA @@ -51,9 +49,6 @@ import { rotation2, declarePI } from '../shader-utils.js'; export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; -uniform vec2 u_resolution; -uniform float u_pixelRatio; - uniform vec4 u_colorFront; uniform vec4 u_colorBack; @@ -139,7 +134,7 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { float c = 0.7648; // cos(0.7) float s = 0.6442; // sin(0.7) float rc = 1., rs = 0.; // accumulated rotation - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 4; i++) { if (i < 3) { vec2 ipR = floor(pR); vec2 fpR = fract(pR); @@ -294,7 +289,7 @@ void main() { vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); float roughness = clamp(5. * rf.x, 0., 1.); - float fiber = clamp(2. * pow(rf.y - 1., 4.), 0., 1.); + float fiber = (rf.y - 1.); fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); From f6b85413f47301c8f6f7b9d93570e172c697894e Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 16:15:19 +0400 Subject: [PATCH 050/186] crumples second layer removed --- packages/shaders/src/shaders/paper-texture.ts | 40 ++++++------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index db6c147e5..aa61a81c4 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -176,41 +176,27 @@ vec2 randomGB(vec2 p) { } float getCrumples(vec2 uv) { - vec2 t1 = uv * .25; - vec2 t2 = uv * .5; - vec2 p1 = floor(t1), p2 = floor(t2); - vec2 wsum1 = vec2(0.), cl1 = vec2(0.); - vec2 wsum2 = vec2(0.), cl2 = vec2(0.); + vec2 t = uv * .5; + vec2 p = floor(t); + vec2 wsum = vec2(0.), cl = vec2(0.); for (int y = -1; y < 2; y += 1) { for (int x = -1; x < 2; x += 1) { vec2 b = vec2(float(x), float(y)); - // Scale 1 - vec2 q1 = b + p1; - vec2 q1m = mod(q1, 8.); - vec2 c1 = q1 + randomGB(q1m + u_seed); - float val1 = .5 + .5 * sin((q1m.x + q1m.y * 5.) * 8.); - vec2 r1 = c1 - t1; - float wy1 = pow(sst(0., 1., 1. - abs(r1.y)), 16.); - vec2 w1 = pow(smoothstep(0., 1., 1. - abs(vec2(r1.x, r1.x - .0125))), vec2(16.)) * wy1; - cl1 += val1 * w1; wsum1 += w1; - // Scale 2 - vec2 q2 = b + p2; - vec2 q2m = mod(q2, 8.); - vec2 c2 = q2 + randomGB(q2m); - float val2 = .5 + .5 * sin((q2m.x + q2m.y * 5.) * 8.); - vec2 r2 = c2 - t2; - float sy2 = sst(0., 1., 1. - abs(r2.y)); - float wy2 = sy2 * sy2; - vec2 sx2 = smoothstep(0., 1., 1. - abs(vec2(r2.x, r2.x - .025))); - vec2 w2 = sx2 * sx2 * wy2; - cl2 += val2 * w2; wsum2 += w2; + vec2 q = b + p; + vec2 qm = mod(q, 8.); + vec2 c = q + randomGB(qm + u_seed); + float val = .5 + .5 * sin((qm.x + qm.y * 5.) * 8.); + vec2 r = c - t; + float wy = pow(sst(0., 1., 1. - abs(r.y)), 16.); + vec2 w = pow(smoothstep(0., 1., 1. - abs(vec2(r.x, r.x - .0125))), vec2(16.)) * wy; + cl += val * w; + wsum += w; } } - vec2 n = (cl1 / wsum1) * (cl2 / wsum2); + vec2 n = cl / wsum; return 4. * (n.y - n.x); } - float getDrops(vec2 uv) { vec2 iDropsUV = floor(uv); vec2 fDropsUV = fract(uv); From e827da8e7c49ea4346628129ee3dc7eb6c14f8ae Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 16:32:14 +0400 Subject: [PATCH 051/186] frame simplification --- packages/shaders/src/shaders/paper-texture.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index aa61a81c4..0ba6a6944 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -77,18 +77,13 @@ in vec2 v_imageUV; out vec4 fragColor; float getUvFrame(vec2 uv) { - float aax = 2. * fwidth(uv.x); - float aay = 2. * fwidth(uv.y); - - float left = smoothstep(0., aax, uv.x); - float right = 1. - smoothstep(1. - aax, 1., uv.x); - float bottom = smoothstep(0., aay, uv.y); - float top = 1. - smoothstep(1. - aay, 1., uv.y); - + float left = step(0., uv.x); + float right = 1. - step(1., uv.x); + float bottom = step(0., uv.y); + float top = 1. - step(1., uv.y); return left * right * bottom * top; } - float lst(float edge0, float edge1, float x) { return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } @@ -279,11 +274,11 @@ void main() { fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); - fiber *= u_fiber; + fiber *= (.05 + u_fiber); pattern += fiber; distortionPatternRadial += .06 * fiber; - roughness *= u_roughness; + roughness *= (.05 + u_roughness); pattern += roughness; distortionPatternRadial += .05 * roughness; From bf7dc6a31cc6f096204e80058fd6b9102d05d3f9 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 16:41:59 +0400 Subject: [PATCH 052/186] small optimisations --- packages/shaders/src/shaders/paper-texture.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0ba6a6944..6e5616e2a 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -111,7 +111,7 @@ float valueNoise(vec2 st) { } float getFadeMask(vec2 n) { float total = 0.0, amplitude = .4; - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 2; i++) { total += valueNoise(n) * amplitude; n *= 1.99; amplitude *= 0.65; @@ -182,8 +182,11 @@ float getCrumples(vec2 uv) { vec2 c = q + randomGB(qm + u_seed); float val = .5 + .5 * sin((qm.x + qm.y * 5.) * 8.); vec2 r = c - t; - float wy = pow(sst(0., 1., 1. - abs(r.y)), 16.); - vec2 w = pow(smoothstep(0., 1., 1. - abs(vec2(r.x, r.x - .0125))), vec2(16.)) * wy; + float wy = sst(0., 1., 1. - abs(r.y)); + wy *= wy; wy *= wy; wy *= wy; wy *= wy; // pow16 + vec2 w = smoothstep(0., 1., 1. - abs(vec2(r.x, r.x - .0125))); + w *= w; w *= w; w *= w; w *= w; // pow16 + w *= wy; cl += val * w; wsum += w; } @@ -206,7 +209,7 @@ float getDrops(vec2 uv) { dropsMinDist = min(dropsMinDist, dropsMinDist*dist); } } - return 1. - lst(.05, .09, pow(dropsMinDist, .5)); + return 1. - lst(.05, .09, sqrt(dropsMinDist)); } vec2 getFolds(vec2 uv1, vec2 uv2) { @@ -263,7 +266,7 @@ void main() { float distortionPatternRadial = 0.; float distortionPatternLinear = 0.; - float fade = u_fade * getFadeMask(.17 * patternUV + 10. * u_seed); + float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; @@ -341,7 +344,7 @@ void main() { color = mix(color, pic, frame); - fragColor = vec4(color, opacity); + fragColor = vec4(vec3(fade), opacity); } `; From 77d54da321802cdfe7547688f1d247dc35074706 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 16:42:33 +0400 Subject: [PATCH 053/186] output back --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6e5616e2a..6154b5cfc 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -344,7 +344,7 @@ void main() { color = mix(color, pic, frame); - fragColor = vec4(vec3(fade), opacity); + fragColor = vec4(color, opacity); } `; From 52e39785b87afd74f7d72e3c60e10e087eb14833 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 16:55:33 +0400 Subject: [PATCH 054/186] presets tweaks --- .../src/shaders/paper-texture.tsx | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 918ed776c..ff706c326 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -54,34 +54,6 @@ export const defaultPreset: PaperTexturePreset = { }, }; -export const fiberPreset: PaperTexturePreset = { - name: 'Fiber', - params: { - ...defaultObjectSizing, - colorBack: "#eaffe0", - colorFront: "#c19ac6", - blending: 0.4, - distortion: 0.4, - seed: 180, - roughness: 0.63, - roughnessSize: 0.50, - fiber: 0.76, - fiberSize: 0.61, - crumples: 0, - crumpleSize: 0.30, - folds: 0, - foldType: 'folds', - foldCount: 3, - foldsShape: 0.50, - drops: 1, - fade: 1, - scale: 0.80, - fit: "contain", - speed: 1, - frame: 0 - }, -}; - export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { @@ -121,7 +93,7 @@ export const spreadPreset: PaperTexturePreset = { seed: 448, roughness: 0.4, roughnessSize: 0.4, - fiber: 0.24, + fiber: 0.1, fiberSize: 0.5, crumples: 0.2, crumpleSize: 0.42, @@ -149,7 +121,7 @@ export const flatPreset: PaperTexturePreset = { distortion: 0.33, seed: 415, roughness: 0.31, - roughnessSize: 1.0, + roughnessSize: .3, fiber: 0, fiberSize: 0.61, crumples: 0.7, @@ -171,7 +143,6 @@ export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, spreadPreset, cardboardPreset, - fiberPreset, flatPreset ] as const; From a89fb08790d02e1387e846d29ee7025e70fc802a Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 17:10:52 +0400 Subject: [PATCH 055/186] clamping fiber --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6154b5cfc..ee838a442 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -273,7 +273,7 @@ void main() { vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); float roughness = clamp(5. * rf.x, 0., 1.); - float fiber = (rf.y - 1.); + float fiber = clamp(rf.y - 1., -1., 1.); fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); From 5b1640a994e879bdda300320a9277f8e39c014d7 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 17:15:44 +0400 Subject: [PATCH 056/186] folds tweak --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index ee838a442..8f15096bb 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -304,7 +304,7 @@ void main() { foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; - distortionPatternRadial += .25 * u_folds * (foldsPattern - 1.); + distortionPatternRadial += .2 * u_folds * (foldsPattern - 1.); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); foldsPattern = creasesResult.x * mix(1., .0, fade); From fc40482d60d25bf01bee03db1c9118ec8115ae98 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 17:31:27 +0400 Subject: [PATCH 057/186] removing performance profiling --- packages/shaders/src/shader-mount.ts | 207 --------------------------- 1 file changed, 207 deletions(-) diff --git a/packages/shaders/src/shader-mount.ts b/packages/shaders/src/shader-mount.ts index 2fbf1bd69..b327d682b 100644 --- a/packages/shaders/src/shader-mount.ts +++ b/packages/shaders/src/shader-mount.ts @@ -2,23 +2,6 @@ import { vertexShaderSource } from './vertex-shader.js'; const DEFAULT_MAX_PIXEL_COUNT: number = 1920 * 1080 * 4; -// Type definition for the WebGL timer query extension -interface EXT_disjoint_timer_query_webgl2 { - readonly TIME_ELAPSED_EXT: 0x88bf; - readonly GPU_DISJOINT_EXT: 0x8fbb; -} - -// Interface for performance statistics -interface PerformanceStats { - avgFrameTime: number; - avgGPUTime: number; - fps: number; - resolution: { width: number; height: number }; - renderScale: number; - pixelCount: number; - supportsTimerQueries: boolean; -} - export class ShaderMount { public parentElement: PaperShaderElement; public canvasElement: HTMLCanvasElement; @@ -53,14 +36,6 @@ export class ShaderMount { private uniformCache: Record = {}; private textureUnitMap: Map = new Map(); - // Performance measurement properties - private timerExt: EXT_disjoint_timer_query_webgl2 | null = null; - private performanceQueries: WebGLQuery[] = []; - private performanceResults: number[] = []; - private frameTimeHistory: number[] = []; - private lastPerformanceLog = 0; - private performanceLoggingEnabled = true; - constructor( /** The div you'd like to mount the shader to. The shader will match its size. */ parentElement: HTMLElement, @@ -118,9 +93,6 @@ export class ShaderMount { } this.gl = gl; - // Initialize performance measurement - this.initPerformanceMeasurement(); - this.initProgram(); this.setupPositionAttribute(); // Grab the locations of the uniforms in the fragment shader @@ -145,163 +117,6 @@ export class ShaderMount { document.addEventListener('visibilitychange', this.handleDocumentVisibilityChange); } - private initPerformanceMeasurement = (): void => { - // Try to get timer query extension - this.timerExt = this.gl.getExtension('EXT_disjoint_timer_query_webgl2'); - - if (!this.timerExt) { - console.warn('Timer queries not supported - using frame timing fallback'); - } else { - console.log('✓ GPU timer queries supported'); - } - }; - - // Enable/disable performance logging - public enablePerformanceLogging = (enabled: boolean = true): void => { - this.performanceLoggingEnabled = enabled; - if (enabled) { - console.log('🎮 Performance logging enabled'); - } - }; - - private measureGPUTime = (): WebGLQuery | null => { - if (!this.timerExt) { - return null; - } - - const query = this.gl.createQuery(); - if (!query) return null; - - this.gl.beginQuery(this.timerExt.TIME_ELAPSED_EXT, query); - return query; - }; - - private endGPUMeasurement = (query: WebGLQuery | null) => { - if (!query || !this.timerExt) return; - - this.gl.endQuery(this.timerExt.TIME_ELAPSED_EXT); - this.performanceQueries.push(query); - }; - - private checkPerformanceResults = () => { - if (!this.timerExt || this.performanceQueries.length === 0) return; - - // Check completed queries - const completedQueries: WebGLQuery[] = []; - - this.performanceQueries.forEach((query) => { - const available = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE); - const disjoint = this.gl.getParameter(this.timerExt!.GPU_DISJOINT_EXT); - - if (available && !disjoint) { - const timeElapsed = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT); - const timeInMs = timeElapsed / 1000000; // Convert to milliseconds - - this.performanceResults.push(timeInMs); - completedQueries.push(query); - - // Keep only last 100 results - if (this.performanceResults.length > 100) { - this.performanceResults.shift(); - } - } - }); - - // Remove completed queries - completedQueries.forEach((completedQuery) => { - const index = this.performanceQueries.indexOf(completedQuery); - if (index > -1) { - this.performanceQueries.splice(index, 1); - } - this.gl.deleteQuery(completedQuery); - }); - }; - - private logPerformanceStats = (frameTime: number) => { - if (!this.performanceLoggingEnabled) return; - - const now = performance.now(); - if (now - this.lastPerformanceLog < 2000) return; // Log every 2 seconds - - this.lastPerformanceLog = now; - - // Frame time stats - const avgFrameTime = this.frameTimeHistory.reduce((a, b) => a + b, 0) / this.frameTimeHistory.length; - const maxFrameTime = Math.max(...this.frameTimeHistory); - const minFrameTime = Math.min(...this.frameTimeHistory); - - console.group('🎮 Shader Performance Stats'); - console.log(`📊 Frame Time: ${frameTime.toFixed(2)}ms (avg: ${avgFrameTime.toFixed(2)}ms)`); - console.log( - `⚡ FPS: ${(1000 / avgFrameTime).toFixed(1)} (range: ${(1000 / maxFrameTime).toFixed(1)}-${(1000 / minFrameTime).toFixed(1)})` - ); - console.log( - `🖥️ Resolution: ${this.gl.canvas.width}x${this.gl.canvas.height} (${((this.gl.canvas.width * this.gl.canvas.height) / 1000000).toFixed(1)}MP)` - ); - console.log(`📏 Render Scale: ${this.renderScale.toFixed(2)}x`); - - // GPU timing stats (if available) - if (this.performanceResults.length > 0) { - const avgGPUTime = this.performanceResults.reduce((a, b) => a + b, 0) / this.performanceResults.length; - const maxGPUTime = Math.max(...this.performanceResults); - const minGPUTime = Math.min(...this.performanceResults); - - console.log( - `🎯 GPU Time: ${avgGPUTime.toFixed(2)}ms (range: ${minGPUTime.toFixed(2)}-${maxGPUTime.toFixed(2)}ms)` - ); - console.log(`⚙️ GPU Usage: ${((avgGPUTime / avgFrameTime) * 100).toFixed(1)}% of frame time`); - } else { - console.log('⚠️ GPU timing not available (timer queries not supported)'); - } - console.groupEnd(); - }; - - // Public method to get current performance stats - public getPerformanceStats = (): PerformanceStats => { - const avgFrameTime = - this.frameTimeHistory.length > 0 - ? this.frameTimeHistory.reduce((a, b) => a + b, 0) / this.frameTimeHistory.length - : 0; - - const avgGPUTime = - this.performanceResults.length > 0 - ? this.performanceResults.reduce((a, b) => a + b, 0) / this.performanceResults.length - : 0; - - return { - avgFrameTime, - avgGPUTime, - fps: avgFrameTime > 0 ? 1000 / avgFrameTime : 0, - resolution: { width: this.gl.canvas.width, height: this.gl.canvas.height }, - renderScale: this.renderScale, - pixelCount: this.gl.canvas.width * this.gl.canvas.height, - supportsTimerQueries: !!this.timerExt, - }; - }; - - // Method to stress test with multiple passes - public stressTest = (passes: number = 10): void => { - if (!this.program) return; - - console.log(`🔥 Starting stress test with ${passes} passes...`); - - const gpuQuery = this.measureGPUTime(); - const startTime = performance.now(); - - for (let i = 0; i < passes; i++) { - this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); - } - - this.endGPUMeasurement(gpuQuery); - - const endTime = performance.now(); - const totalTime = endTime - startTime; - - console.log( - `⏱️ Stress test completed: ${totalTime.toFixed(2)}ms total, ${(totalTime / passes).toFixed(2)}ms per pass` - ); - }; - private initProgram = () => { const program = createProgram(this.gl, vertexShaderSource, this.fragmentShader); if (!program) return; @@ -461,13 +276,6 @@ export class ShaderMount { return; } - // Calculate frame timing - const frameTime = currentTime - this.lastRenderTime; - this.frameTimeHistory.push(frameTime); - if (this.frameTimeHistory.length > 60) { - this.frameTimeHistory.shift(); // Keep last 60 frames - } - // Calculate the delta time const dt = currentTime - this.lastRenderTime; this.lastRenderTime = currentTime; @@ -492,17 +300,8 @@ export class ShaderMount { this.resolutionChanged = false; } - // === PERFORMANCE MEASUREMENT START === - const gpuQuery = this.measureGPUTime(); - - // Your main draw call - this is what we're measuring this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); - this.endGPUMeasurement(gpuQuery); - this.checkPerformanceResults(); - this.logPerformanceStats(frameTime); - // === PERFORMANCE MEASUREMENT END === - // Loop if we're animating if (this.currentSpeed !== 0) { this.requestRender(); @@ -743,12 +542,6 @@ export class ShaderMount { this.rafId = null; } - // Clean up performance queries - this.performanceQueries.forEach((query) => { - this.gl.deleteQuery(query); - }); - this.performanceQueries = []; - if (this.gl && this.program) { // Clean up all textures this.textures.forEach((texture) => { From 2f6fbb57a5f28967c7e4dfaa9e4eeb817ba985f3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 17:38:59 +0400 Subject: [PATCH 058/186] tweaks --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- packages/shaders/src/shaders/paper-texture.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index ff706c326..9f44dd8d5 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -47,7 +47,7 @@ export const defaultPreset: PaperTexturePreset = { folds: 0.75, foldType: 'folds', foldCount: 11, - foldsShape: 0.35, + foldsShape: 0.75, fade: 0, drops: 0.4, seed: 74, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 8f15096bb..8cff8cd11 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -299,7 +299,7 @@ void main() { if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); - vec2 radialFolds = clamp(3. * getFolds(foldsUV1, foldsUV2), 0., 1.); + vec2 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); radialFolds = mix(radialFolds, vec2(0.), fade); foldsPattern = radialFolds.x + radialFolds.y; From 3053fad3b8ed4887dad8d609399ad0a53693d3d5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 21 Jan 2026 23:33:40 +0400 Subject: [PATCH 059/186] distortion to -1..1 --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 2ff55a442..4124211a2 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -74,7 +74,7 @@ const PaperTextureWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, - distortion: { value: defaults.distortion, min: 0, max: 1, order: 199 }, + distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, From 3d28192d0311c2651bcec58bd05b67a063286a22 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 21:34:14 +0400 Subject: [PATCH 060/186] scd folds line w/ distortion --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 8cff8cd11..18a67291d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -298,7 +298,7 @@ void main() { float foldsPattern = 0.; if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = rotate(foldsUV1 + .009 * cos(u_seed), .012 * sin(u_seed)); + vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); vec2 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); radialFolds = mix(radialFolds, vec2(0.), fade); foldsPattern = radialFolds.x + radialFolds.y; From 94bedcd445726113a7eefbbb026594ac05d8c560 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 21:36:58 +0400 Subject: [PATCH 061/186] soft image frame edges --- packages/shaders/src/shaders/paper-texture.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 18a67291d..e4ba41190 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -77,10 +77,11 @@ in vec2 v_imageUV; out vec4 fragColor; float getUvFrame(vec2 uv) { - float left = step(0., uv.x); - float right = 1. - step(1., uv.x); - float bottom = step(0., uv.y); - float top = 1. - step(1., uv.y); + float aa = .01 * u_distortion; + float left = smoothstep(0., aa, uv.x); + float right = 1. - smoothstep(1. - aa, 1., uv.x); + float bottom = smoothstep(0., aa, uv.y); + float top = 1. - smoothstep(1. - aa, 1., uv.y); return left * right * bottom * top; } @@ -329,7 +330,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); - float frame = getUvFrame(imageUV); + float frame = getUvFrame(imageUV, u_distortion); vec4 image = texture(u_image, imageUV); frame *= image.a; From adb2ecadb79f385a435add546888134b2cdf3aeb Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 21:37:33 +0400 Subject: [PATCH 062/186] soft image frame edges: fix --- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index e4ba41190..9b94f0add 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -76,8 +76,8 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; -float getUvFrame(vec2 uv) { - float aa = .01 * u_distortion; +float getUvFrame(vec2 uv, float blur) { + float aa = .01 * blur; float left = smoothstep(0., aa, uv.x); float right = 1. - smoothstep(1. - aa, 1., uv.x); float bottom = smoothstep(0., aa, uv.y); From 162e1855f66fbec3cb639c4d5467f2369610046b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 21:39:46 +0400 Subject: [PATCH 063/186] soft image frame edges: fix #2 --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9b94f0add..57219147d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -330,7 +330,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); - float frame = getUvFrame(imageUV, u_distortion); + float frame = getUvFrame(imageUV, abs(u_distortion)); vec4 image = texture(u_image, imageUV); frame *= image.a; From 122112b8dd93bb873a41f3206a74141fe3de66c1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 21:47:43 +0400 Subject: [PATCH 064/186] soft image frame edges: another amount calculation --- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 57219147d..7e7c3b2bd 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -77,7 +77,7 @@ in vec2 v_imageUV; out vec4 fragColor; float getUvFrame(vec2 uv, float blur) { - float aa = .01 * blur; + float aa = .003 * blur; float left = smoothstep(0., aa, uv.x); float right = 1. - smoothstep(1. - aa, 1., uv.x); float bottom = smoothstep(0., aa, uv.y); @@ -330,7 +330,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); - float frame = getUvFrame(imageUV, abs(u_distortion)); + float frame = getUvFrame(imageUV, .02 + .03 * abs(u_distortion) + .1 * u_fiber + .1 * u_roughness + .3 * u_crumples); vec4 image = texture(u_image, imageUV); frame *= image.a; From b7e566fc2eef9651c9e84d4d848b32a954fd9d4f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 22:17:03 +0400 Subject: [PATCH 065/186] normalising foldCount --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- packages/shaders-react/src/shaders/paper-texture.tsx | 6 +++--- packages/shaders/src/shaders/paper-texture.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 4124211a2..25b4c2673 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -84,7 +84,7 @@ const PaperTextureWithControls = () => { crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, - foldCount: { value: defaults.foldCount, min: 1, max: 20, step: 1, order: 232 }, + foldCount: { value: defaults.foldCount, min: 0, max: 1, order: 232 }, foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 9f44dd8d5..ef2b36e18 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -46,7 +46,7 @@ export const defaultPreset: PaperTexturePreset = { crumpleSize: 0.5, folds: 0.75, foldType: 'folds', - foldCount: 11, + foldCount: 0.7, foldsShape: 0.75, fade: 0, drops: 0.4, @@ -74,7 +74,7 @@ export const cardboardPreset: PaperTexturePreset = { crumpleSize: 0.1, folds: 0.25, foldType: 'creases', - foldCount: 20, + foldCount: 0.4, foldsShape: 0.28, fade: 0, drops: 0.1, @@ -128,7 +128,7 @@ export const flatPreset: PaperTexturePreset = { crumpleSize: 0.2, folds: 0, foldType: 'folds', - foldCount: 6, + foldCount: 0.5, foldsShape: 0.30, drops: 0.20, fade: 0.75, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 7e7c3b2bd..a49faa233 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -19,7 +19,7 @@ import { rotation2, declarePI } from '../shader-utils.js'; * - u_crumpleSize (float): Cell-based crumple pattern scale (0 to 1) * - u_folds (float): Depth of the folds (0 to 1) * - u_foldType (float): Type of folds pattern (0 = radial folds, 1 = creases) - * - u_foldCount (float): Number of folds or creases (1 to 20) + * - u_foldCount (float): Size/frequency of folds or creases (0 to 1) * - u_foldsShape (float): Shape/width of the folds (0 to 1) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds, crumples and dots (0 to 1000) @@ -216,8 +216,8 @@ float getDrops(vec2 uv) { vec2 getFolds(vec2 uv1, vec2 uv2) { vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; - for (int i = 0; i < 15; i++) { - if (float(i) >= u_foldCount) break; + for (int i = 0; i < 16; i++) { + if (float(i) >= max(1., u_foldCount * 16.)) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; @@ -240,7 +240,7 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { } vec2 getGrid(vec2 uv) { - float gridX = fract(uv.x * .1 * u_foldCount + .5); + float gridX = fract(uv.x * mix(4., .01, u_foldCount) + .5); float dx = gridX - .5; float foldWidth = u_foldsShape; float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); From acac5a2414d10c8957d6013aa095cb661be2be4f Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 22:20:36 +0400 Subject: [PATCH 066/186] reversed foldCount in creases mode --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index a49faa233..68b842190 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -240,7 +240,7 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { } vec2 getGrid(vec2 uv) { - float gridX = fract(uv.x * mix(4., .01, u_foldCount) + .5); + float gridX = fract(uv.x * mix(.01, 4., u_foldCount) + .5); float dx = gridX - .5; float foldWidth = u_foldsShape; float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); From 812e38263abc487b9b7c7d59bf18c2f7ae6c5fae Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 22:25:24 +0400 Subject: [PATCH 067/186] tweaking edge blur amount --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 68b842190..1b3cd94d8 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -330,7 +330,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); - float frame = getUvFrame(imageUV, .02 + .03 * abs(u_distortion) + .1 * u_fiber + .1 * u_roughness + .3 * u_crumples); + float frame = getUvFrame(imageUV, .01 + .03 * abs(u_distortion) + .05 * u_fiber + .05 * u_roughness + .05 * u_crumples); vec4 image = texture(u_image, imageUV); frame *= image.a; From ada1e1633217477579e0b8797e9d754c517982f0 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 22 Jan 2026 22:26:38 +0400 Subject: [PATCH 068/186] crumples to almost not affect the distortion --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 1b3cd94d8..e03278e84 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -291,7 +291,7 @@ void main() { crumples = mix(crumples, 0., fade); pattern += u_crumples * crumples; - distortionPatternLinear -= .08 * u_crumples * (crumples - .25); + distortionPatternLinear -= .01 * u_crumples * (crumples - .25); float drops = getDrops(patternUV * 2.); drops = mix(drops, 0., fade); From ca4bed6f015b36e88039740f1a36ff959473e7d5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 25 Jan 2026 14:02:17 +0100 Subject: [PATCH 069/186] distortion polishing --- packages/shaders/src/shaders/paper-texture.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index e03278e84..c2eb6f292 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -213,7 +213,7 @@ float getDrops(vec2 uv) { return 1. - lst(.05, .09, sqrt(dropsMinDist)); } -vec2 getFolds(vec2 uv1, vec2 uv2) { +vec3 getFolds(vec2 uv1, vec2 uv2) { vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; for (int i = 0; i < 16; i++) { @@ -233,9 +233,10 @@ vec2 getFolds(vec2 uv1, vec2 uv2) { } } float mult2 = mix(.22, .02, u_foldsShape); - return vec2( + return vec3( mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), - mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)) + mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)), + .2 * pp2.y ); } @@ -280,11 +281,11 @@ void main() { fiber *= (.05 + u_fiber); pattern += fiber; - distortionPatternRadial += .06 * fiber; + distortionPatternRadial += .04 * fiber; roughness *= (.05 + u_roughness); pattern += roughness; - distortionPatternRadial += .05 * roughness; + distortionPatternRadial += .04 * roughness; vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); @@ -300,12 +301,13 @@ void main() { if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); - vec2 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); - radialFolds = mix(radialFolds, vec2(0.), fade); + vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); + radialFolds = mix(radialFolds, vec3(0.), fade); foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; - distortionPatternRadial += .2 * u_folds * (foldsPattern - 1.); + distortionPatternRadial += .04 * u_folds * radialFolds.z; + distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); foldsPattern = creasesResult.x * mix(1., .0, fade); @@ -330,7 +332,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); - float frame = getUvFrame(imageUV, .01 + .03 * abs(u_distortion) + .05 * u_fiber + .05 * u_roughness + .05 * u_crumples); + float frame = getUvFrame(imageUV, .01 + .04 * abs(u_distortion) + .05 * u_fiber + .05 * u_roughness + .05 * u_crumples); vec4 image = texture(u_image, imageUV); frame *= image.a; From b85f1043ef64ea20b08be255be5a0a02fc091c3c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 14:42:12 +0100 Subject: [PATCH 070/186] test param for folds distortion curviness --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + docs/src/shader-defs/paper-texture-def.ts | 8 ++++++++ packages/shaders-react/src/shaders/paper-texture.tsx | 10 ++++++++-- packages/shaders/src/shaders/paper-texture.ts | 5 ++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 25b4c2673..dc08c3340 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -88,6 +88,7 @@ const PaperTextureWithControls = () => { foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, + test: { value: defaults.test, min: 0, max: 2, order: 0 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 36dfec274..cc613c5d5 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -28,6 +28,14 @@ export const paperTextureDef: ShaderDef = { isColor: true, description: 'Foreground color', }, + { + name: 'test', + type: 'number', + defaultValue: defaultParams.test, + min: 0, + max: 2, + description: 'Test value for adjustments', + }, ...staticImageCommonParams, ], }; diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index ef2b36e18..36f424958 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -51,6 +51,7 @@ export const defaultPreset: PaperTexturePreset = { fade: 0, drops: 0.4, seed: 74, + test: 1, }, }; @@ -79,6 +80,7 @@ export const cardboardPreset: PaperTexturePreset = { fade: 0, drops: 0.1, seed: 1.6, + test: 1, }, }; @@ -106,7 +108,8 @@ export const spreadPreset: PaperTexturePreset = { scale: 0.8, fit: "contain", speed: 1, - frame: 0 + frame: 0, + test: 1, }, }; @@ -135,7 +138,8 @@ export const flatPreset: PaperTexturePreset = { scale: 0.8, fit: 'contain', speed: 1, - frame: 0 + frame: 0, + test: 1, }, }; @@ -164,6 +168,7 @@ export const PaperTexture: React.FC = memo(function PaperText seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, distortion = defaultPreset.params.distortion, + test = defaultPreset.params.test, // Reworked props fiberScale, @@ -209,6 +214,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_seed: seed, u_blending: blending, u_distortion: distortion, + u_test: test, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index c2eb6f292..b1ce53752 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -70,6 +70,7 @@ uniform float u_seed; uniform float u_fade; uniform float u_blending; uniform float u_distortion; +uniform float u_test; uniform sampler2D u_noiseTexture; @@ -306,7 +307,7 @@ void main() { foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; - distortionPatternRadial += .04 * u_folds * radialFolds.z; + distortionPatternRadial += .2 * u_test * u_folds * radialFolds.z; distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); @@ -371,6 +372,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_seed: number; u_blending: number; u_distortion: number; + u_test: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -392,6 +394,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara seed?: number; blending?: number; distortion?: number; + test?: number; } export type PaperTextureFoldType = 'folds' | 'creases'; From 435e173b2d3b1db697c160ab7a477f88b257fffe Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 15:16:32 +0100 Subject: [PATCH 071/186] edge softness fix prep --- packages/shaders/src/shaders/paper-texture.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b1ce53752..f414b8408 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -78,11 +78,10 @@ in vec2 v_imageUV; out vec4 fragColor; float getUvFrame(vec2 uv, float blur) { - float aa = .003 * blur; - float left = smoothstep(0., aa, uv.x); - float right = 1. - smoothstep(1. - aa, 1., uv.x); - float bottom = smoothstep(0., aa, uv.y); - float top = 1. - smoothstep(1. - aa, 1., uv.y); + float left = smoothstep(0., blur, uv.x); + float right = 1. - smoothstep(1. - blur, 1., uv.x); + float bottom = smoothstep(0., blur, uv.y); + float top = 1. - smoothstep(1. - blur, 1., uv.y); return left * right * bottom * top; } @@ -333,7 +332,8 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); - float frame = getUvFrame(imageUV, .01 + .04 * abs(u_distortion) + .05 * u_fiber + .05 * u_roughness + .05 * u_crumples); + float frameSoftness = .005 + .04 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; From 0a539376393fed1dae5c57a417813848a9dc0a86 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 15:42:35 +0100 Subject: [PATCH 072/186] edge distortion fixed --- packages/shaders/src/shaders/paper-texture.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index f414b8408..2bccd4287 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -283,7 +283,7 @@ void main() { pattern += fiber; distortionPatternRadial += .04 * fiber; - roughness *= (.05 + u_roughness); + roughness *= (.03 + u_roughness); pattern += roughness; distortionPatternRadial += .04 * roughness; @@ -330,9 +330,9 @@ void main() { vec2 dc = imageUV - .5; float r2 = dot(dc, dc); - imageUV = .5 + dc * (1. - u_distortion * distortionPatternRadial * r2); + imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .04 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .005 + .02 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; From 0f1a85d305b1ccf0cb8728b83442c47a2a66a56c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 15:44:32 +0100 Subject: [PATCH 073/186] edge distortion tweaking --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 2bccd4287..fffe31a08 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -332,7 +332,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .02 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .005 + .03 * (u_distortion * u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; From cb0df049d8e87acd0c2c105aedd021b37043decd Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 16:07:44 +0100 Subject: [PATCH 074/186] edge distortion tweaking --- packages/shaders/src/shaders/paper-texture.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index fffe31a08..2069e2b0b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -281,11 +281,11 @@ void main() { fiber *= (.05 + u_fiber); pattern += fiber; - distortionPatternRadial += .04 * fiber; + distortionPatternRadial += .02 * fiber; roughness *= (.03 + u_roughness); pattern += roughness; - distortionPatternRadial += .04 * roughness; + distortionPatternRadial += .02 * roughness; vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); @@ -332,7 +332,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .03 * (u_distortion * u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .005 + .02 * (u_distortion * u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; From 519f68a657967dbb283ab5a24cf277e7f4ed1dc5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 16:13:20 +0100 Subject: [PATCH 075/186] edge softness param added --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + docs/src/shader-defs/paper-texture-def.ts | 8 ++++++++ packages/shaders-react/src/shaders/paper-texture.tsx | 6 ++++++ packages/shaders/src/shaders/paper-texture.ts | 5 ++++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index dc08c3340..dfb5c4536 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -89,6 +89,7 @@ const PaperTextureWithControls = () => { drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, test: { value: defaults.test, min: 0, max: 2, order: 0 }, + test2: { value: defaults.test2, min: 0, max: 0.1, order: 0 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index cc613c5d5..154515a1c 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -36,6 +36,14 @@ export const paperTextureDef: ShaderDef = { max: 2, description: 'Test value for adjustments', }, + { + name: 'test2', + type: 'number', + defaultValue: defaultParams.test2, + min: 0, + max: 0.1, + description: 'Test2 value for adjustments', + }, ...staticImageCommonParams, ], }; diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 36f424958..adb98a887 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -52,6 +52,7 @@ export const defaultPreset: PaperTexturePreset = { drops: 0.4, seed: 74, test: 1, + test2: 0.2, }, }; @@ -81,6 +82,7 @@ export const cardboardPreset: PaperTexturePreset = { drops: 0.1, seed: 1.6, test: 1, + test2: 0.03, }, }; @@ -110,6 +112,7 @@ export const spreadPreset: PaperTexturePreset = { speed: 1, frame: 0, test: 1, + test2: 0.03, }, }; @@ -140,6 +143,7 @@ export const flatPreset: PaperTexturePreset = { speed: 1, frame: 0, test: 1, + test2: 0.03, }, }; @@ -169,6 +173,7 @@ export const PaperTexture: React.FC = memo(function PaperText blending = defaultPreset.params.blending, distortion = defaultPreset.params.distortion, test = defaultPreset.params.test, + test2 = defaultPreset.params.test2, // Reworked props fiberScale, @@ -215,6 +220,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_blending: blending, u_distortion: distortion, u_test: test, + u_test2: test2, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 2069e2b0b..9f6774cf9 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -71,6 +71,7 @@ uniform float u_fade; uniform float u_blending; uniform float u_distortion; uniform float u_test; +uniform float u_test2; uniform sampler2D u_noiseTexture; @@ -332,7 +333,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .02 * (u_distortion * u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .005 + .4 * u_test2 * (u_distortion * u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; @@ -373,6 +374,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_blending: number; u_distortion: number; u_test: number; + u_test2: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -395,6 +397,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara blending?: number; distortion?: number; test?: number; + test2?: number; } export type PaperTextureFoldType = 'folds' | 'creases'; From aa297b61dd75ecad70e1975980b01adb32392c95 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 16:13:51 +0100 Subject: [PATCH 076/186] edge softness tweaking --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9f6774cf9..5d0c7dbc5 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -333,7 +333,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .4 * u_test2 * (u_distortion * u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .005 + .4 * u_test2 * pow(u_distortion, 1.4) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; From a307d2c33573cd02f0779ae5c2c3118c287b9d40 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 16:14:19 +0100 Subject: [PATCH 077/186] edge softness tweaking --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5d0c7dbc5..2c6ecf031 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -333,7 +333,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .4 * u_test2 * pow(u_distortion, 1.4) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .005 + .4 * u_test2 * pow(abs(u_distortion), 1.4) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; From cd053b96b00fac823c2a88be18595ec5048bbfad Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 16:22:24 +0100 Subject: [PATCH 078/186] pure blendMultiply --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 2c6ecf031..948d22e8b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -345,7 +345,7 @@ void main() { opacity += bgOpacity * (1. - opacity); float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - vec3 pic = (1. + .6 * u_blending * (1. - lum)) * blendMultiply(image.rgb, pow(color, vec3(4. - 3. * lum)), u_blending); + vec3 pic = blendMultiply(image.rgb, color, u_blending); color = mix(color, pic, frame); From 0064951d275cfd4edf5b1e6361ccc0e857499cd8 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 16:54:54 +0100 Subject: [PATCH 079/186] blending reworked --- packages/shaders/src/shaders/paper-texture.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 948d22e8b..28fbd6d3c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -344,8 +344,17 @@ void main() { color += bgColor * (1. - opacity); opacity += bgOpacity * (1. - opacity); - float lum = dot(vec3(.2126, .7152, .0722), image.rgb); + float maxC = max(max(image.r, image.g), image.b); + float minC = min(min(image.r, image.g), image.b); + float sat = maxC > 0. ? (maxC - minC) / maxC : 0.; + float midC = image.r + image.g + image.b - maxC - minC; + float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; + float satDampen = sat * (1. - .5 * secondaryness); + float darkDampen = 1. - dot(vec3(.2126, .7152, .0722), image.rgb); + float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); + vec3 pic = blendMultiply(image.rgb, color, u_blending); + pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); color = mix(color, pic, frame); From a120c6e7d75a8d69fe7d8211df18a6f0fd154411 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 17:03:57 +0100 Subject: [PATCH 080/186] tweaking test2 --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index dfb5c4536..cc33df1a8 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -89,7 +89,7 @@ const PaperTextureWithControls = () => { drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, test: { value: defaults.test, min: 0, max: 2, order: 0 }, - test2: { value: defaults.test2, min: 0, max: 0.1, order: 0 }, + test2: { value: defaults.test2, min: 0, max: 0.2, order: 0 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index adb98a887..22026ee5e 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -52,7 +52,7 @@ export const defaultPreset: PaperTexturePreset = { drops: 0.4, seed: 74, test: 1, - test2: 0.2, + test2: 0.05, }, }; From 34c4e1f5c4fda86db857c121639278f88085abcf Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 17:44:54 +0100 Subject: [PATCH 081/186] backing test & test2 --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 -- docs/src/shader-defs/paper-texture-def.ts | 16 ---------------- .../shaders-react/src/shaders/paper-texture.tsx | 12 ------------ packages/shaders/src/shaders/paper-texture.ts | 10 +--------- 4 files changed, 1 insertion(+), 39 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index cc33df1a8..25b4c2673 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -88,8 +88,6 @@ const PaperTextureWithControls = () => { foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, - test: { value: defaults.test, min: 0, max: 2, order: 0 }, - test2: { value: defaults.test2, min: 0, max: 0.2, order: 0 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 154515a1c..36dfec274 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -28,22 +28,6 @@ export const paperTextureDef: ShaderDef = { isColor: true, description: 'Foreground color', }, - { - name: 'test', - type: 'number', - defaultValue: defaultParams.test, - min: 0, - max: 2, - description: 'Test value for adjustments', - }, - { - name: 'test2', - type: 'number', - defaultValue: defaultParams.test2, - min: 0, - max: 0.1, - description: 'Test2 value for adjustments', - }, ...staticImageCommonParams, ], }; diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 22026ee5e..22a12a691 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -51,8 +51,6 @@ export const defaultPreset: PaperTexturePreset = { fade: 0, drops: 0.4, seed: 74, - test: 1, - test2: 0.05, }, }; @@ -81,8 +79,6 @@ export const cardboardPreset: PaperTexturePreset = { fade: 0, drops: 0.1, seed: 1.6, - test: 1, - test2: 0.03, }, }; @@ -111,8 +107,6 @@ export const spreadPreset: PaperTexturePreset = { fit: "contain", speed: 1, frame: 0, - test: 1, - test2: 0.03, }, }; @@ -142,8 +136,6 @@ export const flatPreset: PaperTexturePreset = { fit: 'contain', speed: 1, frame: 0, - test: 1, - test2: 0.03, }, }; @@ -172,8 +164,6 @@ export const PaperTexture: React.FC = memo(function PaperText seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, distortion = defaultPreset.params.distortion, - test = defaultPreset.params.test, - test2 = defaultPreset.params.test2, // Reworked props fiberScale, @@ -219,8 +209,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_seed: seed, u_blending: blending, u_distortion: distortion, - u_test: test, - u_test2: test2, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 28fbd6d3c..2083a1ace 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -70,9 +70,6 @@ uniform float u_seed; uniform float u_fade; uniform float u_blending; uniform float u_distortion; -uniform float u_test; -uniform float u_test2; - uniform sampler2D u_noiseTexture; in vec2 v_imageUV; @@ -307,7 +304,6 @@ void main() { foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; - distortionPatternRadial += .2 * u_test * u_folds * radialFolds.z; distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); @@ -333,7 +329,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); - float frameSoftness = .005 + .4 * u_test2 * pow(abs(u_distortion), 1.4) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; @@ -382,8 +378,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_seed: number; u_blending: number; u_distortion: number; - u_test: number; - u_test2: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -405,8 +399,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara seed?: number; blending?: number; distortion?: number; - test?: number; - test2?: number; } export type PaperTextureFoldType = 'folds' | 'creases'; From 44188ef743564be2855e8cf0fb7bd36c8a35ee74 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 17:50:43 +0100 Subject: [PATCH 082/186] 60% fade for folds --- packages/shaders/src/shaders/paper-texture.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 2083a1ace..29be35677 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -296,6 +296,7 @@ void main() { drops = mix(drops, 0., fade); float foldsPattern = 0.; + fade *= .6; if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); From e9c4e4d602e3350968b72a28948416ce69b633ad Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 18:00:09 +0100 Subject: [PATCH 083/186] 40% fade for folds, 0% fade for creases --- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 29be35677..f562112b0 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -296,8 +296,8 @@ void main() { drops = mix(drops, 0., fade); float foldsPattern = 0.; - fade *= .6; if (u_foldType < .5) { + fade *= .4; vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); @@ -308,7 +308,7 @@ void main() { distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); - foldsPattern = creasesResult.x * mix(1., .0, fade); + foldsPattern = creasesResult.x; drops *= mix(1., creasesResult.y, u_folds); pattern += u_folds * foldsPattern; From 6f849eb747d8a19b76732d56e3cbfee982f5217b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 18:12:59 +0100 Subject: [PATCH 084/186] fade logic fix --- packages/shaders/src/shaders/paper-texture.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index f562112b0..93e60e69f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -297,18 +297,17 @@ void main() { float foldsPattern = 0.; if (u_foldType < .5) { - fade *= .4; vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); - radialFolds = mix(radialFolds, vec3(0.), fade); + radialFolds = mix(radialFolds, vec3(.5), fade); foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); - foldsPattern = creasesResult.x; + foldsPattern = mix(creasesResult.x, .5, fade); drops *= mix(1., creasesResult.y, u_folds); pattern += u_folds * foldsPattern; From 3fb266a3184a386c89c2ec3a3cc8c3dc40269c39 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 18:25:33 +0100 Subject: [PATCH 085/186] fade logic update + img fade --- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 93e60e69f..fb2c58aaa 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -300,14 +300,13 @@ void main() { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); - radialFolds = mix(radialFolds, vec3(.5), fade); + radialFolds = mix(radialFolds, vec3(.5), .4 * fade); foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); - foldsPattern = mix(creasesResult.x, .5, fade); drops *= mix(1., creasesResult.y, u_folds); pattern += u_folds * foldsPattern; @@ -333,6 +332,7 @@ void main() { float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; + frame = mix(frame, 0., .2 * fade); vec3 color = fgColor * pattern; float opacity = fgOpacity * pattern; From 97c81bee4694a6b987a03adea48f664dc1f152f3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 18:33:18 +0100 Subject: [PATCH 086/186] creases fix --- packages/shaders/src/shaders/paper-texture.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index fb2c58aaa..fff699478 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -307,6 +307,7 @@ void main() { distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); } else { vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); + foldsPattern = creasesResult.x; drops *= mix(1., creasesResult.y, u_folds); pattern += u_folds * foldsPattern; From c7bf9e5104df62bb418cbd92d498ddb9e6a001a1 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 26 Jan 2026 20:24:31 +0100 Subject: [PATCH 087/186] spreadPreset as it was meant to be --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 22a12a691..ac7716489 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -99,7 +99,7 @@ export const spreadPreset: PaperTexturePreset = { crumpleSize: 0.42, folds: 1, foldType: 'creases', - foldCount: 1, + foldCount: 0, foldsShape: 0.2, drops: 0, fade: 0.68, From a8d6a359efe001bbad7cb46f782f473b27890b83 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 27 Jan 2026 01:12:10 +0100 Subject: [PATCH 088/186] multiple changes --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- .../src/shaders/paper-texture.tsx | 10 +-- packages/shaders/src/shaders/paper-texture.ts | 75 +++++++++++-------- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 25b4c2673..4124211a2 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -84,7 +84,7 @@ const PaperTextureWithControls = () => { crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, - foldCount: { value: defaults.foldCount, min: 0, max: 1, order: 232 }, + foldCount: { value: defaults.foldCount, min: 1, max: 20, step: 1, order: 232 }, foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index ac7716489..1e93275f3 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -46,8 +46,8 @@ export const defaultPreset: PaperTexturePreset = { crumpleSize: 0.5, folds: 0.75, foldType: 'folds', - foldCount: 0.7, - foldsShape: 0.75, + foldCount: 15, + foldsShape: 0.5, fade: 0, drops: 0.4, seed: 74, @@ -74,7 +74,7 @@ export const cardboardPreset: PaperTexturePreset = { crumpleSize: 0.1, folds: 0.25, foldType: 'creases', - foldCount: 0.4, + foldCount: 18, foldsShape: 0.28, fade: 0, drops: 0.1, @@ -99,7 +99,7 @@ export const spreadPreset: PaperTexturePreset = { crumpleSize: 0.42, folds: 1, foldType: 'creases', - foldCount: 0, + foldCount: 1, foldsShape: 0.2, drops: 0, fade: 0.68, @@ -128,7 +128,7 @@ export const flatPreset: PaperTexturePreset = { crumpleSize: 0.2, folds: 0, foldType: 'folds', - foldCount: 0.5, + foldCount: 5, foldsShape: 0.30, drops: 0.20, fade: 0.75, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index fff699478..f842dec3c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -214,8 +214,8 @@ float getDrops(vec2 uv) { vec3 getFolds(vec2 uv1, vec2 uv2) { vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; - for (int i = 0; i < 16; i++) { - if (float(i) >= max(1., u_foldCount * 16.)) break; + for (int i = 0; i < 18; i++) { + if (float(i) >= u_foldCount) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; @@ -238,15 +238,15 @@ vec3 getFolds(vec2 uv1, vec2 uv2) { ); } -vec2 getGrid(vec2 uv) { - float gridX = fract(uv.x * mix(.01, 4., u_foldCount) + .5); +vec3 getGrid(vec2 uv) { + float gridX = fract(uv.x * u_foldCount + .5 * mod(u_foldCount, 2.)); float dx = gridX - .5; - float foldWidth = u_foldsShape; + float foldWidth = mix(.15, 1., u_foldsShape); float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); float angle = sign(dx) * 1.1345 * foldAmount; float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); float grid = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., creaseDark); - return vec2(grid, creaseDark); + return vec3(grid, creaseDark, abs(dx)); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -262,13 +262,43 @@ void main() { vec2 patternUV = v_imageUV - .5; patternUV *= 5. * vec2(u_imageAspectRatio, 1.); + float ySide = (imageUV.y - .5); + float pattern = 0.; float distortionPatternRadial = 0.; float distortionPatternLinear = 0.; + float distortionPatternLinearDirectional = 0.; float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); + float drops = getDrops(patternUV * 2.); + drops = mix(drops, 0., fade); + + float foldsPattern = 0.; + if (u_foldType < .5) { + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); + vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); + radialFolds = mix(radialFolds, vec3(.5), .4 * fade); + foldsPattern = radialFolds.x + radialFolds.y; + + pattern += u_folds * foldsPattern; + distortionPatternLinearDirectional += .2 * u_folds * radialFolds.z; + distortionPatternRadial -= .2 * u_folds * (radialFolds.x - .5); + } else { + vec3 creasesResult = getGrid(imageUV + .5); + foldsPattern = creasesResult.x; + drops *= mix(1., creasesResult.y, u_folds); + + pattern += u_folds * foldsPattern; + float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); + distortionPatternLinearDirectional -= mix(.13, .05, u_foldCount / 18.) * (1. - distortBase); + } + + patternUV.x += u_distortion * distortionPatternLinear; + patternUV.y -= u_distortion * distortionPatternLinearDirectional * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); + vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); @@ -288,31 +318,10 @@ void main() { vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); crumples = mix(crumples, 0., fade); - - pattern += u_crumples * crumples; - distortionPatternLinear -= .01 * u_crumples * (crumples - .25); - - float drops = getDrops(patternUV * 2.); - drops = mix(drops, 0., fade); - float foldsPattern = 0.; - if (u_foldType < .5) { - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); - vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); - radialFolds = mix(radialFolds, vec3(.5), .4 * fade); - foldsPattern = radialFolds.x + radialFolds.y; - - pattern += u_folds * foldsPattern; - distortionPatternLinear += .2 * u_folds * (radialFolds.z - .5); - } else { - vec2 creasesResult = clamp(getGrid(patternUV), 0., 1.); - foldsPattern = creasesResult.x; - drops *= mix(1., creasesResult.y, u_folds); + pattern += u_crumples * crumples; + distortionPatternLinearDirectional -= .01 * u_crumples * (crumples - .25); - pattern += u_folds * foldsPattern; - distortionPatternLinear += .03 * u_folds * (pow(creasesResult.y, .2) - .5); - } drops *= u_drops; pattern += drops; @@ -323,7 +332,8 @@ void main() { vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; - imageUV.y += u_distortion * distortionPatternLinear; + imageUV.x += u_distortion * distortionPatternLinear; + imageUV.y -= u_distortion * distortionPatternLinearDirectional * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); vec2 dc = imageUV - .5; float r2 = dot(dc, dc); @@ -335,6 +345,8 @@ void main() { frame *= image.a; frame = mix(frame, 0., .2 * fade); +// pattern = mix(.5, pattern, frame); + vec3 color = fgColor * pattern; float opacity = fgOpacity * pattern; @@ -355,7 +367,10 @@ void main() { color = mix(color, pic, frame); +// color.r = creasesResult.x; + fragColor = vec4(color, opacity); +// fragColor = vec4(vec3(smoothstep(.0, .1, .3 * radialFolds.z)), opacity); } `; From a5070616d977e4bd4ca3cec5b70a08a2589516da Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 27 Jan 2026 01:14:47 +0100 Subject: [PATCH 089/186] presets tweaks --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 1e93275f3..cd6647e04 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -88,7 +88,7 @@ export const spreadPreset: PaperTexturePreset = { ...defaultObjectSizing, colorBack: "#ffffff", colorFront: "#c0c4c0", - blending: 0.36, + blending: 1, distortion: 0.7, seed: 448, roughness: 0.4, From 7b2a23f942f66c0f6f01159e492823e707c50977 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 28 Jan 2026 15:24:37 +0100 Subject: [PATCH 090/186] maxFoldCount to metadata --- docs/src/app/(shaders)/paper-texture/page.tsx | 3 ++- packages/shaders-react/src/index.ts | 1 + packages/shaders/src/index.ts | 1 + packages/shaders/src/shaders/paper-texture.ts | 8 ++++++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 4124211a2..dbd781d37 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -8,6 +8,7 @@ import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; import { ShaderFit, PaperTextureFoldTypes, + paperTextureMeta, type PaperTextureFoldType } from '@paper-design/shaders'; import { levaImageButton, levaDeleteImageButton } from '@/helpers/leva-image-button'; @@ -84,7 +85,7 @@ const PaperTextureWithControls = () => { crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, - foldCount: { value: defaults.foldCount, min: 1, max: 20, step: 1, order: 232 }, + foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 44d3c5471..ffa6ff807 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -125,6 +125,7 @@ export { grainGradientMeta, meshGradientMeta, metaballsMeta, + paperTextureMeta, pulsingBorderMeta, simplexNoiseMeta, smokeRingMeta, diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index fca1f8569..2ec2944cb 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -156,6 +156,7 @@ export { /** A static texture built from multiple noise layers, usable for a realistic paper and cardboard surfaces or generating abstract patterns. Can be used as a image filter or as a texture */ export { paperTextureFragmentShader, + paperTextureMeta, PaperTextureFoldTypes, type PaperTextureFoldType, type PaperTextureParams, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index f842dec3c..ac84db4ed 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -2,6 +2,10 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; import { rotation2, declarePI } from '../shader-utils.js'; +export const paperTextureMeta = { + maxFoldCount: 20, +} as const; + /** * A static texture built from multiple noise layers, usable for realistic paper and cardboard surfaces. * Can be used as an image filter or as a standalone texture. @@ -214,7 +218,7 @@ float getDrops(vec2 uv) { vec3 getFolds(vec2 uv1, vec2 uv2) { vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; - for (int i = 0; i < 18; i++) { + for (int i = 0; i < ${ paperTextureMeta.maxFoldCount }; i++) { if (float(i) >= u_foldCount) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; @@ -293,7 +297,7 @@ void main() { pattern += u_folds * foldsPattern; float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); - distortionPatternLinearDirectional -= mix(.13, .05, u_foldCount / 18.) * (1. - distortBase); + distortionPatternLinearDirectional -= mix(.13, .05, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); } patternUV.x += u_distortion * distortionPatternLinear; From 99b2c31784bafce6948cc695b171a8afea286f99 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 28 Jan 2026 15:38:54 +0100 Subject: [PATCH 091/186] backgroundFade added --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + packages/shaders-react/src/shaders/paper-texture.tsx | 8 +++++++- packages/shaders/src/shaders/paper-texture.ts | 9 +++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index dbd781d37..decc80016 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -89,6 +89,7 @@ const PaperTextureWithControls = () => { foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, + backgroundFade: { value: defaults.backgroundFade, min: 0, max: 1, order: 270 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index cd6647e04..c9649fd49 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -50,7 +50,8 @@ export const defaultPreset: PaperTexturePreset = { foldsShape: 0.5, fade: 0, drops: 0.4, - seed: 74, + seed: 464, + backgroundFade: 0, }, }; @@ -79,6 +80,7 @@ export const cardboardPreset: PaperTexturePreset = { fade: 0, drops: 0.1, seed: 1.6, + backgroundFade: 0, }, }; @@ -107,6 +109,7 @@ export const spreadPreset: PaperTexturePreset = { fit: "contain", speed: 1, frame: 0, + backgroundFade: 0, }, }; @@ -136,6 +139,7 @@ export const flatPreset: PaperTexturePreset = { fit: 'contain', speed: 1, frame: 0, + backgroundFade: 0, }, }; @@ -164,6 +168,7 @@ export const PaperTexture: React.FC = memo(function PaperText seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, distortion = defaultPreset.params.distortion, + backgroundFade = defaultPreset.params.backgroundFade, // Reworked props fiberScale, @@ -209,6 +214,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_seed: seed, u_blending: blending, u_distortion: distortion, + u_backgroundFade: backgroundFade, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index ac84db4ed..7a4945378 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -30,6 +30,7 @@ export const paperTextureMeta = { * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) * - u_blending (float): Amount of image-to-paper blending, 0 for original color, 1 for mix with colorBack (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) + * - u_backgroundFade (float): Fades the pattern outside the image frame (0 to 1) * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -74,6 +75,7 @@ uniform float u_seed; uniform float u_fade; uniform float u_blending; uniform float u_distortion; +uniform float u_backgroundFade; uniform sampler2D u_noiseTexture; in vec2 v_imageUV; @@ -349,7 +351,7 @@ void main() { frame *= image.a; frame = mix(frame, 0., .2 * fade); -// pattern = mix(.5, pattern, frame); + pattern = mix(.5, pattern, mix(1., frame, u_backgroundFade)); vec3 color = fgColor * pattern; float opacity = fgOpacity * pattern; @@ -370,11 +372,8 @@ void main() { pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); color = mix(color, pic, frame); - -// color.r = creasesResult.x; fragColor = vec4(color, opacity); -// fragColor = vec4(vec3(smoothstep(.0, .1, .3 * radialFolds.z)), opacity); } `; @@ -398,6 +397,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_seed: number; u_blending: number; u_distortion: number; + u_backgroundFade: number; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -419,6 +419,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara seed?: number; blending?: number; distortion?: number; + backgroundFade?: number; } export type PaperTextureFoldType = 'folds' | 'creases'; From 558dafe8fb551a5fbd18cc883780c119ed8ba18b Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 28 Jan 2026 19:42:39 +0100 Subject: [PATCH 092/186] distortion refine --- packages/shaders/src/shaders/paper-texture.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 7a4945378..b86e63c57 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -217,9 +217,10 @@ float getDrops(vec2 uv) { return 1. - lst(.05, .09, sqrt(dropsMinDist)); } -vec3 getFolds(vec2 uv1, vec2 uv2) { +vec4 getFolds(vec2 uv1, vec2 uv2) { vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; + float cellRand = 0.; for (int i = 0; i < ${ paperTextureMeta.maxFoldCount }; i++) { if (float(i) >= u_foldCount) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); @@ -234,13 +235,15 @@ vec3 getFolds(vec2 uv1, vec2 uv2) { if (dist2 < l2) { l2 = dist2; pp2 = vec3(uv2.x - p.x, dist2, rand.y); + cellRand = .5 * (rand.x + rand.y); } } float mult2 = mix(.22, .02, u_foldsShape); - return vec3( + return vec4( mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)), - .2 * pp2.y + .2 * pp2.y, + cellRand ); } @@ -280,18 +283,27 @@ void main() { float drops = getDrops(patternUV * 2.); drops = mix(drops, 0., fade); - + + vec4 foldsRaw = vec4(0.); + vec4 radialFolds = vec4(0.); float foldsPattern = 0.; if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .015 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); - vec3 radialFolds = clamp(5. * getFolds(foldsUV1, foldsUV2), 0., 1.); - radialFolds = mix(radialFolds, vec3(.5), .4 * fade); + vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); + foldsRaw = getFolds(foldsUV1, foldsUV2); + radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); + radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; - distortionPatternLinearDirectional += .2 * u_folds * radialFolds.z; - distortionPatternRadial -= .2 * u_folds * (radialFolds.x - .5); + + vec2 fromCenter = imageUV - .5; + float scaleFactor = u_folds * mix(-.1, .2, radialFolds.w); + scaleFactor -= .12 * radialFolds.x; + scaleFactor += .1 * radialFolds.y; + scaleFactor *= u_distortion; + scaleFactor *= mix(.5, .0, 2. * radialFolds.z); + imageUV = .5 + fromCenter * (1. + scaleFactor); } else { vec3 creasesResult = getGrid(imageUV + .5); foldsPattern = creasesResult.x; @@ -299,7 +311,7 @@ void main() { pattern += u_folds * foldsPattern; float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); - distortionPatternLinearDirectional -= mix(.13, .05, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); + distortionPatternLinearDirectional -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); } patternUV.x += u_distortion * distortionPatternLinear; @@ -373,7 +385,8 @@ void main() { color = mix(color, pic, frame); - fragColor = vec4(color, opacity); +// fragColor = vec4(vec3(smoothstep(.0, .7, 2. * radialFolds.z)), opacity); + fragColor = vec4(color, opacity); } `; From 7f35d0d676ec16ea0b2071ce3047c59f8d0d8662 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 28 Jan 2026 20:52:53 +0100 Subject: [PATCH 093/186] clean up --- .../src/shaders/paper-texture.tsx | 2 +- packages/shaders/src/shaders/paper-texture.ts | 49 +++++++++---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index c9649fd49..f2389d184 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -50,7 +50,7 @@ export const defaultPreset: PaperTexturePreset = { foldsShape: 0.5, fade: 0, drops: 0.4, - seed: 464, + seed: 354, backgroundFade: 0, }, }; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b86e63c57..900604b1f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -268,15 +268,17 @@ vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { void main() { vec2 imageUV = v_imageUV; + vec2 fromCenter = imageUV - .5; vec2 patternUV = v_imageUV - .5; patternUV *= 5. * vec2(u_imageAspectRatio, 1.); float ySide = (imageUV.y - .5); float pattern = 0.; - float distortionPatternRadial = 0.; - float distortionPatternLinear = 0.; - float distortionPatternLinearDirectional = 0.; + float radialDistortion = 0.; + float xDistortion = 0.; + float yDistortion = 0.; + float scaleDistortion = 0.; float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); @@ -284,26 +286,23 @@ void main() { float drops = getDrops(patternUV * 2.); drops = mix(drops, 0., fade); - vec4 foldsRaw = vec4(0.); - vec4 radialFolds = vec4(0.); float foldsPattern = 0.; if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); - foldsRaw = getFolds(foldsUV1, foldsUV2); - radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); + vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); + vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); foldsPattern = radialFolds.x + radialFolds.y; pattern += u_folds * foldsPattern; vec2 fromCenter = imageUV - .5; - float scaleFactor = u_folds * mix(-.1, .2, radialFolds.w); - scaleFactor -= .12 * radialFolds.x; - scaleFactor += .1 * radialFolds.y; - scaleFactor *= u_distortion; - scaleFactor *= mix(.5, .0, 2. * radialFolds.z); - imageUV = .5 + fromCenter * (1. + scaleFactor); + scaleDistortion = u_folds * mix(-.1, .2, radialFolds.w); + scaleDistortion -= .12 * radialFolds.x; + scaleDistortion += .1 * radialFolds.y; + scaleDistortion *= u_distortion; + scaleDistortion *= mix(.5, .0, 2. * radialFolds.z); } else { vec3 creasesResult = getGrid(imageUV + .5); foldsPattern = creasesResult.x; @@ -311,11 +310,11 @@ void main() { pattern += u_folds * foldsPattern; float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); - distortionPatternLinearDirectional -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); + yDistortion -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); } - patternUV.x += u_distortion * distortionPatternLinear; - patternUV.y -= u_distortion * distortionPatternLinearDirectional * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); + patternUV.x += u_distortion * xDistortion; + patternUV.y -= u_distortion * yDistortion * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; @@ -327,35 +326,34 @@ void main() { fiber *= (.05 + u_fiber); pattern += fiber; - distortionPatternRadial += .02 * fiber; + radialDistortion += .02 * fiber; roughness *= (.03 + u_roughness); pattern += roughness; - distortionPatternRadial += .02 * roughness; + radialDistortion += .02 * roughness; vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); crumples = mix(crumples, 0., fade); pattern += u_crumples * crumples; - distortionPatternLinearDirectional -= .01 * u_crumples * (crumples - .25); - + yDistortion -= .01 * u_crumples * (crumples - .25); drops *= u_drops; pattern += drops; - distortionPatternLinear += .03 * drops; + xDistortion += .03 * drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; - imageUV.x += u_distortion * distortionPatternLinear; - imageUV.y -= u_distortion * distortionPatternLinearDirectional * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); - + imageUV = .5 + fromCenter * (1. + scaleDistortion); + imageUV.x += u_distortion * xDistortion; + imageUV.y -= u_distortion * yDistortion * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); vec2 dc = imageUV - .5; float r2 = dot(dc, dc); - imageUV = .5 + dc * (1. - abs(u_distortion) * distortionPatternRadial * r2); + imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); float frame = getUvFrame(imageUV, frameSoftness); @@ -385,7 +383,6 @@ void main() { color = mix(color, pic, frame); -// fragColor = vec4(vec3(smoothstep(.0, .7, 2. * radialFolds.z)), opacity); fragColor = vec4(color, opacity); } `; From 248d9cc444b5e795ea789cf971a4cd15e107493c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 28 Jan 2026 21:07:53 +0100 Subject: [PATCH 094/186] vert distortion cleanup --- packages/shaders/src/shaders/paper-texture.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 900604b1f..6aeb7abdf 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -273,6 +273,7 @@ void main() { patternUV *= 5. * vec2(u_imageAspectRatio, 1.); float ySide = (imageUV.y - .5); + float ySidePower = sign(ySide) * mix(1., .7, step(0., ySide)) * abs(ySide); float pattern = 0.; float radialDistortion = 0.; @@ -314,7 +315,7 @@ void main() { } patternUV.x += u_distortion * xDistortion; - patternUV.y -= u_distortion * yDistortion * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); + patternUV.y -= u_distortion * yDistortion * ySidePower; vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; @@ -350,7 +351,7 @@ void main() { imageUV = .5 + fromCenter * (1. + scaleDistortion); imageUV.x += u_distortion * xDistortion; - imageUV.y -= u_distortion * yDistortion * sign(ySide) * mix(1., .5, step(0., ySide)) * abs(ySide); + imageUV.y -= u_distortion * yDistortion * ySidePower; vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); From bc5c02f474ed147884fb089037e8662a87dc620c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Thu, 19 Mar 2026 21:30:30 +0400 Subject: [PATCH 095/186] tweaking distortion and presets --- packages/shaders-react/src/shaders/paper-texture.tsx | 4 ++-- packages/shaders/src/shaders/paper-texture.ts | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index f2389d184..f017b5520 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -30,7 +30,7 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - blending: 0.7, + blending: 0.8, distortion: 0.5, fit: 'contain', scale: 0.8, @@ -59,7 +59,7 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - blending: 0.35, + blending: 0.6, distortion: 0.5, fit: 'contain', speed: 1, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6aeb7abdf..24244f0a4 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -299,11 +299,10 @@ void main() { pattern += u_folds * foldsPattern; vec2 fromCenter = imageUV - .5; - scaleDistortion = u_folds * mix(-.1, .2, radialFolds.w); - scaleDistortion -= .12 * radialFolds.x; - scaleDistortion += .1 * radialFolds.y; + scaleDistortion = .2 * radialFolds.z; + xDistortion += .04 * radialFolds.w; + scaleDistortion *= u_folds; scaleDistortion *= u_distortion; - scaleDistortion *= mix(.5, .0, 2. * radialFolds.z); } else { vec3 creasesResult = getGrid(imageUV + .5); foldsPattern = creasesResult.x; @@ -312,11 +311,9 @@ void main() { pattern += u_folds * foldsPattern; float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); yDistortion -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); + patternUV.y -= u_distortion * yDistortion * ySidePower; } - patternUV.x += u_distortion * xDistortion; - patternUV.y -= u_distortion * yDistortion * ySidePower; - vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); From 19dab53ee9830ba157ecd81e9d21162931ffafd3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 21 Mar 2026 09:57:21 +0100 Subject: [PATCH 096/186] backgroundFade => boolean background --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- .../src/shaders/paper-texture.tsx | 12 ++++----- packages/shaders/src/shaders/paper-texture.ts | 26 +++++++++++-------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index decc80016..80ea272ce 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -89,7 +89,7 @@ const PaperTextureWithControls = () => { foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, - backgroundFade: { value: defaults.backgroundFade, min: 0, max: 1, order: 270 }, + background: { value: defaults.background, order: 270 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index f017b5520..d63e37dca 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -51,7 +51,7 @@ export const defaultPreset: PaperTexturePreset = { fade: 0, drops: 0.4, seed: 354, - backgroundFade: 0, + background: true, }, }; @@ -80,7 +80,7 @@ export const cardboardPreset: PaperTexturePreset = { fade: 0, drops: 0.1, seed: 1.6, - backgroundFade: 0, + background: true, }, }; @@ -109,7 +109,7 @@ export const spreadPreset: PaperTexturePreset = { fit: "contain", speed: 1, frame: 0, - backgroundFade: 0, + background: true, }, }; @@ -139,7 +139,7 @@ export const flatPreset: PaperTexturePreset = { fit: 'contain', speed: 1, frame: 0, - backgroundFade: 0, + background: true, }, }; @@ -168,7 +168,7 @@ export const PaperTexture: React.FC = memo(function PaperText seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, distortion = defaultPreset.params.distortion, - backgroundFade = defaultPreset.params.backgroundFade, + background = defaultPreset.params.background, // Reworked props fiberScale, @@ -214,7 +214,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_seed: seed, u_blending: blending, u_distortion: distortion, - u_backgroundFade: backgroundFade, + u_background: background, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 24244f0a4..27ffb87d9 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -30,7 +30,7 @@ export const paperTextureMeta = { * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) * - u_blending (float): Amount of image-to-paper blending, 0 for original color, 1 for mix with colorBack (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) - * - u_backgroundFade (float): Fades the pattern outside the image frame (0 to 1) + * - u_background (bool): Shows or hides the paper texture outside the image frame * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -75,7 +75,7 @@ uniform float u_seed; uniform float u_fade; uniform float u_blending; uniform float u_distortion; -uniform float u_backgroundFade; +uniform bool u_background; uniform sampler2D u_noiseTexture; in vec2 v_imageUV; @@ -301,8 +301,9 @@ void main() { vec2 fromCenter = imageUV - .5; scaleDistortion = .2 * radialFolds.z; xDistortion += .04 * radialFolds.w; + + xDistortion *= u_folds; scaleDistortion *= u_folds; - scaleDistortion *= u_distortion; } else { vec3 creasesResult = getGrid(imageUV + .5); foldsPattern = creasesResult.x; @@ -311,7 +312,7 @@ void main() { pattern += u_folds * foldsPattern; float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); yDistortion -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); - patternUV.y -= u_distortion * yDistortion * ySidePower; + patternUV.y -= yDistortion * ySidePower; } vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; @@ -346,7 +347,7 @@ void main() { vec3 bgColor = u_colorBack.rgb * u_colorBack.a; float bgOpacity = u_colorBack.a; - imageUV = .5 + fromCenter * (1. + scaleDistortion); + imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); imageUV.x += u_distortion * xDistortion; imageUV.y -= u_distortion * yDistortion * ySidePower; vec2 dc = imageUV - .5; @@ -359,8 +360,6 @@ void main() { frame *= image.a; frame = mix(frame, 0., .2 * fade); - pattern = mix(.5, pattern, mix(1., frame, u_backgroundFade)); - vec3 color = fgColor * pattern; float opacity = fgOpacity * pattern; @@ -378,9 +377,14 @@ void main() { vec3 pic = blendMultiply(image.rgb, color, u_blending); pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); - + color = mix(color, pic, frame); - + + if (!u_background) { + opacity *= frame; + color *= frame; + } + fragColor = vec4(color, opacity); } `; @@ -405,7 +409,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_seed: number; u_blending: number; u_distortion: number; - u_backgroundFade: number; + u_background: boolean; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -427,7 +431,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara seed?: number; blending?: number; distortion?: number; - backgroundFade?: number; + background?: boolean; } export type PaperTextureFoldType = 'folds' | 'creases'; From 372f8160b024f87141fc9f9dcd365e57663eb290 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 21 Mar 2026 10:07:46 +0100 Subject: [PATCH 097/186] smoother distortion on folds --- packages/shaders/src/shaders/paper-texture.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 27ffb87d9..755a576e3 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -299,10 +299,7 @@ void main() { pattern += u_folds * foldsPattern; vec2 fromCenter = imageUV - .5; - scaleDistortion = .2 * radialFolds.z; - xDistortion += .04 * radialFolds.w; - - xDistortion *= u_folds; + scaleDistortion = .22 * radialFolds.z; scaleDistortion *= u_folds; } else { vec3 creasesResult = getGrid(imageUV + .5); From de93150d4c21c767ec0ead2172729b02340ab220 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sat, 21 Mar 2026 10:17:24 +0100 Subject: [PATCH 098/186] conditional UI --- docs/src/app/(shaders)/paper-texture/page.tsx | 71 +++++++++++-------- docs/src/helpers/leva-control-rules.ts | 52 ++++++++++++++ 2 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 docs/src/helpers/leva-control-rules.ts diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 80ea272ce..74dd851e6 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -12,6 +12,7 @@ import { type PaperTextureFoldType } from '@paper-design/shaders'; import { levaImageButton, levaDeleteImageButton } from '@/helpers/leva-image-button'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; import { ShaderDetails } from '@/components/shader-details'; @@ -21,6 +22,13 @@ import { useUrlParams } from '@/helpers/use-url-params'; const { worldWidth, worldHeight, ...defaults } = paperTexturePresets[0].params; +const controlRules: ControlRules = { + roughnessSize: { showWhen: { roughness: { gt: 0 } } }, + fiberSize: { showWhen: { fiber: { gt: 0 } } }, + crumpleSize: { showWhen: { crumples: { gt: 0 } } }, + // foldsShape: { showWhen: { foldType: { eq: 'creases' } } }, +}; + const imageFiles = [ '001.webp', '002.webp', @@ -71,36 +79,39 @@ const PaperTextureWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, - distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, - roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, - roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, - fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, - fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, - crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, - crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, - folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, - foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, - foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, - foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, - drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, - fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, - background: { value: defaults.background, order: 270 }, - scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, - fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, - Image: folder( - { - 'Upload image': levaImageButton(setImageWithoutStatus), - ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), - }, - { order: 0 } - ), - Presets: folder(presets, { order: -1 }), - }; + return applyControlRules( + { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, + distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, + seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, + roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, + roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, + fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, + fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, + crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, + crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, + folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, + foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, + foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, + foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, + drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, + fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, + background: { value: defaults.background, order: 270 }, + scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, + fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, + Image: folder( + { + 'Upload image': levaImageButton(setImageWithoutStatus), + ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), + }, + { order: 0 } + ), + Presets: folder(presets, { order: -1 }), + }, + controlRules + ); }, [image]); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/helpers/leva-control-rules.ts b/docs/src/helpers/leva-control-rules.ts new file mode 100644 index 000000000..08a0b4c86 --- /dev/null +++ b/docs/src/helpers/leva-control-rules.ts @@ -0,0 +1,52 @@ +/** + * Conditional visibility rules for Leva controls. + * + * Each rule maps a control name to the conditions under which it should be + * visible. Conditions reference other controls by name and support simple + * comparison operators. + */ + +export type ControlCondition = + | { gt: number } + | { eq: string | number }; + +export type ControlRule = { + /** All conditions must be met for the control to be shown. */ + showWhen: Record; + /** Custom label override. Defaults to "↳ {controlName}". */ + label?: string; +}; + +export type ControlRules = Record; + +function evaluateCondition(value: unknown, condition: ControlCondition): boolean { + if ('gt' in condition) return typeof value === 'number' && value > condition.gt; + if ('eq' in condition) return value === condition.eq; + return true; +} + +/** + * Adds Leva `render` callbacks to controls based on declarative rules. + * Controls not mentioned in `rules` are returned unchanged. + */ +export function applyControlRules( + schema: Record, + rules: ControlRules +): Record { + const result: Record = { ...schema }; + + for (const [controlName, rule] of Object.entries(rules)) { + if (result[controlName] != null && typeof result[controlName] === 'object') { + result[controlName] = { + ...result[controlName], + label: rule.label ?? `↳ ${controlName}`, + render: (get: (key: string) => unknown) => + Object.entries(rule.showWhen).every(([dep, condition]) => + evaluateCondition(get(dep), condition) + ), + }; + } + } + + return result; +} From d25ca0859b6d49dbebf9d6058c3228e55e091fb8 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Wed, 25 Mar 2026 11:19:47 +0100 Subject: [PATCH 099/186] saving some PT progress --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 - .../src/shaders/paper-texture.tsx | 18 ++++----- packages/shaders/src/shaders/paper-texture.ts | 40 +++++++++++-------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 74dd851e6..ae37731c4 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -23,7 +23,6 @@ import { useUrlParams } from '@/helpers/use-url-params'; const { worldWidth, worldHeight, ...defaults } = paperTexturePresets[0].params; const controlRules: ControlRules = { - roughnessSize: { showWhen: { roughness: { gt: 0 } } }, fiberSize: { showWhen: { fiber: { gt: 0 } } }, crumpleSize: { showWhen: { crumples: { gt: 0 } } }, // foldsShape: { showWhen: { foldType: { eq: 'creases' } } }, @@ -87,7 +86,6 @@ const PaperTextureWithControls = () => { distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, - roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index d63e37dca..cf86882c2 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,13 +38,13 @@ export const defaultPreset: PaperTexturePreset = { frame: 0, colorFront: '#c4cbd4', colorBack: '#ffffff', - roughness: 0.4, - roughnessSize: 0.5, - fiber: 0.2, + roughness: 1, + + fiber: 0, fiberSize: 0.5, - crumples: 0.3, + crumples: 0, crumpleSize: 0.5, - folds: 0.75, + folds: 0, foldType: 'folds', foldCount: 15, foldsShape: 0.5, @@ -68,7 +68,7 @@ export const cardboardPreset: PaperTexturePreset = { colorBack: "#dcc593", colorFront: "#685327", roughness: 0.1, - roughnessSize: 0.50, + fiber: 0.35, fiberSize: 0.14, crumples: 0., @@ -94,7 +94,7 @@ export const spreadPreset: PaperTexturePreset = { distortion: 0.7, seed: 448, roughness: 0.4, - roughnessSize: 0.4, + fiber: 0.1, fiberSize: 0.5, crumples: 0.2, @@ -124,7 +124,7 @@ export const flatPreset: PaperTexturePreset = { distortion: 0.33, seed: 415, roughness: 0.31, - roughnessSize: .3, + fiber: 0, fiberSize: 0.61, crumples: 0.7, @@ -158,7 +158,6 @@ export const PaperTexture: React.FC = memo(function PaperText colorBack = defaultPreset.params.colorBack, image = '', roughness = defaultPreset.params.roughness, - roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, @@ -200,7 +199,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_colorFront: getShaderColorFromString(colorFront), u_colorBack: getShaderColorFromString(colorBack), u_roughness: roughness, - u_roughnessSize: roughnessSize, u_fiber: fiber, u_fiberSize: fiberSize, u_crumples: crumples, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 755a576e3..5c65185ba 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -16,7 +16,6 @@ export const paperTextureMeta = { * - u_colorFront (vec4): Foreground color in RGBA * - u_colorBack (vec4): Background color in RGBA * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) - * - u_roughnessSize (float): Scale of the roughness pattern (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) * - u_fiberSize (float): Curly-shaped noise scale (0 to 1) * - u_crumples (float): Cell-based crumple pattern intensity (0 to 1) @@ -60,7 +59,6 @@ uniform vec4 u_colorBack; uniform sampler2D u_image; uniform float u_imageAspectRatio; -uniform float u_roughnessSize; uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; @@ -129,24 +127,33 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { vec2 fiberGrad = vec2(0.); pR *= .1; float scaleR = .1; + + // Roughness: adaptive octaves at 1.4x steps, from base down to ~2px cells + float pxFootprint = length(fwidth(pR)); + for (int i = 0; i < 12; i++) { + float pxPerCell = 1. / max(pxFootprint, .0001); + if (pxPerCell < 2.) break; + vec2 ipR = floor(pR); + vec2 fpR = fract(pR); + float octaveShift = float(i) * .1; + vec4 uvR = fract(vec4(ipR, ipR + 1.) / 50. + .5 + octaveShift); + float aR = texture(u_noiseTexture, uvR.xy).r; + float bR = texture(u_noiseTexture, uvR.xw).r; + float cR = texture(u_noiseTexture, uvR.zy).r; + float dR = texture(u_noiseTexture, uvR.zw).r; + roughDx += scaleR * mix(cR - aR, dR - bR, fpR.y); + pR *= 1.4; + scaleR *= 1.4; + pxFootprint *= 1.4; + } + + // Fiber: original 4 octaves float scaleF = 1.; float amplitude = 1.; float c = 0.7648; // cos(0.7) float s = 0.6442; // sin(0.7) float rc = 1., rs = 0.; // accumulated rotation for (int i = 0; i < 4; i++) { - if (i < 3) { - vec2 ipR = floor(pR); - vec2 fpR = fract(pR); - vec4 uvR = fract(vec4(ipR, ipR + 1.) / 50. + .5); - float aR = texture(u_noiseTexture, uvR.xy).r; - float bR = texture(u_noiseTexture, uvR.xw).r; - float cR = texture(u_noiseTexture, uvR.zy).r; - float dR = texture(u_noiseTexture, uvR.zw).r; - roughDx += scaleR * mix(cR - aR, dR - bR, fpR.y); - pR *= 2.1; - scaleR *= 2.1; - } pF = vec2(c * pF.x - s * pF.y, s * pF.x + c * pF.y); float rc2 = rc * c - rs * s; rs = rs * c + rc * s; @@ -167,6 +174,7 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { scaleF *= 2.; amplitude *= 0.6; } + return vec2(roughDx, length(fiberGrad)); } @@ -312,7 +320,7 @@ void main() { patternUV.y -= yDistortion * ySidePower; } - vec2 roughnessUV = mix(330., 100., u_roughnessSize) * patternUV; + vec2 roughnessUV = 200. * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); float roughness = clamp(5. * rf.x, 0., 1.); @@ -392,7 +400,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_colorBack: [number, number, number, number]; u_roughness: number; - u_roughnessSize: number; u_fiber: number; u_fiberSize: number; u_crumples: number; @@ -414,7 +421,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara colorFront?: string; colorBack?: string; roughness?: number; - roughnessSize?: number; fiber?: number; fiberSize?: number; crumples?: number; From 1311c2a5e9f364b93d07e66c5389748b2047ae10 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 26 Apr 2026 20:34:28 +0400 Subject: [PATCH 100/186] LOD saved --- packages/shaders/src/shaders/paper-texture.ts | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5c65185ba..41fe0de19 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -53,6 +53,7 @@ export const paperTextureMeta = { export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; +uniform vec2 u_resulution; uniform vec4 u_colorFront; uniform vec4 u_colorBack; @@ -123,37 +124,56 @@ float getFadeMask(vec2 n) { } vec2 getRoughnessFiber(vec2 pR, vec2 pF) { - float roughDx = 0.; - vec2 fiberGrad = vec2(0.); - pR *= .1; - float scaleR = .1; - - // Roughness: adaptive octaves at 1.4x steps, from base down to ~2px cells - float pxFootprint = length(fwidth(pR)); - for (int i = 0; i < 12; i++) { - float pxPerCell = 1. / max(pxFootprint, .0001); - if (pxPerCell < 2.) break; - vec2 ipR = floor(pR); - vec2 fpR = fract(pR); - float octaveShift = float(i) * .1; - vec4 uvR = fract(vec4(ipR, ipR + 1.) / 50. + .5 + octaveShift); - float aR = texture(u_noiseTexture, uvR.xy).r; - float bR = texture(u_noiseTexture, uvR.xw).r; - float cR = texture(u_noiseTexture, uvR.zy).r; - float dR = texture(u_noiseTexture, uvR.zw).r; - roughDx += scaleR * mix(cR - aR, dR - bR, fpR.y); - pR *= 1.4; - scaleR *= 1.4; - pxFootprint *= 1.4; + float w = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); + float eps = 100. * w; + vec2 u = pR / vec2(2, 4); + float roughnessScale = 4.; + float level = -log2(w + 1e-8); + float baseLevel = floor(level) - 4. + roughnessScale; + + vec2 pPlus = (u + vec2(eps, 0.)) * .1; + vec2 pMinus = (u - vec2(eps, 0.)) * .1; + float sumP = 0., sumM = 0., norm = 0., amp = .5; + for (int i = 0; i < 5; i++) { + float absIdx = baseLevel + float(i); + float freq = exp2(absIdx); + float shift = .5 + absIdx * .1; + + vec2 qP = pPlus * freq; + vec2 fP = fract(qP); + vec4 uvP = fract(vec4(floor(qP), ceil(qP)) / 50. + shift); + float aP = texture(u_noiseTexture, uvP.xy).r; + float bP = texture(u_noiseTexture, uvP.xw).r; + float cP = texture(u_noiseTexture, uvP.zy).r; + float dP = texture(u_noiseTexture, uvP.zw).r; + sumP += amp * (mix(mix(aP, bP, fP.y), mix(cP, dP, fP.y), fP.x) + + .2 / exp(2. * abs(sin(.2 * qP.x + .1 * qP.y)))); + + vec2 qM = pMinus * freq; + vec2 fM = fract(qM); + vec4 uvM = fract(vec4(floor(qM), ceil(qM)) / 50. + shift); + float aM = texture(u_noiseTexture, uvM.xy).r; + float bM = texture(u_noiseTexture, uvM.xw).r; + float cM = texture(u_noiseTexture, uvM.zy).r; + float dM = texture(u_noiseTexture, uvM.zw).r; + sumM += amp * (mix(mix(aM, bM, fM.y), mix(cM, dM, fM.y), fM.x) + + .2 / exp(2. * abs(sin(.2 * qM.x + .1 * qM.y)))); + + norm += amp; + amp *= .5; } + float roughDx = .8 + .3 * (sumP / norm - sumM / norm); + roughDx = sqrt((roughDx - .8) * 3. + .6); + + vec2 fiberGrad = vec2(0.); - // Fiber: original 4 octaves + // Fiber float scaleF = 1.; float amplitude = 1.; float c = 0.7648; // cos(0.7) float s = 0.6442; // sin(0.7) float rc = 1., rs = 0.; // accumulated rotation - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 7; i++) { pF = vec2(c * pF.x - s * pF.y, s * pF.x + c * pF.y); float rc2 = rc * c - rs * s; rs = rs * c + rc * s; @@ -323,16 +343,16 @@ void main() { vec2 roughnessUV = 200. * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); - float roughness = clamp(5. * rf.x, 0., 1.); - float fiber = clamp(rf.y - 1., -1., 1.); + float roughness = rf.x; + float fiber = rf.y; fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); - fiber *= (.05 + u_fiber); + fiber *= u_fiber; pattern += fiber; radialDistortion += .02 * fiber; - roughness *= (.03 + u_roughness); + roughness *= u_roughness; pattern += roughness; radialDistortion += .02 * roughness; @@ -390,7 +410,8 @@ void main() { color *= frame; } - fragColor = vec4(color, opacity); +// fragColor = vec4(color, opacity); + fragColor = vec4(vec3(1. - roughness), opacity); } `; From 3240f5f9cdbd4deb4577e9ea44482ac015cb0a93 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 26 Apr 2026 20:43:41 +0400 Subject: [PATCH 101/186] LOD working fine --- packages/shaders/src/shaders/paper-texture.ts | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 41fe0de19..aca999742 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -123,57 +123,57 @@ float getFadeMask(vec2 n) { return total; } -vec2 getRoughnessFiber(vec2 pR, vec2 pF) { - float w = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); - float eps = 100. * w; - vec2 u = pR / vec2(2, 4); - float roughnessScale = 4.; - float level = -log2(w + 1e-8); +float N(vec2 p, float w) { + p *= .1; + float roughnessScale = 3.; + float logLac = log2(2.1); + float level = -log2(w + 1e-8) / logLac; float baseLevel = floor(level) - 4. + roughnessScale; + float fade = fract(level); - vec2 pPlus = (u + vec2(eps, 0.)) * .1; - vec2 pMinus = (u - vec2(eps, 0.)) * .1; - float sumP = 0., sumM = 0., norm = 0., amp = .5; + float sum = 0., norm = 0., amp = .5; for (int i = 0; i < 5; i++) { float absIdx = baseLevel + float(i); - float freq = exp2(absIdx); - float shift = .5 + absIdx * .1; + float freq = pow(2.1, absIdx); + vec2 q = p * freq; + + float wi = 1.; + if (i == 0) wi = 1. - fade; + if (i == 4) wi = fade; - vec2 qP = pPlus * freq; - vec2 fP = fract(qP); - vec4 uvP = fract(vec4(floor(qP), ceil(qP)) / 50. + shift); - float aP = texture(u_noiseTexture, uvP.xy).r; - float bP = texture(u_noiseTexture, uvP.xw).r; - float cP = texture(u_noiseTexture, uvP.zy).r; - float dP = texture(u_noiseTexture, uvP.zw).r; - sumP += amp * (mix(mix(aP, bP, fP.y), mix(cP, dP, fP.y), fP.x) - + .2 / exp(2. * abs(sin(.2 * qP.x + .1 * qP.y)))); - - vec2 qM = pMinus * freq; - vec2 fM = fract(qM); - vec4 uvM = fract(vec4(floor(qM), ceil(qM)) / 50. + shift); - float aM = texture(u_noiseTexture, uvM.xy).r; - float bM = texture(u_noiseTexture, uvM.xw).r; - float cM = texture(u_noiseTexture, uvM.zy).r; - float dM = texture(u_noiseTexture, uvM.zw).r; - sumM += amp * (mix(mix(aM, bM, fM.y), mix(cM, dM, fM.y), fM.x) - + .2 / exp(2. * abs(sin(.2 * qM.x + .1 * qM.y)))); - - norm += amp; + vec2 f = fract(q); + float shift = .5 + absIdx * .1; + float uvY = floor(q.y) / 50. + shift; + vec2 s0 = texture(u_noiseTexture, fract(vec2(floor(q.x) / 50. + shift, uvY))).rg; + vec2 s1 = texture(u_noiseTexture, fract(vec2( ceil(q.x) / 50. + shift, uvY))).rg; + float an = absIdx * 1.7; + float n = mix(mix(s0.r, s0.g, f.y), mix(s1.r, s1.g, f.y), f.x) + + .2 / exp(2. * abs(sin(.22 * (q.x * cos(an) + q.y * sin(an))))); + + sum += amp * wi * n; + norm += amp * wi; amp *= .5; } - float roughDx = .8 + .3 * (sumP / norm - sumM / norm); + return sum / norm; +} + + +vec2 getRoughnessFiber(vec2 pR, vec2 pF) { + vec2 u = pR / vec2(2, 4); + float w = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); + float eps = 4. * w; + float roughDx = (.8 + .3 * (N(u + vec2(eps, 0.), w) - N(u - vec2(eps, 0.), w))); roughDx = sqrt((roughDx - .8) * 3. + .6); vec2 fiberGrad = vec2(0.); - // Fiber + // Fiber: original 4 octaves float scaleF = 1.; float amplitude = 1.; float c = 0.7648; // cos(0.7) float s = 0.6442; // sin(0.7) float rc = 1., rs = 0.; // accumulated rotation - for (int i = 0; i < 7; i++) { + for (int i = 0; i < 4; i++) { pF = vec2(c * pF.x - s * pF.y, s * pF.x + c * pF.y); float rc2 = rc * c - rs * s; rs = rs * c + rc * s; From 4bc72659bb047e669091e303f77cb096e11290d5 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 26 Apr 2026 21:01:06 +0400 Subject: [PATCH 102/186] small opt --- packages/shaders/src/shaders/paper-texture.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index aca999742..754075eb7 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -132,7 +132,7 @@ float N(vec2 p, float w) { float fade = fract(level); float sum = 0., norm = 0., amp = .5; - for (int i = 0; i < 5; i++) { + for (int i = 0; i < 4; i++) { float absIdx = baseLevel + float(i); float freq = pow(2.1, absIdx); vec2 q = p * freq; @@ -142,17 +142,16 @@ float N(vec2 p, float w) { if (i == 4) wi = fade; vec2 f = fract(q); - float shift = .5 + absIdx * .1; + float shift = .5 + absIdx * .3; float uvY = floor(q.y) / 50. + shift; vec2 s0 = texture(u_noiseTexture, fract(vec2(floor(q.x) / 50. + shift, uvY))).rg; vec2 s1 = texture(u_noiseTexture, fract(vec2( ceil(q.x) / 50. + shift, uvY))).rg; float an = absIdx * 1.7; - float n = mix(mix(s0.r, s0.g, f.y), mix(s1.r, s1.g, f.y), f.x) - + .2 / exp(2. * abs(sin(.22 * (q.x * cos(an) + q.y * sin(an))))); + float n = mix(mix(s0.r, s0.g, f.y), mix(s1.r, s1.g, f.y), f.x); sum += amp * wi * n; norm += amp * wi; - amp *= .5; + amp *= .8; } return sum / norm; } @@ -162,12 +161,12 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { vec2 u = pR / vec2(2, 4); float w = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); float eps = 4. * w; - float roughDx = (.8 + .3 * (N(u + vec2(eps, 0.), w) - N(u - vec2(eps, 0.), w))); - roughDx = sqrt((roughDx - .8) * 3. + .6); + float roughDx = (.5 + (N(u + vec2(eps, 0.), w) - N(u - vec2(eps, 0.), w))); + roughDx = roughDx * roughDx; - vec2 fiberGrad = vec2(0.); // Fiber: original 4 octaves + vec2 fiberGrad = vec2(0.); float scaleF = 1.; float amplitude = 1.; float c = 0.7648; // cos(0.7) @@ -410,8 +409,7 @@ void main() { color *= frame; } -// fragColor = vec4(color, opacity); - fragColor = vec4(vec3(1. - roughness), opacity); + fragColor = vec4(color, opacity); } `; From 1299164f050f8e8bc8bed7af406aeaacc37977b6 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Sun, 26 Apr 2026 21:36:33 +0400 Subject: [PATCH 103/186] roughness to share --- packages/shaders-react/src/shaders/paper-texture.tsx | 7 +++---- packages/shaders/src/shaders/paper-texture.ts | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index cf86882c2..2a67cd2f5 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -36,10 +36,9 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#c4cbd4', + colorFront: '#586c6f', colorBack: '#ffffff', - roughness: 1, - + roughness: 0.5, fiber: 0, fiberSize: 0.5, crumples: 0, @@ -49,7 +48,7 @@ export const defaultPreset: PaperTexturePreset = { foldCount: 15, foldsShape: 0.5, fade: 0, - drops: 0.4, + drops: 0, seed: 354, background: true, }, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 754075eb7..9cf97d3e2 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -164,15 +164,13 @@ vec2 getRoughnessFiber(vec2 pR, vec2 pF) { float roughDx = (.5 + (N(u + vec2(eps, 0.), w) - N(u - vec2(eps, 0.), w))); roughDx = roughDx * roughDx; - - // Fiber: original 4 octaves vec2 fiberGrad = vec2(0.); float scaleF = 1.; float amplitude = 1.; float c = 0.7648; // cos(0.7) float s = 0.6442; // sin(0.7) float rc = 1., rs = 0.; // accumulated rotation - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { pF = vec2(c * pF.x - s * pF.y, s * pF.x + c * pF.y); float rc2 = rc * c - rs * s; rs = rs * c + rc * s; @@ -342,8 +340,8 @@ void main() { vec2 roughnessUV = 200. * patternUV; vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); - float roughness = rf.x; - float fiber = rf.y; + float roughness = 2. * rf.x; + float fiber = .3 * rf.y; fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); From d2762822bc622e995476865b1f369da6edaeeaae Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 27 Apr 2026 18:16:30 +0400 Subject: [PATCH 104/186] roughnessSize added --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 ++ packages/shaders-react/src/shaders/paper-texture.tsx | 6 ++++++ packages/shaders/src/shaders/paper-texture.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index ae37731c4..74dd851e6 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -23,6 +23,7 @@ import { useUrlParams } from '@/helpers/use-url-params'; const { worldWidth, worldHeight, ...defaults } = paperTexturePresets[0].params; const controlRules: ControlRules = { + roughnessSize: { showWhen: { roughness: { gt: 0 } } }, fiberSize: { showWhen: { fiber: { gt: 0 } } }, crumpleSize: { showWhen: { crumples: { gt: 0 } } }, // foldsShape: { showWhen: { foldType: { eq: 'creases' } } }, @@ -86,6 +87,7 @@ const PaperTextureWithControls = () => { distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, + roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 2a67cd2f5..7eec3ff73 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -39,6 +39,7 @@ export const defaultPreset: PaperTexturePreset = { colorFront: '#586c6f', colorBack: '#ffffff', roughness: 0.5, + roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, crumples: 0, @@ -67,6 +68,7 @@ export const cardboardPreset: PaperTexturePreset = { colorBack: "#dcc593", colorFront: "#685327", roughness: 0.1, + roughnessSize: 0.25, fiber: 0.35, fiberSize: 0.14, @@ -93,6 +95,7 @@ export const spreadPreset: PaperTexturePreset = { distortion: 0.7, seed: 448, roughness: 0.4, + roughnessSize: 0.25, fiber: 0.1, fiberSize: 0.5, @@ -123,6 +126,7 @@ export const flatPreset: PaperTexturePreset = { distortion: 0.33, seed: 415, roughness: 0.31, + roughnessSize: 0.25, fiber: 0, fiberSize: 0.61, @@ -157,6 +161,7 @@ export const PaperTexture: React.FC = memo(function PaperText colorBack = defaultPreset.params.colorBack, image = '', roughness = defaultPreset.params.roughness, + roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, @@ -198,6 +203,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_colorFront: getShaderColorFromString(colorFront), u_colorBack: getShaderColorFromString(colorBack), u_roughness: roughness, + u_roughnessSize: roughnessSize, u_fiber: fiber, u_fiberSize: fiberSize, u_crumples: crumples, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9cf97d3e2..5d082a896 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -16,6 +16,7 @@ export const paperTextureMeta = { * - u_colorFront (vec4): Foreground color in RGBA * - u_colorBack (vec4): Background color in RGBA * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) + * - u_roughnessSize (float): Scale of the roughness noise (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) * - u_fiberSize (float): Curly-shaped noise scale (0 to 1) * - u_crumples (float): Cell-based crumple pattern intensity (0 to 1) @@ -74,6 +75,7 @@ uniform float u_seed; uniform float u_fade; uniform float u_blending; uniform float u_distortion; +uniform float u_roughnessSize; uniform bool u_background; uniform sampler2D u_noiseTexture; @@ -125,10 +127,10 @@ float getFadeMask(vec2 n) { float N(vec2 p, float w) { p *= .1; - float roughnessScale = 3.; + float roughnessSize = mix(3., 2., u_roughnessSize); float logLac = log2(2.1); float level = -log2(w + 1e-8) / logLac; - float baseLevel = floor(level) - 4. + roughnessScale; + float baseLevel = floor(level) - 4. + roughnessSize; float fade = fract(level); float sum = 0., norm = 0., amp = .5; @@ -417,6 +419,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_colorFront: [number, number, number, number]; u_colorBack: [number, number, number, number]; u_roughness: number; + u_roughnessSize: number; u_fiber: number; u_fiberSize: number; u_crumples: number; @@ -438,6 +441,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara colorFront?: string; colorBack?: string; roughness?: number; + roughnessSize?: number; fiber?: number; fiberSize?: number; crumples?: number; From 517922ce9878a9c29f382a3909ebceecf99d9671 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 27 Apr 2026 22:06:53 +0400 Subject: [PATCH 105/186] upd --- docs/src/app/(shaders)/paper-texture/page.tsx | 4 +- .../src/shaders/paper-texture.tsx | 6 +- packages/shaders/src/shaders/paper-texture.ts | 68 +++++++++++-------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 74dd851e6..afb0c3ef0 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -26,7 +26,9 @@ const controlRules: ControlRules = { roughnessSize: { showWhen: { roughness: { gt: 0 } } }, fiberSize: { showWhen: { fiber: { gt: 0 } } }, crumpleSize: { showWhen: { crumples: { gt: 0 } } }, - // foldsShape: { showWhen: { foldType: { eq: 'creases' } } }, + foldType: { showWhen: { folds: { gt: 0 } } }, + foldCount: { showWhen: { folds: { gt: 0 } } }, + foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'creases' } } }, }; const imageFiles = [ diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 7eec3ff73..5563c7b5c 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,10 +38,10 @@ export const defaultPreset: PaperTexturePreset = { frame: 0, colorFront: '#586c6f', colorBack: '#ffffff', - roughness: 0.5, + roughness: 0, roughnessSize: 0.25, - fiber: 0, - fiberSize: 0.5, + fiber: 1, + fiberSize: 1, crumples: 0, crumpleSize: 0.5, folds: 0, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5d082a896..19a6daf4b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -125,19 +125,19 @@ float getFadeMask(vec2 n) { return total; } -float N(vec2 p, float w) { +float getRoughness(vec2 p, float w) { p *= .1; - float roughnessSize = mix(3., 2., u_roughnessSize); + float roughnessSize = mix(3.2, .6, u_roughnessSize); float logLac = log2(2.1); float level = -log2(w + 1e-8) / logLac; - float baseLevel = floor(level) - 4. + roughnessSize; + float baseLevel = floor(level) - 2.; float fade = fract(level); float sum = 0., norm = 0., amp = .5; for (int i = 0; i < 4; i++) { float absIdx = baseLevel + float(i); float freq = pow(2.1, absIdx); - vec2 q = p * freq; + vec2 q = roughnessSize * p * freq; float wi = 1.; if (i == 0) wi = 1. - fade; @@ -160,38 +160,50 @@ float N(vec2 p, float w) { vec2 getRoughnessFiber(vec2 pR, vec2 pF) { - vec2 u = pR / vec2(2, 4); - float w = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); - float eps = 4. * w; - float roughDx = (.5 + (N(u + vec2(eps, 0.), w) - N(u - vec2(eps, 0.), w))); + vec2 uR = pR / vec2(2, 4); + float wR = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); + float epsR = 4. * wR; + float roughDx = (.5 + (getRoughness(uR + vec2(epsR, 0.), wR) - getRoughness(uR - vec2(epsR, 0.), wR))); roughDx = roughDx * roughDx; - + + + float fiberSize = mix(2., .4, u_fiberSize); + float wF = max(length(dFdx(pF)), length(dFdy(pF))); + float levelF = -log2(wF + 1e-8); + float baseLevelF = floor(levelF) - 3.; + float fadeF = fract(levelF); + vec2 fiberGrad = vec2(0.); float scaleF = 1.; float amplitude = 1.; - float c = 0.7648; // cos(0.7) - float s = 0.6442; // sin(0.7) - float rc = 1., rs = 0.; // accumulated rotation for (int i = 0; i < 5; i++) { - pF = vec2(c * pF.x - s * pF.y, s * pF.x + c * pF.y); - float rc2 = rc * c - rs * s; - rs = rs * c + rc * s; - rc = rc2; - vec2 ipF = floor(pF); - vec2 fpF = fract(pF); - vec4 uvF = fract(vec4(ipF, ipF + 1.) / 50. + .5); + float absIdx = baseLevelF + float(i); + float freq = pow(1.8, absIdx); + vec2 q = fiberSize * pF * freq; + + float an = absIdx; + float rc = cos(an), rs = sin(an); + q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); + + float wi = 1.; + if (i == 0) wi = 1. - fadeF; + if (i == 4) wi = fadeF; + + vec2 ipQ = floor(q); + vec2 fpQ = fract(q); + float shift = absIdx * .3; + vec4 uvF = fract(vec4(ipQ, ipQ + 1.) / 50. + .5 + shift); float aF = texture(u_noiseTexture, uvF.xy).b; float bF = texture(u_noiseTexture, uvF.zy).b; float cF = texture(u_noiseTexture, uvF.xw).b; float dF = texture(u_noiseTexture, uvF.zw).b; - vec2 u = fpF * fpF * (3. - 2. * fpF); - vec2 du = 6. * fpF * (1. - fpF); + vec2 u = fpQ * fpQ * (3. - 2. * fpQ); + vec2 du = 8. * fpQ * (1. - fpQ); float dxF = du.x * mix(bF - aF, dF - cF, u.y); float dyF = du.y * mix(cF - aF, dF - bF, u.x); - fiberGrad += amplitude * scaleF * vec2(rc * dxF + rs * dyF, -rs * dxF + rc * dyF); - pF *= 2.; - scaleF *= 2.; - amplitude *= 0.6; + fiberGrad += wi * amplitude * scaleF * vec2(rc * dxF + rs * dyF, -rs * dxF + rc * dyF); + scaleF *= 1.7; + amplitude *= .5; } return vec2(roughDx, length(fiberGrad)); @@ -265,7 +277,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { cellRand = .5 * (rand.x + rand.y); } } - float mult2 = mix(.22, .02, u_foldsShape); + float mult2 = mix(.22, .02, 1.); return vec4( mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)), @@ -277,7 +289,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { vec3 getGrid(vec2 uv) { float gridX = fract(uv.x * u_foldCount + .5 * mod(u_foldCount, 2.)); float dx = gridX - .5; - float foldWidth = mix(.15, 1., u_foldsShape); + float foldWidth = mix(.1, .45, u_foldsShape); float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); float angle = sign(dx) * 1.1345 * foldAmount; float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); @@ -340,7 +352,7 @@ void main() { } vec2 roughnessUV = 200. * patternUV; - vec2 fiberUV = mix(22., 4., u_fiberSize) * patternUV; + vec2 fiberUV = 10. * patternUV; vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); float roughness = 2. * rf.x; float fiber = .3 * rf.y; From 70cb710adf3176cda0fbb7d5c627dd0a3e263aa2 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 27 Apr 2026 22:35:22 +0400 Subject: [PATCH 106/186] upd --- docs/src/app/(shaders)/paper-texture/page.tsx | 4 +- .../src/shaders/paper-texture.tsx | 6 +- packages/shaders/src/shaders/paper-texture.ts | 112 +++++++++--------- 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index afb0c3ef0..f79263e65 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -27,8 +27,8 @@ const controlRules: ControlRules = { fiberSize: { showWhen: { fiber: { gt: 0 } } }, crumpleSize: { showWhen: { crumples: { gt: 0 } } }, foldType: { showWhen: { folds: { gt: 0 } } }, - foldCount: { showWhen: { folds: { gt: 0 } } }, - foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'creases' } } }, + foldCount: { showWhen: { folds: { gt: 0 } }, label: '↳ count' }, + foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'creases' } }, label: '↳ shape' }, }; const imageFiles = [ diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 5563c7b5c..f5cdd0e41 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -36,12 +36,12 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#586c6f', + colorFront: '#a6a6a6', colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, - fiber: 1, - fiberSize: 1, + fiber: 0.5, + fiberSize: 0.5, crumples: 0, crumpleSize: 0.5, folds: 0, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 19a6daf4b..6003820a7 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -125,88 +125,95 @@ float getFadeMask(vec2 n) { return total; } -float getRoughness(vec2 p, float w) { - p *= .1; - float roughnessSize = mix(3.2, .6, u_roughnessSize); +float getRoughness(vec2 p) { + vec2 u = p / vec2(2, 4); + float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); + float eps = 4. * w; + + float size = mix(3.2, .6, u_roughnessSize); float logLac = log2(2.1); float level = -log2(w + 1e-8) / logLac; float baseLevel = floor(level) - 2.; float fade = fract(level); - float sum = 0., norm = 0., amp = .5; + vec2 px = (u.x + vec2(eps, -eps)) * .1; + float py = u.y * .1; + + vec2 sum = vec2(0.); + float norm = 0., amp = .5; for (int i = 0; i < 4; i++) { float absIdx = baseLevel + float(i); float freq = pow(2.1, absIdx); - vec2 q = roughnessSize * p * freq; + vec2 qx = size * px * freq; + float qy = size * py * freq; float wi = 1.; if (i == 0) wi = 1. - fade; if (i == 4) wi = fade; - vec2 f = fract(q); + vec2 fx = fract(qx); + float fy = fract(qy); float shift = .5 + absIdx * .3; - float uvY = floor(q.y) / 50. + shift; - vec2 s0 = texture(u_noiseTexture, fract(vec2(floor(q.x) / 50. + shift, uvY))).rg; - vec2 s1 = texture(u_noiseTexture, fract(vec2( ceil(q.x) / 50. + shift, uvY))).rg; - float an = absIdx * 1.7; - float n = mix(mix(s0.r, s0.g, f.y), mix(s1.r, s1.g, f.y), f.x); + float uvY = floor(qy) / 50. + shift; + vec2 s0a = texture(u_noiseTexture, fract(vec2(floor(qx.x) / 50. + shift, uvY))).rg; + vec2 s1a = texture(u_noiseTexture, fract(vec2( ceil(qx.x) / 50. + shift, uvY))).rg; + vec2 s0b = texture(u_noiseTexture, fract(vec2(floor(qx.y) / 50. + shift, uvY))).rg; + vec2 s1b = texture(u_noiseTexture, fract(vec2( ceil(qx.y) / 50. + shift, uvY))).rg; + vec2 ny0 = mix(vec2(s0a.r, s0b.r), vec2(s0a.g, s0b.g), fy); + vec2 ny1 = mix(vec2(s1a.r, s1b.r), vec2(s1a.g, s1b.g), fy); + vec2 n = mix(ny0, ny1, fx); sum += amp * wi * n; norm += amp * wi; amp *= .8; } - return sum / norm; -} - - -vec2 getRoughnessFiber(vec2 pR, vec2 pF) { - vec2 uR = pR / vec2(2, 4); - float wR = max(length(dFdx(pR * .1)), length(dFdy(pR * .1))); - float epsR = 4. * wR; - float roughDx = (.5 + (getRoughness(uR + vec2(epsR, 0.), wR) - getRoughness(uR - vec2(epsR, 0.), wR))); - roughDx = roughDx * roughDx; + vec2 r = sum / norm; + float dx = .5 + r.x - r.y; + return 3. * dx * dx; +} - float fiberSize = mix(2., .4, u_fiberSize); - float wF = max(length(dFdx(pF)), length(dFdy(pF))); - float levelF = -log2(wF + 1e-8); - float baseLevelF = floor(levelF) - 3.; - float fadeF = fract(levelF); +float getFiber(vec2 p) { + float size = mix(2., .4, u_fiberSize); + float w = max(length(dFdx(p)), length(dFdy(p))); + float level = -log2(w + 1e-8); + float baseLevel = floor(level) - 3.; + float fade = fract(level); - vec2 fiberGrad = vec2(0.); - float scaleF = 1.; - float amplitude = 1.; + vec2 grad = vec2(0.); + float scale = 1.; + float amp = 1.; for (int i = 0; i < 5; i++) { - float absIdx = baseLevelF + float(i); - float freq = pow(1.8, absIdx); - vec2 q = fiberSize * pF * freq; + float absIdx = baseLevel + float(i); + float freq = pow(1.7, absIdx); + vec2 q = size * p * freq; - float an = absIdx; + float an = absIdx * .8; float rc = cos(an), rs = sin(an); q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); float wi = 1.; - if (i == 0) wi = 1. - fadeF; - if (i == 4) wi = fadeF; + if (i == 0) wi = 1. - fade; + if (i == 4) wi = fade; - vec2 ipQ = floor(q); - vec2 fpQ = fract(q); + vec2 iq = floor(q); + vec2 fq = fract(q); float shift = absIdx * .3; - vec4 uvF = fract(vec4(ipQ, ipQ + 1.) / 50. + .5 + shift); - float aF = texture(u_noiseTexture, uvF.xy).b; - float bF = texture(u_noiseTexture, uvF.zy).b; - float cF = texture(u_noiseTexture, uvF.xw).b; - float dF = texture(u_noiseTexture, uvF.zw).b; - vec2 u = fpQ * fpQ * (3. - 2. * fpQ); - vec2 du = 8. * fpQ * (1. - fpQ); - float dxF = du.x * mix(bF - aF, dF - cF, u.y); - float dyF = du.y * mix(cF - aF, dF - bF, u.x); - fiberGrad += wi * amplitude * scaleF * vec2(rc * dxF + rs * dyF, -rs * dxF + rc * dyF); - scaleF *= 1.7; - amplitude *= .5; + vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift); + float aF = texture(u_noiseTexture, uv.xy).b; + float bF = texture(u_noiseTexture, uv.zy).b; + float cF = texture(u_noiseTexture, uv.xw).b; + float dF = texture(u_noiseTexture, uv.zw).b; + vec2 u = fq * fq * (3. - 2. * fq); + vec2 du = 8. * fq * (1. - fq); + float dx = du.x * mix(bF - aF, dF - cF, u.y); + float dy = du.y * mix(cF - aF, dF - bF, u.x); + grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); + scale *= 1.7; + amp *= .5; } - return vec2(roughDx, length(fiberGrad)); + return min(1., .5 * length(grad)); } vec2 randomGB(vec2 p) { @@ -353,9 +360,8 @@ void main() { vec2 roughnessUV = 200. * patternUV; vec2 fiberUV = 10. * patternUV; - vec2 rf = getRoughnessFiber(roughnessUV, fiberUV); - float roughness = 2. * rf.x; - float fiber = .3 * rf.y; + float roughness = getRoughness(roughnessUV); + float fiber = getFiber(fiberUV); fiber *= mix(1., .5, fade); roughness *= mix(1., .5, fade); From 06372cd41303dc9f36e524cf2f8c468b5f6b506c Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 27 Apr 2026 22:42:50 +0400 Subject: [PATCH 107/186] zero conditions wrap --- packages/shaders/src/shaders/paper-texture.ts | 110 +++++++++--------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6003820a7..2a651977d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -330,59 +330,65 @@ void main() { float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); fade = clamp(8. * fade * fade * fade, 0., 1.); - float drops = getDrops(patternUV * 2.); - drops = mix(drops, 0., fade); - - float foldsPattern = 0.; - if (u_foldType < .5) { - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); - vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); - vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); - radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); - foldsPattern = radialFolds.x + radialFolds.y; - - pattern += u_folds * foldsPattern; - - vec2 fromCenter = imageUV - .5; - scaleDistortion = .22 * radialFolds.z; - scaleDistortion *= u_folds; - } else { - vec3 creasesResult = getGrid(imageUV + .5); - foldsPattern = creasesResult.x; - drops *= mix(1., creasesResult.y, u_folds); - - pattern += u_folds * foldsPattern; - float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); - yDistortion -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); - patternUV.y -= yDistortion * ySidePower; + float drops = 0.; + if (u_drops > 0.) { + drops = getDrops(patternUV * 2.); + drops = mix(drops, 0., fade); + } + + if (u_folds > 0.) { + if (u_foldType < .5) { + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); + vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); + vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); + radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); + float foldsPattern = radialFolds.x + radialFolds.y; + + pattern += u_folds * foldsPattern; + + vec2 fromCenter = imageUV - .5; + scaleDistortion = .22 * radialFolds.z * u_folds; + } else { + vec3 creasesResult = getGrid(imageUV + .5); + drops *= mix(1., creasesResult.y, u_folds); + + pattern += u_folds * creasesResult.x; + float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); + yDistortion -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); + patternUV.y -= yDistortion * ySidePower; + } + } + + if (u_roughness > 0.) { + float roughness = getRoughness(200. * patternUV); + roughness *= mix(1., .5, fade); + roughness *= u_roughness; + pattern += roughness; + radialDistortion += .02 * roughness; + } + + if (u_fiber > 0.) { + float fiber = getFiber(10. * patternUV); + fiber *= mix(1., .5, fade); + fiber *= u_fiber; + pattern += fiber; + radialDistortion += .02 * fiber; + } + + if (u_crumples > 0.) { + vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; + float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); + crumples = mix(crumples, 0., fade); + pattern += u_crumples * crumples; + yDistortion -= .01 * u_crumples * (crumples - .25); + } + + if (u_drops > 0.) { + drops *= u_drops; + pattern += drops; + xDistortion += .03 * drops; } - - vec2 roughnessUV = 200. * patternUV; - vec2 fiberUV = 10. * patternUV; - float roughness = getRoughness(roughnessUV); - float fiber = getFiber(fiberUV); - fiber *= mix(1., .5, fade); - roughness *= mix(1., .5, fade); - - fiber *= u_fiber; - pattern += fiber; - radialDistortion += .02 * fiber; - - roughness *= u_roughness; - pattern += roughness; - radialDistortion += .02 * roughness; - - vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; - float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); - crumples = mix(crumples, 0., fade); - - pattern += u_crumples * crumples; - yDistortion -= .01 * u_crumples * (crumples - .25); - - drops *= u_drops; - pattern += drops; - xDistortion += .03 * drops; vec3 fgColor = u_colorFront.rgb * u_colorFront.a; float fgOpacity = u_colorFront.a; From 9a5f493165a675a9b1e0f5180dad39e2fd199655 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 28 Apr 2026 00:31:56 +0400 Subject: [PATCH 108/186] saving prog --- docs/src/app/(shaders)/paper-texture/page.tsx | 12 ++- docs/src/helpers/leva-control-rules.ts | 4 +- .../src/shaders/paper-texture.tsx | 22 ++++-- packages/shaders/src/shaders/paper-texture.ts | 76 ++++++++++++++----- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index f79263e65..07787cac7 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -27,8 +27,10 @@ const controlRules: ControlRules = { fiberSize: { showWhen: { fiber: { gt: 0 } } }, crumpleSize: { showWhen: { crumples: { gt: 0 } } }, foldType: { showWhen: { folds: { gt: 0 } } }, - foldCount: { showWhen: { folds: { gt: 0 } }, label: '↳ count' }, - foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'creases' } }, label: '↳ shape' }, + foldCount: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'folds' } }, label: '↳ count' }, + foldSize: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ size' }, + foldOffset: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ offset' }, + foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ shape' }, }; const imageFiles = [ @@ -87,7 +89,6 @@ const PaperTextureWithControls = () => { colorFront: { value: toHsla(defaults.colorFront), order: 101 }, blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, @@ -97,8 +98,11 @@ const PaperTextureWithControls = () => { folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, - foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 233 }, + foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, + foldOffset: { value: defaults.foldOffset, min: 0, max: 1, order: 233 }, + foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 234 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, + seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, background: { value: defaults.background, order: 270 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, diff --git a/docs/src/helpers/leva-control-rules.ts b/docs/src/helpers/leva-control-rules.ts index 08a0b4c86..6571a6704 100644 --- a/docs/src/helpers/leva-control-rules.ts +++ b/docs/src/helpers/leva-control-rules.ts @@ -8,7 +8,8 @@ export type ControlCondition = | { gt: number } - | { eq: string | number }; + | { eq: string | number } + | { neq: string | number }; export type ControlRule = { /** All conditions must be met for the control to be shown. */ @@ -22,6 +23,7 @@ export type ControlRules = Record; function evaluateCondition(value: unknown, condition: ControlCondition): boolean { if ('gt' in condition) return typeof value === 'number' && value > condition.gt; if ('eq' in condition) return value === condition.eq; + if ('neq' in condition) return value !== condition.neq; return true; } diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index f5cdd0e41..7f307a5db 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -36,7 +36,7 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#a6a6a6', + colorFront: '#c4c4c4', colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, @@ -44,10 +44,12 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleSize: 0.5, - folds: 0, - foldType: 'folds', - foldCount: 15, - foldsShape: 0.5, + folds: 1, + foldType: 'creases', + foldCount: 5, + foldSize: 0.79, + foldOffset: 0, + foldsShape: 1, fade: 0, drops: 0, seed: 354, @@ -77,6 +79,8 @@ export const cardboardPreset: PaperTexturePreset = { folds: 0.25, foldType: 'creases', foldCount: 18, + foldSize: 0.1, + foldOffset: 0, foldsShape: 0.28, fade: 0, drops: 0.1, @@ -104,6 +108,8 @@ export const spreadPreset: PaperTexturePreset = { folds: 1, foldType: 'creases', foldCount: 1, + foldSize: 1, + foldOffset: 0, foldsShape: 0.2, drops: 0, fade: 0.68, @@ -135,6 +141,8 @@ export const flatPreset: PaperTexturePreset = { folds: 0, foldType: 'folds', foldCount: 5, + foldSize: 0.79, + foldOffset: 0, foldsShape: 0.30, drops: 0.20, fade: 0.75, @@ -166,6 +174,8 @@ export const PaperTexture: React.FC = memo(function PaperText crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, foldType = defaultPreset.params.foldType, + foldSize = defaultPreset.params.foldSize, + foldOffset = defaultPreset.params.foldOffset, foldsShape = defaultPreset.params.foldsShape, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, @@ -209,6 +219,8 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumples: crumples, u_crumpleSize: crumpleSize, u_foldCount: foldCount, + u_foldSize: foldSize, + u_foldOffset: foldOffset, u_folds: folds, u_foldType: PaperTextureFoldTypes[foldType], u_foldsShape: foldsShape, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 2a651977d..7999483af 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -69,7 +69,9 @@ uniform float u_crumpleSize; uniform float u_folds; uniform float u_foldType; uniform float u_foldCount; +uniform float u_foldSize; uniform float u_foldsShape; +uniform float u_foldOffset; uniform float u_drops; uniform float u_seed; uniform float u_fade; @@ -268,7 +270,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { float l1 = 9., l2 = 9.; float cellRand = 0.; for (int i = 0; i < ${ paperTextureMeta.maxFoldCount }; i++) { - if (float(i) >= u_foldCount) break; + if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; @@ -293,15 +295,18 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { ); } -vec3 getGrid(vec2 uv) { - float gridX = fract(uv.x * u_foldCount + .5 * mod(u_foldCount, 2.)); - float dx = gridX - .5; - float foldWidth = mix(.1, .45, u_foldsShape); - float foldAmount = 1. - smoothstep(0., foldWidth, abs(dx)); - float angle = sign(dx) * 1.1345 * foldAmount; +vec4 getCrease(float coord, float offset) { + float count = mix(20., 1., u_foldSize); + float g = (coord - 1.) * count + .5 * offset; + float crX = fract(g); + float creaseIdx = floor(g); + float depthMod = .7 + .3 * fract(sin(creaseIdx * 127.1 + u_seed * 3.7) * 43758.5453); + float dx = crX - .5; + float foldWidth = mix(.1, .5, u_foldsShape); + float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * depthMod; + float slope = sign(dx) * foldAmount; float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); - float grid = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., creaseDark); - return vec3(grid, creaseDark, abs(dx)); + return vec4(slope, creaseDark, abs(dx), foldAmount); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -319,12 +324,13 @@ void main() { patternUV *= 5. * vec2(u_imageAspectRatio, 1.); float ySide = (imageUV.y - .5); - float ySidePower = sign(ySide) * mix(1., .7, step(0., ySide)) * abs(ySide); + float ySidePower = sign(ySide) * mix(1., 1.25, step(0., ySide)) * abs(ySide); float pattern = 0.; float radialDistortion = 0.; float xDistortion = 0.; float yDistortion = 0.; + float yShift = 0.; float scaleDistortion = 0.; float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); @@ -349,14 +355,33 @@ void main() { vec2 fromCenter = imageUV - .5; scaleDistortion = .22 * radialFolds.z * u_folds; + } else if (u_foldType < 1.5) { + vec4 cr = getCrease((imageUV + .5).x, u_foldOffset); + float angle = cr.x * 1.1345; + float crLight = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., cr.y); + drops *= mix(1., cr.y, u_folds); + + pattern += u_folds * crLight; + float distortBase = mix(pow(cr.y, .2), cr.z, .5 * u_foldsShape); + yShift += .022 * (1. - distortBase); + patternUV.y += yShift; } else { - vec3 creasesResult = getGrid(imageUV + .5); - drops *= mix(1., creasesResult.y, u_folds); - - pattern += u_folds * creasesResult.x; - float distortBase = mix(pow(creasesResult.y, .2), creasesResult.z, pow(u_foldsShape, 3.)); - yDistortion -= mix(.1, .02, u_foldCount / float(${ paperTextureMeta.maxFoldCount })) * (1. - distortBase); - patternUV.y -= yDistortion * ySidePower; + vec2 uv = imageUV + .5; + vec4 h = getCrease(uv.x, u_foldOffset); + vec4 v = getCrease(uv.y, u_foldOffset); + float ax = h.x * 1.1345; + float ay = v.x * 1.1345; + float gridDark = h.y * v.y; + float gridLight = max(-.5 * sin(ax) - .5 * sin(ay) + .5 * cos(ax) * cos(ay), 0.) * mix(.9, 1., gridDark); + float gridDist = min(h.z, v.z); + drops *= mix(1., gridDark, u_folds); + + pattern += u_folds * gridLight; + float distortBaseH = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); + float distortBaseV = mix(pow(v.y, .2), v.z, .5 * u_foldsShape); + patternUV.x += .022 * (1. - distortBaseV); + yShift += .022 * (1. - distortBaseH); + patternUV.y += yShift; } } @@ -397,7 +422,7 @@ void main() { imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); imageUV.x += u_distortion * xDistortion; - imageUV.y -= u_distortion * yDistortion * ySidePower; + imageUV.y -= u_distortion * (yDistortion * ySidePower + yShift); vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); @@ -406,7 +431,6 @@ void main() { float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; - frame = mix(frame, 0., .2 * fade); vec3 color = fgColor * pattern; float opacity = fgOpacity * pattern; @@ -429,9 +453,14 @@ void main() { color = mix(color, pic, frame); if (!u_background) { - opacity *= frame; + vec2 shadowUV = imageUV + vec2(-.01, -.015); + float shadowFrame = getUvFrame(shadowUV, .05); + float shadow = 1.2 * shadowFrame * (1. - frame); + opacity = opacity * frame + shadow; color *= frame; } + frame = mix(frame, 0., .2 * fade); + fragColor = vec4(color, opacity); } @@ -450,6 +479,8 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_folds: number; u_foldType: number; u_foldCount: number; + u_foldSize: number; + u_foldOffset: number; u_foldsShape: number; u_fade: number; u_crumpleSize: number; @@ -472,6 +503,8 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara folds?: number; foldType?: PaperTextureFoldType; foldCount?: number; + foldSize?: number; + foldOffset?: number; foldsShape?: number; fade?: number; crumpleSize?: number; @@ -482,9 +515,10 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara background?: boolean; } -export type PaperTextureFoldType = 'folds' | 'creases'; +export type PaperTextureFoldType = 'folds' | 'creases' | 'grid'; export const PaperTextureFoldTypes: Record = { folds: 0, creases: 1, + grid: 2, }; From 97272c8f9c79235a1be99b814f4daab5a2e6422a Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 28 Apr 2026 20:20:45 +0400 Subject: [PATCH 109/186] to share --- docs/src/app/(shaders)/paper-texture/page.tsx | 4 +- .../src/shaders/paper-texture.tsx | 77 ++++++++++--------- packages/shaders/src/shaders/paper-texture.ts | 65 ++++++++-------- 3 files changed, 77 insertions(+), 69 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 07787cac7..7e524fe46 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -28,7 +28,8 @@ const controlRules: ControlRules = { crumpleSize: { showWhen: { crumples: { gt: 0 } } }, foldType: { showWhen: { folds: { gt: 0 } } }, foldCount: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'folds' } }, label: '↳ count' }, - foldSize: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ size' }, + foldSize: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ size x' }, + foldY: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ horizontal' }, foldOffset: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ offset' }, foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ shape' }, }; @@ -99,6 +100,7 @@ const PaperTextureWithControls = () => { foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, + foldY: { value: defaults.foldY, order: 232.6 }, foldOffset: { value: defaults.foldOffset, min: 0, max: 1, order: 233 }, foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 234 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 7f307a5db..9793d33bc 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -44,10 +44,11 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleSize: 0.5, - folds: 1, - foldType: 'creases', - foldCount: 5, + folds: 0.7, + foldType: 'folds', + foldCount: 15, foldSize: 0.79, + foldY: false, foldOffset: 0, foldsShape: 1, fade: 0, @@ -61,7 +62,7 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - blending: 0.6, + blending: 0.9, distortion: 0.5, fit: 'contain', speed: 1, @@ -71,7 +72,6 @@ export const cardboardPreset: PaperTexturePreset = { colorFront: "#685327", roughness: 0.1, roughnessSize: 0.25, - fiber: 0.35, fiberSize: 0.14, crumples: 0., @@ -80,6 +80,7 @@ export const cardboardPreset: PaperTexturePreset = { foldType: 'creases', foldCount: 18, foldSize: 0.1, + foldY: false, foldOffset: 0, foldsShape: 0.28, fade: 0, @@ -94,63 +95,63 @@ export const spreadPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, colorBack: "#ffffff", - colorFront: "#c0c4c0", + colorFront: "#c4c4c4", blending: 1, distortion: 0.7, seed: 448, - roughness: 0.4, + roughness: 0.2, roughnessSize: 0.25, - - fiber: 0.1, + fiber: 0, fiberSize: 0.5, - crumples: 0.2, + crumples: 0, crumpleSize: 0.42, folds: 1, foldType: 'creases', foldCount: 1, foldSize: 1, + foldY: true, foldOffset: 0, - foldsShape: 0.2, + foldsShape: 1, drops: 0, fade: 0.68, - scale: 0.8, + scale: 0.95, fit: "contain", speed: 1, frame: 0, - background: true, + background: false, }, }; -export const flatPreset: PaperTexturePreset = { - name: 'Flat', +export const sheetPreset: PaperTexturePreset = { + name: 'Sheet', params: { ...defaultObjectSizing, - colorBack: '#fff3e0', - colorFront: '#9c9c9c', - blending: 0.9, - distortion: 0.33, - seed: 415, - roughness: 0.31, + blending: 0.8, + distortion: 0.5, + fit: 'contain', + scale: 0.8, + speed: 0, + frame: 0, + colorFront: '#c4c4c4', + colorBack: '#ffffff', + roughness: 0, roughnessSize: 0.25, - - fiber: 0, - fiberSize: 0.61, - crumples: 0.7, - crumpleSize: 0.2, - folds: 0, + fiber: 0.5, + fiberSize: 0.5, + crumples: 0, + crumpleSize: 0.5, + folds: 1, foldType: 'folds', - foldCount: 5, + foldCount: 10, foldSize: 0.79, + foldY: false, foldOffset: 0, - foldsShape: 0.30, - drops: 0.20, - fade: 0.75, - scale: 0.8, - fit: 'contain', - speed: 1, - frame: 0, - background: true, + foldsShape: 1, + fade: 1, + drops: 1, + seed: 784, + background: false, }, }; @@ -158,7 +159,7 @@ export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, spreadPreset, cardboardPreset, - flatPreset + sheetPreset ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ @@ -175,6 +176,7 @@ export const PaperTexture: React.FC = memo(function PaperText folds = defaultPreset.params.folds, foldType = defaultPreset.params.foldType, foldSize = defaultPreset.params.foldSize, + foldY = defaultPreset.params.foldY, foldOffset = defaultPreset.params.foldOffset, foldsShape = defaultPreset.params.foldsShape, drops = defaultPreset.params.drops, @@ -220,6 +222,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumpleSize: crumpleSize, u_foldCount: foldCount, u_foldSize: foldSize, + u_foldY: foldY, u_foldOffset: foldOffset, u_folds: folds, u_foldType: PaperTextureFoldTypes[foldType], diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 7999483af..3f06eb297 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -70,6 +70,7 @@ uniform float u_folds; uniform float u_foldType; uniform float u_foldCount; uniform float u_foldSize; +uniform bool u_foldY; uniform float u_foldsShape; uniform float u_foldOffset; uniform float u_drops; @@ -295,8 +296,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { ); } -vec4 getCrease(float coord, float offset) { - float count = mix(20., 1., u_foldSize); +vec4 getCrease(float coord, float offset, float count) { float g = (coord - 1.) * count + .5 * offset; float crX = fract(g); float creaseIdx = floor(g); @@ -355,39 +355,41 @@ void main() { vec2 fromCenter = imageUV - .5; scaleDistortion = .22 * radialFolds.z * u_folds; - } else if (u_foldType < 1.5) { - vec4 cr = getCrease((imageUV + .5).x, u_foldOffset); - float angle = cr.x * 1.1345; - float crLight = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., cr.y); - drops *= mix(1., cr.y, u_folds); - - pattern += u_folds * crLight; - float distortBase = mix(pow(cr.y, .2), cr.z, .5 * u_foldsShape); - yShift += .022 * (1. - distortBase); - patternUV.y += yShift; } else { vec2 uv = imageUV + .5; - vec4 h = getCrease(uv.x, u_foldOffset); - vec4 v = getCrease(uv.y, u_foldOffset); - float ax = h.x * 1.1345; - float ay = v.x * 1.1345; - float gridDark = h.y * v.y; - float gridLight = max(-.5 * sin(ax) - .5 * sin(ay) + .5 * cos(ax) * cos(ay), 0.) * mix(.9, 1., gridDark); - float gridDist = min(h.z, v.z); - drops *= mix(1., gridDark, u_folds); - - pattern += u_folds * gridLight; - float distortBaseH = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); - float distortBaseV = mix(pow(v.y, .2), v.z, .5 * u_foldsShape); - patternUV.x += .022 * (1. - distortBaseV); - yShift += .022 * (1. - distortBaseH); + float countX = mix(25., 1., pow(u_foldSize, .4)); + vec4 h = getCrease(uv.x, 1. - u_foldOffset, countX); + + if (u_foldY) { + vec4 v = getCrease(uv.y, 1., 1.); + float ax = h.x * 1.1345; + float ay = v.x * 1.1345; + float gridDark = h.y * v.y; + float crLight = max(-.7 * sin(ax) - .2 * sin(ay) + .5 * cos(ax) * cos(ay), 0.) * mix(.9, 1., gridDark); + drops *= mix(1., gridDark, u_folds); + + pattern += u_folds * crLight; + float distortBaseH = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); + float distortBaseV = mix(pow(v.y, .2), v.z, .5 * u_foldsShape); + xDistortion += .022 * u_folds * (1. - distortBaseV) * 2. * (imageUV.x - .5); + patternUV.x += .022 * u_folds * (1. - distortBaseV); + yShift += .022 * u_folds * (1. - distortBaseH); + } else { + float angle = h.x * 1.1345; + float crLight = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., h.y); + drops *= mix(1., h.y, u_folds); + + pattern += u_folds * crLight; + float distortBase = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); + yShift += .022 * u_folds * (1. - distortBase); + } patternUV.y += yShift; } } if (u_roughness > 0.) { float roughness = getRoughness(200. * patternUV); - roughness *= mix(1., .5, fade); + roughness *= mix(1., .3, fade); roughness *= u_roughness; pattern += roughness; radialDistortion += .02 * roughness; @@ -395,7 +397,7 @@ void main() { if (u_fiber > 0.) { float fiber = getFiber(10. * patternUV); - fiber *= mix(1., .5, fade); + fiber *= mix(1., .3, fade); fiber *= u_fiber; pattern += fiber; radialDistortion += .02 * fiber; @@ -454,7 +456,7 @@ void main() { if (!u_background) { vec2 shadowUV = imageUV + vec2(-.01, -.015); - float shadowFrame = getUvFrame(shadowUV, .05); + float shadowFrame = getUvFrame(shadowUV, .05) * texture(u_image, shadowUV).a; float shadow = 1.2 * shadowFrame * (1. - frame); opacity = opacity * frame + shadow; color *= frame; @@ -480,6 +482,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_foldType: number; u_foldCount: number; u_foldSize: number; + u_foldY: boolean; u_foldOffset: number; u_foldsShape: number; u_fade: number; @@ -504,6 +507,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara foldType?: PaperTextureFoldType; foldCount?: number; foldSize?: number; + foldY?: boolean; foldOffset?: number; foldsShape?: number; fade?: number; @@ -515,10 +519,9 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara background?: boolean; } -export type PaperTextureFoldType = 'folds' | 'creases' | 'grid'; +export type PaperTextureFoldType = 'folds' | 'creases'; export const PaperTextureFoldTypes: Record = { folds: 0, creases: 1, - grid: 2, }; From 193e732e514c78fffaf2fe071b7c5092461bd672 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Tue, 28 Apr 2026 20:22:15 +0400 Subject: [PATCH 110/186] preset tweaks --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 9793d33bc..33d1fe921 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -105,7 +105,7 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleSize: 0.42, - folds: 1, + folds: 0.7, foldType: 'creases', foldCount: 1, foldSize: 1, From 2339405efee65b670dcae32f313a0b2523a178ba Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 4 May 2026 19:54:46 +0400 Subject: [PATCH 111/186] crumples removed --- docs/src/app/(shaders)/paper-texture/page.tsx | 3 -- .../src/shaders/paper-texture.tsx | 15 ------ packages/shaders/src/shaders/paper-texture.ts | 51 ++----------------- 3 files changed, 3 insertions(+), 66 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 7e524fe46..c0e76329e 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -25,7 +25,6 @@ const { worldWidth, worldHeight, ...defaults } = paperTexturePresets[0].params; const controlRules: ControlRules = { roughnessSize: { showWhen: { roughness: { gt: 0 } } }, fiberSize: { showWhen: { fiber: { gt: 0 } } }, - crumpleSize: { showWhen: { crumples: { gt: 0 } } }, foldType: { showWhen: { folds: { gt: 0 } } }, foldCount: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'folds' } }, label: '↳ count' }, foldSize: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ size x' }, @@ -94,8 +93,6 @@ const PaperTextureWithControls = () => { roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, - crumples: { value: defaults.crumples, min: 0, max: 1, order: 220 }, - crumpleSize: { value: defaults.crumpleSize, min: 0, max: 1, order: 221 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 33d1fe921..e398d5168 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -16,8 +16,6 @@ import { export interface PaperTextureProps extends ShaderComponentProps, PaperTextureParams { /** @deprecated use `fiberSize` instead */ fiberScale?: number; - /** @deprecated use `crumpleSize` instead */ - crumplesScale?: number; /** @deprecated use `foldCount` instead */ foldsNumber?: number; /** @deprecated use `fade` instead */ @@ -42,8 +40,6 @@ export const defaultPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.5, fiberSize: 0.5, - crumples: 0, - crumpleSize: 0.5, folds: 0.7, foldType: 'folds', foldCount: 15, @@ -74,8 +70,6 @@ export const cardboardPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.35, fiberSize: 0.14, - crumples: 0., - crumpleSize: 0.1, folds: 0.25, foldType: 'creases', foldCount: 18, @@ -103,8 +97,6 @@ export const spreadPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, - crumples: 0, - crumpleSize: 0.42, folds: 0.7, foldType: 'creases', foldCount: 1, @@ -139,8 +131,6 @@ export const sheetPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.5, fiberSize: 0.5, - crumples: 0, - crumpleSize: 0.5, folds: 1, foldType: 'folds', foldCount: 10, @@ -172,7 +162,6 @@ export const PaperTexture: React.FC = memo(function PaperText roughness = defaultPreset.params.roughness, roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, - crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, foldType = defaultPreset.params.foldType, foldSize = defaultPreset.params.foldSize, @@ -188,8 +177,6 @@ export const PaperTexture: React.FC = memo(function PaperText // Reworked props fiberScale, fiberSize = fiberScale === undefined ? defaultPreset.params.fiberSize : 0.2 / fiberScale, - crumplesScale, - crumpleSize = crumplesScale === undefined ? defaultPreset.params.crumpleSize : 0.2 / crumplesScale, blur, fade = blur === undefined ? defaultPreset.params.fade : blur, foldsNumber, @@ -218,8 +205,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_roughnessSize: roughnessSize, u_fiber: fiber, u_fiberSize: fiberSize, - u_crumples: crumples, - u_crumpleSize: crumpleSize, u_foldCount: foldCount, u_foldSize: foldSize, u_foldY: foldY, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 3f06eb297..c98636f36 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -19,14 +19,12 @@ export const paperTextureMeta = { * - u_roughnessSize (float): Scale of the roughness noise (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) * - u_fiberSize (float): Curly-shaped noise scale (0 to 1) - * - u_crumples (float): Cell-based crumple pattern intensity (0 to 1) - * - u_crumpleSize (float): Cell-based crumple pattern scale (0 to 1) * - u_folds (float): Depth of the folds (0 to 1) * - u_foldType (float): Type of folds pattern (0 = radial folds, 1 = creases) * - u_foldCount (float): Size/frequency of folds or creases (0 to 1) * - u_foldsShape (float): Shape/width of the folds (0 to 1) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) - * - u_seed (float): Seed applied to folds, crumples and dots (0 to 1000) + * - u_seed (float): Seed applied to folds and dots (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) * - u_blending (float): Amount of image-to-paper blending, 0 for original color, 1 for mix with colorBack (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) @@ -64,8 +62,6 @@ uniform float u_imageAspectRatio; uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; -uniform float u_crumples; -uniform float u_crumpleSize; uniform float u_folds; uniform float u_foldType; uniform float u_foldCount; @@ -224,31 +220,6 @@ vec2 randomGB(vec2 p) { return texture(u_noiseTexture, fract(uv)).gb; } -float getCrumples(vec2 uv) { - vec2 t = uv * .5; - vec2 p = floor(t); - vec2 wsum = vec2(0.), cl = vec2(0.); - for (int y = -1; y < 2; y += 1) { - for (int x = -1; x < 2; x += 1) { - vec2 b = vec2(float(x), float(y)); - vec2 q = b + p; - vec2 qm = mod(q, 8.); - vec2 c = q + randomGB(qm + u_seed); - float val = .5 + .5 * sin((qm.x + qm.y * 5.) * 8.); - vec2 r = c - t; - float wy = sst(0., 1., 1. - abs(r.y)); - wy *= wy; wy *= wy; wy *= wy; wy *= wy; // pow16 - vec2 w = smoothstep(0., 1., 1. - abs(vec2(r.x, r.x - .0125))); - w *= w; w *= w; w *= w; w *= w; // pow16 - w *= wy; - cl += val * w; - wsum += w; - } - } - vec2 n = cl / wsum; - return 4. * (n.y - n.x); -} - float getDrops(vec2 uv) { vec2 iDropsUV = floor(uv); vec2 fDropsUV = fract(uv); @@ -323,13 +294,9 @@ void main() { vec2 patternUV = v_imageUV - .5; patternUV *= 5. * vec2(u_imageAspectRatio, 1.); - float ySide = (imageUV.y - .5); - float ySidePower = sign(ySide) * mix(1., 1.25, step(0., ySide)) * abs(ySide); - float pattern = 0.; float radialDistortion = 0.; float xDistortion = 0.; - float yDistortion = 0.; float yShift = 0.; float scaleDistortion = 0.; @@ -403,14 +370,6 @@ void main() { radialDistortion += .02 * fiber; } - if (u_crumples > 0.) { - vec2 crumplesUV = mix(14.4, .64, pow(u_crumpleSize, .3)) * patternUV - 32. * u_seed; - float crumples = clamp(.2 + getCrumples(crumplesUV), 0., 1.); - crumples = mix(crumples, 0., fade); - pattern += u_crumples * crumples; - yDistortion -= .01 * u_crumples * (crumples - .25); - } - if (u_drops > 0.) { drops *= u_drops; pattern += drops; @@ -424,12 +383,12 @@ void main() { imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); imageUV.x += u_distortion * xDistortion; - imageUV.y -= u_distortion * (yDistortion * ySidePower + yShift); + imageUV.y -= u_distortion * yShift; vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness + .2 * u_crumples); + float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); frame *= image.a; @@ -477,7 +436,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_roughnessSize: number; u_fiber: number; u_fiberSize: number; - u_crumples: number; u_folds: number; u_foldType: number; u_foldCount: number; @@ -486,7 +444,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_foldOffset: number; u_foldsShape: number; u_fade: number; - u_crumpleSize: number; u_drops: number; u_seed: number; u_blending: number; @@ -502,7 +459,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara roughnessSize?: number; fiber?: number; fiberSize?: number; - crumples?: number; folds?: number; foldType?: PaperTextureFoldType; foldCount?: number; @@ -511,7 +467,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara foldOffset?: number; foldsShape?: number; fade?: number; - crumpleSize?: number; drops?: number; seed?: number; blending?: number; From 9c66c16972e2838a61b45ead18e9672ab48577d3 Mon Sep 17 00:00:00 2001 From: Ksenia Kondrashova Date: Mon, 4 May 2026 20:01:51 +0400 Subject: [PATCH 112/186] shadow removed --- packages/shaders/src/shaders/paper-texture.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index c98636f36..e1ff6f5cc 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -173,7 +173,7 @@ float getRoughness(vec2 p) { } float getFiber(vec2 p) { - float size = mix(2., .4, u_fiberSize); + float size = mix(4.5, 1.2, u_fiberSize); float w = max(length(dFdx(p)), length(dFdy(p))); float level = -log2(w + 1e-8); float baseLevel = floor(level) - 3.; @@ -414,10 +414,7 @@ void main() { color = mix(color, pic, frame); if (!u_background) { - vec2 shadowUV = imageUV + vec2(-.01, -.015); - float shadowFrame = getUvFrame(shadowUV, .05) * texture(u_image, shadowUV).a; - float shadow = 1.2 * shadowFrame * (1. - frame); - opacity = opacity * frame + shadow; + opacity *= frame; color *= frame; } frame = mix(frame, 0., .2 * fade); From 625af59980115432a2df0723d03041c394b2dad4 Mon Sep 17 00:00:00 2001 From: ksenia Date: Thu, 9 Jul 2026 19:49:02 +0400 Subject: [PATCH 113/186] more on conditional params --- docs/src/app/(shaders)/paper-texture/page.tsx | 31 +++++++---- docs/src/helpers/leva-control-rules.ts | 55 ++++++++----------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index c0e76329e..e705b6200 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -23,14 +23,14 @@ import { useUrlParams } from '@/helpers/use-url-params'; const { worldWidth, worldHeight, ...defaults } = paperTexturePresets[0].params; const controlRules: ControlRules = { - roughnessSize: { showWhen: { roughness: { gt: 0 } } }, - fiberSize: { showWhen: { fiber: { gt: 0 } } }, - foldType: { showWhen: { folds: { gt: 0 } } }, - foldCount: { showWhen: { folds: { gt: 0 }, foldType: { eq: 'folds' } }, label: '↳ count' }, - foldSize: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ size x' }, - foldY: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ horizontal' }, - foldOffset: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ offset' }, - foldsShape: { showWhen: { folds: { gt: 0 }, foldType: { neq: 'folds' } }, label: '↳ shape' }, + roughnessSize: { showWhen: { roughness: { moreThan: 0 } } }, + fiberSize: { showWhen: { fiber: { moreThan: 0 } } }, + foldType: { showWhen: { folds: { moreThan: 0 } } }, + foldCount: { showWhen: { folds: { moreThan: 0 }, foldType: { is: 'folds' } }, label: '↳ count' }, + foldSize: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ size x' }, + foldY: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ horizontal' }, + foldOffset: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ offset' }, + foldsShape: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ shape' }, }; const imageFiles = [ @@ -83,18 +83,26 @@ const PaperTextureWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); + const hasImage = image !== ''; return applyControlRules( { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - blending: { value: defaults.blending, min: 0, max: 1, order: 198 }, - distortion: { value: defaults.distortion, min: -1, max: 1, order: 199 }, + ...(hasImage && { + blending: { value: defaults.blending, min: 0, max: 1, order: 150 }, + distortion: { value: defaults.distortion, min: -1, max: 1, order: 151 }, + background: { value: defaults.background, order: 152 }, + }), roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, - foldType: { value: defaults.foldType, options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], order: 231 }, + foldType: { + value: defaults.foldType, + options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], + order: 231, + }, foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, foldY: { value: defaults.foldY, order: 232.6 }, @@ -103,7 +111,6 @@ const PaperTextureWithControls = () => { drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, - background: { value: defaults.background, order: 270 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/docs/src/helpers/leva-control-rules.ts b/docs/src/helpers/leva-control-rules.ts index 6571a6704..40e2c69fa 100644 --- a/docs/src/helpers/leva-control-rules.ts +++ b/docs/src/helpers/leva-control-rules.ts @@ -1,53 +1,46 @@ -/** - * Conditional visibility rules for Leva controls. - * - * Each rule maps a control name to the conditions under which it should be - * visible. Conditions reference other controls by name and support simple - * comparison operators. - */ +/** Declarative conditional-visibility rules for Leva controls. */ export type ControlCondition = - | { gt: number } - | { eq: string | number } - | { neq: string | number }; + | { moreThan: number } + | { is: string | number } + | { isNot: string | number }; export type ControlRule = { - /** All conditions must be met for the control to be shown. */ + /** Control is shown only when every dep control satisfies its condition. */ showWhen: Record; - /** Custom label override. Defaults to "↳ {controlName}". */ + /** Label override. Defaults to the control's own label, else "↳ {name}". */ label?: string; }; export type ControlRules = Record; function evaluateCondition(value: unknown, condition: ControlCondition): boolean { - if ('gt' in condition) return typeof value === 'number' && value > condition.gt; - if ('eq' in condition) return value === condition.eq; - if ('neq' in condition) return value !== condition.neq; - return true; + if ('moreThan' in condition) return typeof value === 'number' && value > condition.moreThan; + if ('is' in condition) return value === condition.is; + return value !== condition.isNot; } -/** - * Adds Leva `render` callbacks to controls based on declarative rules. - * Controls not mentioned in `rules` are returned unchanged. - */ +/** Adds Leva `render` callbacks from `rules`; controls not in `rules` pass through. */ export function applyControlRules( schema: Record, rules: ControlRules ): Record { const result: Record = { ...schema }; - for (const [controlName, rule] of Object.entries(rules)) { - if (result[controlName] != null && typeof result[controlName] === 'object') { - result[controlName] = { - ...result[controlName], - label: rule.label ?? `↳ ${controlName}`, - render: (get: (key: string) => unknown) => - Object.entries(rule.showWhen).every(([dep, condition]) => - evaluateCondition(get(dep), condition) - ), - }; - } + for (const [name, rule] of Object.entries(rules)) { + const control = result[name]; + if (control == null || typeof control !== 'object') continue; + + const passesRule = (get: (key: string) => unknown) => + Object.entries(rule.showWhen).every(([dep, condition]) => evaluateCondition(get(dep), condition)); + + result[name] = { + ...control, + label: rule.label ?? control.label ?? `↳ ${name}`, + render: control.render + ? (get: (key: string) => unknown) => control.render(get) && passesRule(get) + : passesRule, + }; } return result; From 5a8b853b5207543f0855b4424cc812e6dac7f63d Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 10 Jul 2026 16:53:59 +0400 Subject: [PATCH 114/186] colorBack #00000000 support --- packages/shaders/src/shaders/paper-texture.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index e1ff6f5cc..6eec8681e 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -241,7 +241,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { vec3 pp1 = vec3(0.), pp2 = vec3(0.); float l1 = 9., l2 = 9.; float cellRand = 0.; - for (int i = 0; i < ${ paperTextureMeta.maxFoldCount }; i++) { + for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; @@ -258,10 +258,9 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { cellRand = .5 * (rand.x + rand.y); } } - float mult2 = mix(.22, .02, 1.); return vec4( - mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), - mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)), + mix(pp1.x, .17 * pp1.z, pow(pp1.y, .02)), + mix(pp2.x, .18 * pp2.z, pow(pp2.y, .02)), .2 * pp2.y, cellRand ); @@ -393,11 +392,10 @@ void main() { vec4 image = texture(u_image, imageUV); frame *= image.a; - vec3 color = fgColor * pattern; - float opacity = fgOpacity * pattern; + pattern = clamp(.5 * pattern, 0., 1.); - color += bgColor * (1. - opacity); - opacity += bgOpacity * (1. - opacity); + vec3 color = mix(fgColor, bgColor, pattern); + float opacity = mix(fgOpacity, bgOpacity, pattern); float maxC = max(max(image.r, image.g), image.b); float minC = min(min(image.r, image.g), image.b); @@ -417,8 +415,6 @@ void main() { opacity *= frame; color *= frame; } - frame = mix(frame, 0., .2 * fade); - fragColor = vec4(color, opacity); } From cd8be99e2427fde6d9142c4f7632c752d6f1c751 Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 10 Jul 2026 16:59:13 +0400 Subject: [PATCH 115/186] colorBack #00000000 support (full) --- packages/shaders/src/shaders/paper-texture.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6eec8681e..868d9c150 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -376,8 +376,8 @@ void main() { } vec3 fgColor = u_colorFront.rgb * u_colorFront.a; - float fgOpacity = u_colorFront.a; vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + float fgOpacity = u_colorFront.a; float bgOpacity = u_colorBack.a; imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); @@ -394,8 +394,11 @@ void main() { pattern = clamp(.5 * pattern, 0., 1.); - vec3 color = mix(fgColor, bgColor, pattern); - float opacity = mix(fgOpacity, bgOpacity, pattern); + // old premultiplied paper — shown outside the image frame + vec3 paperColor = mix(fgColor, bgColor, pattern); + float paperOpacity = mix(fgOpacity, bgOpacity, pattern); + // straight hue — used for the image multiply inside the frame + vec3 tint = mix(u_colorFront.rgb, u_colorBack.rgb, pattern); float maxC = max(max(image.r, image.g), image.b); float minC = min(min(image.r, image.g), image.b); @@ -406,10 +409,12 @@ void main() { float darkDampen = 1. - dot(vec3(.2126, .7152, .0722), image.rgb); float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); - vec3 pic = blendMultiply(image.rgb, color, u_blending); + vec3 pic = blendMultiply(image.rgb, tint, u_blending); pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); - color = mix(color, pic, frame); + // inside the frame: new tinted image (opaque); outside: old premultiplied paper + vec3 color = mix(paperColor, pic, frame); + float opacity = mix(paperOpacity, 1., frame); if (!u_background) { opacity *= frame; From d5d1d51b2891db95cf6bac1524bd30b59a3836c4 Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 10 Jul 2026 17:12:57 +0400 Subject: [PATCH 116/186] presets update --- .../src/shaders/paper-texture.tsx | 22 +++++++++---------- packages/shaders/src/shaders/paper-texture.ts | 7 +++--- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e398d5168..5d17c08d4 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -28,14 +28,14 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - blending: 0.8, + blending: 1, distortion: 0.5, fit: 'contain', scale: 0.8, speed: 0, frame: 0, - colorFront: '#c4c4c4', - colorBack: '#ffffff', + colorFront: '#ffffff', + colorBack: '#b3b3b3', roughness: 0, roughnessSize: 0.25, fiber: 0.5, @@ -58,7 +58,7 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - blending: 0.9, + blending: 0.7, distortion: 0.5, fit: 'contain', speed: 1, @@ -93,7 +93,7 @@ export const spreadPreset: PaperTexturePreset = { blending: 1, distortion: 0.7, seed: 448, - roughness: 0.2, + roughness: 0.6, roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, @@ -115,8 +115,8 @@ export const spreadPreset: PaperTexturePreset = { }; -export const sheetPreset: PaperTexturePreset = { - name: 'Sheet', +export const coloredPreset: PaperTexturePreset = { + name: 'Colored', params: { ...defaultObjectSizing, blending: 0.8, @@ -125,8 +125,8 @@ export const sheetPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#c4c4c4', - colorBack: '#ffffff', + colorFront: '#ffe0e0', + colorBack: '#7e91b9', roughness: 0, roughnessSize: 0.25, fiber: 0.5, @@ -141,7 +141,7 @@ export const sheetPreset: PaperTexturePreset = { fade: 1, drops: 1, seed: 784, - background: false, + background: true, }, }; @@ -149,7 +149,7 @@ export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, spreadPreset, cardboardPreset, - sheetPreset + coloredPreset, ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 868d9c150..cdcc1470c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -96,8 +96,8 @@ float sst(float edge0, float edge1, float x) { return smoothstep(edge0, edge1, x); } -${ declarePI } -${ rotation2 } +${declarePI} +${rotation2} float randomR(vec2 p) { vec2 uv = floor(p) / 100. + .5; return texture(u_noiseTexture, fract(uv)).r; @@ -392,7 +392,7 @@ void main() { vec4 image = texture(u_image, imageUV); frame *= image.a; - pattern = clamp(.5 * pattern, 0., 1.); + pattern = clamp(.8 * pattern, 0., 1.); // old premultiplied paper — shown outside the image frame vec3 paperColor = mix(fgColor, bgColor, pattern); @@ -412,7 +412,6 @@ void main() { vec3 pic = blendMultiply(image.rgb, tint, u_blending); pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); - // inside the frame: new tinted image (opaque); outside: old premultiplied paper vec3 color = mix(paperColor, pic, frame); float opacity = mix(paperOpacity, 1., frame); From 328cf153296b624b6fee2e09df45ed91e4647a73 Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 10 Jul 2026 18:33:46 +0400 Subject: [PATCH 117/186] back to fixing the color mix --- packages/shaders/src/shaders/paper-texture.ts | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index cdcc1470c..c5ae43791 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -26,7 +26,7 @@ export const paperTextureMeta = { * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds and dots (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) - * - u_blending (float): Amount of image-to-paper blending, 0 for original color, 1 for mix with colorBack (0 to 1) + * - u_blending (float): How much the image gives way to the paper pattern behind it; 0 = exact image, 1 = pattern reads through the image's bright/saturated areas (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) * - u_background (bool): Shows or hides the paper texture outside the image frame * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture @@ -279,13 +279,6 @@ vec4 getCrease(float coord, float offset, float count) { return vec4(slope, creaseDark, abs(dx), foldAmount); } -vec3 blendMultiply(vec3 base, vec3 blend) { - return base * blend; -} -vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { - return blendMultiply(base, blend) * opacity + base * (1. - opacity); -} - void main() { vec2 imageUV = v_imageUV; @@ -375,11 +368,12 @@ void main() { xDistortion += .03 * drops; } - vec3 fgColor = u_colorFront.rgb * u_colorFront.a; - vec3 bgColor = u_colorBack.rgb * u_colorBack.a; - float fgOpacity = u_colorFront.a; + vec3 bgColor = u_colorBack.rgb; float bgOpacity = u_colorBack.a; + vec3 fgColor = u_colorFront.rgb; + float fgOpacity = u_colorFront.a; + imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); imageUV.x += u_distortion * xDistortion; imageUV.y -= u_distortion * yShift; @@ -390,15 +384,10 @@ void main() { float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness); float frame = getUvFrame(imageUV, frameSoftness); vec4 image = texture(u_image, imageUV); - frame *= image.a; - pattern = clamp(.8 * pattern, 0., 1.); + pattern = clamp(.75 * pattern, 0., 1.); - // old premultiplied paper — shown outside the image frame - vec3 paperColor = mix(fgColor, bgColor, pattern); - float paperOpacity = mix(fgOpacity, bgOpacity, pattern); - // straight hue — used for the image multiply inside the frame - vec3 tint = mix(u_colorFront.rgb, u_colorBack.rgb, pattern); + float patternAlpha = fgOpacity * pattern; float maxC = max(max(image.r, image.g), image.b); float minC = min(min(image.r, image.g), image.b); @@ -406,18 +395,24 @@ void main() { float midC = image.r + image.g + image.b - maxC - minC; float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; float satDampen = sat * (1. - .5 * secondaryness); - float darkDampen = 1. - dot(vec3(.2126, .7152, .0722), image.rgb); - float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); + float lum = dot(vec3(.2126, .7152, .0722), image.rgb); + float lumFactor = mix(.45, .8, lum); + float reveal = u_blending * max(satDampen, lumFactor); + + vec3 blendedImage = mix(image.rgb, fgColor, reveal * patternAlpha); + + float imageFootprint = frame * image.a; + float imageAlpha = imageFootprint * (1. - reveal); - vec3 pic = blendMultiply(image.rgb, tint, u_blending); - pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); + vec3 overlay = blendedImage * imageAlpha + fgColor * patternAlpha * (1. - imageAlpha); + float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); - vec3 color = mix(paperColor, pic, frame); - float opacity = mix(paperOpacity, 1., frame); + vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); + float opacity = overlayAlpha + bgOpacity * (1. - overlayAlpha); if (!u_background) { - opacity *= frame; - color *= frame; + color *= imageFootprint; + opacity *= imageFootprint; } fragColor = vec4(color, opacity); From d081c1fc6731a17a4db67cf20d2675597872febc Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 12 Jul 2026 09:43:34 +0400 Subject: [PATCH 118/186] only FG color to use for blending, better hor shape, 2-dir offset --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- .../src/shaders/paper-texture.tsx | 32 ++++---- packages/shaders/src/shaders/paper-texture.ts | 78 +++++++++++-------- 3 files changed, 61 insertions(+), 51 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index e705b6200..18de79cf0 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -106,7 +106,7 @@ const PaperTextureWithControls = () => { foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, foldY: { value: defaults.foldY, order: 232.6 }, - foldOffset: { value: defaults.foldOffset, min: 0, max: 1, order: 233 }, + foldOffset: { value: defaults.foldOffset, min: -0.5, max: 0.5, order: 233 }, foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 234 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 5d17c08d4..3da9be578 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -34,8 +34,8 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#ffffff', - colorBack: '#b3b3b3', + colorFront: '#b3b3b3', + colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, fiber: 0.5, @@ -64,21 +64,21 @@ export const cardboardPreset: PaperTexturePreset = { speed: 1, frame: 0, scale: 0.8, - colorBack: "#dcc593", - colorFront: "#685327", + colorBack: '#c3ac79', + colorFront: '#111111', roughness: 0.1, - roughnessSize: 0.25, + roughnessSize: 0.65, fiber: 0.35, fiberSize: 0.14, folds: 0.25, foldType: 'creases', foldCount: 18, - foldSize: 0.1, + foldSize: 0.45, foldY: false, foldOffset: 0, - foldsShape: 0.28, + foldsShape: 0.55, fade: 0, - drops: 0.1, + drops: 0.2, seed: 1.6, background: true, }, @@ -88,12 +88,12 @@ export const spreadPreset: PaperTexturePreset = { name: 'Spread', params: { ...defaultObjectSizing, - colorBack: "#ffffff", - colorFront: "#c4c4c4", - blending: 1, - distortion: 0.7, + colorBack: '#ffffff', + colorFront: '#9d9e9f', + blending: 0.6, + distortion: -0.8, seed: 448, - roughness: 0.6, + roughness: 0.3, roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, @@ -107,7 +107,7 @@ export const spreadPreset: PaperTexturePreset = { drops: 0, fade: 0.68, scale: 0.95, - fit: "contain", + fit: 'contain', speed: 1, frame: 0, background: false, @@ -125,8 +125,8 @@ export const coloredPreset: PaperTexturePreset = { scale: 0.8, speed: 0, frame: 0, - colorFront: '#ffe0e0', - colorBack: '#7e91b9', + colorFront: '#7155b9', + colorBack: '#ffe0e0', roughness: 0, roughnessSize: 0.25, fiber: 0.5, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index c5ae43791..27cc25509 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -26,7 +26,7 @@ export const paperTextureMeta = { * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds and dots (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) - * - u_blending (float): How much the image gives way to the paper pattern behind it; 0 = exact image, 1 = pattern reads through the image's bright/saturated areas (0 to 1) + * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) * - u_background (bool): Shows or hides the paper texture outside the image frame * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture @@ -169,7 +169,7 @@ float getRoughness(vec2 p) { vec2 r = sum / norm; float dx = .5 + r.x - r.y; - return 3. * dx * dx; + return 3. * dx * dx - .7; } float getFiber(vec2 p) { @@ -212,7 +212,7 @@ float getFiber(vec2 p) { amp *= .5; } - return min(1., .5 * length(grad)); + return .5 * length(grad) - .5; } vec2 randomGB(vec2 p) { @@ -276,7 +276,15 @@ vec4 getCrease(float coord, float offset, float count) { float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * depthMod; float slope = sign(dx) * foldAmount; float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); - return vec4(slope, creaseDark, abs(dx), foldAmount); + // .z is the signed in-cell position: 0 at the crease, negative on one side and positive on the other. + return vec4(slope, creaseDark, dx, foldAmount); +} + +vec3 blendMultiply(vec3 base, vec3 blend) { + return base * blend; +} +vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { + return blendMultiply(base, blend) * opacity + base * (1. - opacity); } void main() { @@ -304,7 +312,7 @@ void main() { if (u_folds > 0.) { if (u_foldType < .5) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); + vec2 foldsUV2 = foldsUV1 + .03 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .015 + u_seed)).rg - .5); vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); @@ -316,33 +324,30 @@ void main() { scaleDistortion = .22 * radialFolds.z * u_folds; } else { vec2 uv = imageUV + .5; - float countX = mix(25., 1., pow(u_foldSize, .4)); - vec4 h = getCrease(uv.x, 1. - u_foldOffset, countX); + float count = mix(25., 1., pow(u_foldSize, .4)); + float offset = 1. - u_foldOffset; + + // Normal creases: run vertically (vary along x), displacing the image in y. + vec4 h = getCrease(uv.x, offset, count); + float angleX = h.x * 1.1345; + float crLightX = max(-.5 * sin(angleX) + .5 * cos(angleX), 0.) * mix(.9, 1., h.y); + drops *= mix(1., h.y, u_folds); + pattern += u_folds * crLightX; + float distortBaseX = mix(pow(h.y, .2), abs(h.z), .5 * u_foldsShape); + yShift += .022 * u_folds * (1. - distortBaseX); + patternUV.y += yShift; if (u_foldY) { vec4 v = getCrease(uv.y, 1., 1.); - float ax = h.x * 1.1345; - float ay = v.x * 1.1345; - float gridDark = h.y * v.y; - float crLight = max(-.7 * sin(ax) - .2 * sin(ay) + .5 * cos(ax) * cos(ay), 0.) * mix(.9, 1., gridDark); - drops *= mix(1., gridDark, u_folds); - - pattern += u_folds * crLight; - float distortBaseH = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); - float distortBaseV = mix(pow(v.y, .2), v.z, .5 * u_foldsShape); - xDistortion += .022 * u_folds * (1. - distortBaseV) * 2. * (imageUV.x - .5); - patternUV.x += .022 * u_folds * (1. - distortBaseV); - yShift += .022 * u_folds * (1. - distortBaseH); - } else { - float angle = h.x * 1.1345; - float crLight = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., h.y); - drops *= mix(1., h.y, u_folds); - - pattern += u_folds * crLight; - float distortBase = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); - yShift += .022 * u_folds * (1. - distortBase); + float angleY = v.x * 1.1345; + float crLightY = max(-.5 * sin(angleY) + .5 * cos(angleY), 0.) * mix(.9, 1., v.y); + drops *= mix(1., v.y, u_folds); + pattern += u_folds * crLightY; + float distortBaseY = mix(pow(v.y, .2), abs(v.z), .5 * u_foldsShape); + float xFan = 2. * (imageUV.x - .5); + xDistortion -= .02 * u_folds * (1. - distortBaseY) * xFan; + patternUV.x -= .022 * u_folds * (1. - distortBaseY) * xFan; } - patternUV.y += yShift; } } @@ -395,16 +400,21 @@ void main() { float midC = image.r + image.g + image.b - maxC - minC; float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; float satDampen = sat * (1. - .5 * secondaryness); + float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - float lumFactor = mix(.45, .8, lum); - float reveal = u_blending * max(satDampen, lumFactor); - vec3 blendedImage = mix(image.rgb, fgColor, reveal * patternAlpha); - + vec3 ink = mix(vec3(1.), fgColor, patternAlpha); + vec3 pic = blendMultiply(image.rgb, ink, u_blending); + + float darkDampen = 1. - lum; + float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); + pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); + pic = clamp(pic, 0., 1.); + float imageFootprint = frame * image.a; - float imageAlpha = imageFootprint * (1. - reveal); + float imageAlpha = imageFootprint; - vec3 overlay = blendedImage * imageAlpha + fgColor * patternAlpha * (1. - imageAlpha); + vec3 overlay = pic * imageAlpha + fgColor * patternAlpha * (1. - imageAlpha); float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); From 52b143bac6661e7cf93bef9f089592cfa912abe7 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 12 Jul 2026 17:32:01 +0400 Subject: [PATCH 119/186] conditional props to docs website --- docs/src/app/(shaders)/fluted-glass/page.tsx | 17 +++++--- docs/src/app/(shaders)/gem-smoke/page.tsx | 18 +++++--- docs/src/app/(shaders)/god-rays/page.tsx | 11 +++-- docs/src/app/(shaders)/halftone-dots/page.tsx | 13 ++++-- .../app/(shaders)/image-dithering/page.tsx | 19 ++++++--- .../src/app/(shaders)/pulsing-border/page.tsx | 9 +++- docs/src/app/(shaders)/spiral/page.tsx | 9 +++- .../(shaders)/static-mesh-gradient/page.tsx | 10 ++++- .../(shaders)/static-radial-gradient/page.tsx | 15 +++++-- docs/src/app/(shaders)/swirl/page.tsx | 9 +++- docs/src/app/(shaders)/voronoi/page.tsx | 31 +++++++++----- docs/src/app/(shaders)/warp/page.tsx | 11 +++-- docs/src/app/(shaders)/water/page.tsx | 11 +++-- docs/src/helpers/leva-control-rules.ts | 42 +++++++++++++++++++ docs/src/shader-defs/fluted-glass-def.ts | 6 +-- docs/src/shader-defs/gem-smoke-def.ts | 4 +- docs/src/shader-defs/god-rays-def.ts | 2 +- docs/src/shader-defs/halftone-dots-def.ts | 2 +- docs/src/shader-defs/image-dithering-def.ts | 6 +-- docs/src/shader-defs/spiral-def.ts | 2 +- .../shader-defs/static-mesh-gradient-def.ts | 4 +- docs/src/shader-defs/swirl-def.ts | 2 +- docs/src/shader-defs/voronoi-def.ts | 2 +- docs/src/shader-defs/water-def.ts | 2 +- 24 files changed, 188 insertions(+), 69 deletions(-) create mode 100644 docs/src/helpers/leva-control-rules.ts diff --git a/docs/src/app/(shaders)/fluted-glass/page.tsx b/docs/src/app/(shaders)/fluted-glass/page.tsx index 1852fa7b8..996e5ebcb 100644 --- a/docs/src/app/(shaders)/fluted-glass/page.tsx +++ b/docs/src/app/(shaders)/fluted-glass/page.tsx @@ -14,9 +14,16 @@ import { ShaderDetails } from '@/components/shader-details'; import { flutedGlassDef } from '@/shader-defs/fluted-glass-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = flutedGlassPresets[0].params; +const controlRules: ControlRules = { + colorShadow: { showWhen: { shadows: { moreThan: 0 } } }, + colorHighlight: { showWhen: { highlights: { moreThan: 0 } } }, + shift: { showWhen: { distortion: { moreThan: 0 } } }, +}; + const imageFiles = [ '001.webp', '002.webp', @@ -67,13 +74,13 @@ const FlutedGlassWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorShadow: { value: toHsla(defaults.colorShadow), order: 101 }, - colorHighlight: { value: toHsla(defaults.colorHighlight), order: 102 }, - size: { value: defaults.size, min: 0.01, max: 1, step: 0.01, order: 210 }, shadows: { value: defaults.shadows, min: 0, max: 1, order: 200 }, + colorShadow: { value: toHsla(defaults.colorShadow), order: 200.5 }, highlights: { value: defaults.highlights, min: 0, max: 1, order: 201 }, + colorHighlight: { value: toHsla(defaults.colorHighlight), order: 201.5 }, + size: { value: defaults.size, min: 0.01, max: 1, step: 0.01, order: 210 }, shape: { value: defaults.shape, options: Object.keys(GlassGridShapes) as GlassGridShape[], @@ -110,7 +117,7 @@ const FlutedGlassWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }; + }, controlRules); }); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/(shaders)/gem-smoke/page.tsx b/docs/src/app/(shaders)/gem-smoke/page.tsx index 7312f38e6..fd4b78edf 100644 --- a/docs/src/app/(shaders)/gem-smoke/page.tsx +++ b/docs/src/app/(shaders)/gem-smoke/page.tsx @@ -15,9 +15,15 @@ import { useUrlParams } from '@/helpers/use-url-params'; import { gemSmokeDef } from '@/shader-defs/gem-smoke-def'; import { toHsla } from '@/helpers/color-utils'; import { useColors } from '@/helpers/use-colors'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = gemSmokePresets[0].params; +const controlRules: ControlRules = { + outerDistortion: { showWhen: { outerGlow: { moreThan: 0 } } }, + innerDistortion: { showWhen: { innerGlow: { moreThan: 0 } } }, +}; + const imageFiles = [ 'contra.svg', 'apple.svg', @@ -66,7 +72,7 @@ const GemSmokeWithControls = () => { }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorInner: { value: toHsla(defaults.colorInner), order: 101 }, shape: { @@ -75,10 +81,10 @@ const GemSmokeWithControls = () => { order: 102, disabled: Boolean(image), }, - innerDistortion: { value: defaults.innerDistortion, min: 0, max: 1, order: 201 }, - outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 202 }, - outerGlow: { value: defaults.outerGlow, min: 0, max: 1, order: 203 }, - innerGlow: { value: defaults.innerGlow, min: 0, max: 1, order: 204 }, + outerGlow: { value: defaults.outerGlow, min: 0, max: 1, order: 201 }, + outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 201.5 }, + innerGlow: { value: defaults.innerGlow, min: 0, max: 1, order: 202 }, + innerDistortion: { value: defaults.innerDistortion, min: 0, max: 1, order: 202.5 }, offset: { value: defaults.offset, min: -1, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 250 }, size: { value: defaults.angle, min: 0.1, max: 1, order: 251 }, @@ -92,7 +98,7 @@ const GemSmokeWithControls = () => { 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), }, { order: -1 }), - }; + }, controlRules); }, [colors.length, image]); useControls(() => { diff --git a/docs/src/app/(shaders)/god-rays/page.tsx b/docs/src/app/(shaders)/god-rays/page.tsx index 219ed2f66..7a47cfc47 100644 --- a/docs/src/app/(shaders)/god-rays/page.tsx +++ b/docs/src/app/(shaders)/god-rays/page.tsx @@ -12,9 +12,14 @@ import { ShaderDetails } from '@/components/shader-details'; import { godRaysDef } from '@/shader-defs/god-rays-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = godRaysPresets[0].params; +const controlRules: ControlRules = { + colorBloom: { showWhen: { bloom: { moreThan: 0 } } }, +}; + const GodRaysWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -22,10 +27,10 @@ const GodRaysWithControls = () => { }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorBloom: { value: toHsla(defaults.colorBloom), order: 101 }, bloom: { value: defaults.bloom, min: 0, max: 1, order: 200 }, + colorBloom: { value: toHsla(defaults.colorBloom), order: 200.5 }, intensity: { value: defaults.intensity, min: 0, max: 1, order: 201 }, density: { value: defaults.density, min: 0, max: 1, order: 204 }, spotty: { value: defaults.spotty, min: 0, max: 1, order: 205 }, @@ -36,7 +41,7 @@ const GodRaysWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - }; + }, controlRules); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/halftone-dots/page.tsx b/docs/src/app/(shaders)/halftone-dots/page.tsx index 978ba5de5..3cc764fdf 100644 --- a/docs/src/app/(shaders)/halftone-dots/page.tsx +++ b/docs/src/app/(shaders)/halftone-dots/page.tsx @@ -19,9 +19,14 @@ import { ShaderDetails } from '@/components/shader-details'; import { halftoneDotsDef } from '@/shader-defs/halftone-dots-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = halftoneDotsPresets[0].params; +const controlRules: ControlRules = { + colorFront: { showWhen: { originalColors: { is: false } } }, +}; + const imageFiles = [ '001.webp', '002.webp', @@ -72,10 +77,10 @@ const HalftoneDotsWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - originalColors: { value: defaults.originalColors, order: 102 }, + originalColors: { value: defaults.originalColors, order: 101 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101.5 }, type: { value: defaults.type, options: Object.keys(HalftoneDotsTypes) as HalftoneDotsType[], @@ -107,7 +112,7 @@ const HalftoneDotsWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }; + }, controlRules); }); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/(shaders)/image-dithering/page.tsx b/docs/src/app/(shaders)/image-dithering/page.tsx index bb9e81dec..e8fa0f5be 100644 --- a/docs/src/app/(shaders)/image-dithering/page.tsx +++ b/docs/src/app/(shaders)/image-dithering/page.tsx @@ -13,9 +13,16 @@ import { ShaderDetails } from '@/components/shader-details'; import { imageDitheringDef } from '@/shader-defs/image-dithering-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = imageDitheringPresets[0].params; +const controlRules: ControlRules = { + colorBack: { showWhen: { originalColors: { is: false } } }, + colorFront: { showWhen: { originalColors: { is: false } } }, + colorHighlight: { showWhen: { originalColors: { is: false } } }, +}; + const imageFiles = [ '001.webp', '002.webp', @@ -66,11 +73,11 @@ const ImageDitheringWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 102 }, - colorHighlight: { value: toHsla(defaults.colorHighlight), order: 103 }, - originalColors: { value: defaults.originalColors, order: 104 }, + return applyControlRules({ + originalColors: { value: defaults.originalColors, order: 100 }, + colorBack: { value: toHsla(defaults.colorBack), order: 100.1 }, + colorFront: { value: toHsla(defaults.colorFront), order: 100.2 }, + colorHighlight: { value: toHsla(defaults.colorHighlight), order: 100.3 }, inverted: { value: defaults.inverted, order: 105 }, type: { value: defaults.type, options: Object.keys(DitheringTypes) as DitheringType[], order: 200 }, size: { value: defaults.size, min: 1, max: 20, order: 201 }, @@ -84,7 +91,7 @@ const ImageDitheringWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }; + }, controlRules); }); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/(shaders)/pulsing-border/page.tsx b/docs/src/app/(shaders)/pulsing-border/page.tsx index 81e20750d..1bf16ec44 100644 --- a/docs/src/app/(shaders)/pulsing-border/page.tsx +++ b/docs/src/app/(shaders)/pulsing-border/page.tsx @@ -12,16 +12,21 @@ import { ShaderDetails } from '@/components/shader-details'; import { pulsingBorderDef } from '@/shader-defs/pulsing-border-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = pulsingBorderPresets[0].params; +const controlRules: ControlRules = { + smokeSize: { showWhen: { smoke: { moreThan: 0 } } }, +}; + const PulsingBorderWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, maxColorCount: pulsingBorderMeta.maxColorCount, }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, roundness: { value: defaults.roundness, min: 0, max: 1, order: 200 }, thickness: { value: defaults.thickness, min: 0, max: 1, order: 201 }, @@ -54,7 +59,7 @@ const PulsingBorderWithControls = () => { marginRight: { value: defaults.marginRight, min: 0, max: 0.5, order: 403 }, marginTop: { value: defaults.marginTop, min: 0, max: 0.5, order: 403 }, marginBottom: { value: defaults.marginBottom, min: 0, max: 0.5, order: 403 }, - }; + }, controlRules); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/spiral/page.tsx b/docs/src/app/(shaders)/spiral/page.tsx index 79e1b127b..21307379c 100644 --- a/docs/src/app/(shaders)/spiral/page.tsx +++ b/docs/src/app/(shaders)/spiral/page.tsx @@ -10,6 +10,11 @@ import { ShaderDetails } from '@/components/shader-details'; import { spiralDef } from '@/shader-defs/spiral-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; + +const controlRules: ControlRules = { + noiseFrequency: { showWhen: { noise: { moreThan: 0 } } }, +}; const firstPresetParams = spiralPresets[0].params; const { worldWidth, worldHeight, ...defaults } = { @@ -21,7 +26,7 @@ const { worldWidth, worldHeight, ...defaults } = { const SpiralWithControls = () => { const [params, setParams] = useControls(() => { - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, density: { value: defaults.density, min: 0, max: 1, order: 200 }, @@ -37,7 +42,7 @@ const SpiralWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - }; + }, controlRules); }); useControls(() => { diff --git a/docs/src/app/(shaders)/static-mesh-gradient/page.tsx b/docs/src/app/(shaders)/static-mesh-gradient/page.tsx index e156d43bd..42d219087 100644 --- a/docs/src/app/(shaders)/static-mesh-gradient/page.tsx +++ b/docs/src/app/(shaders)/static-mesh-gradient/page.tsx @@ -11,9 +11,15 @@ import { ShaderDetails } from '@/components/shader-details'; import { staticMeshGradientDef } from '@/shader-defs/static-mesh-gradient-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = staticMeshGradientPresets[0].params; +const controlRules: ControlRules = { + waveXShift: { showWhen: { waveX: { moreThan: 0 } } }, + waveYShift: { showWhen: { waveY: { moreThan: 0 } } }, +}; + const StaticMeshGradientWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -21,7 +27,7 @@ const StaticMeshGradientWithControls = () => { }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ positions: { value: defaults.positions, min: 0, max: 100, order: 200 }, waveX: { value: defaults.waveX, min: 0, max: 1, order: 201 }, waveXShift: { value: defaults.waveXShift, min: 0, max: 1, order: 202 }, @@ -34,7 +40,7 @@ const StaticMeshGradientWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 301 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 302 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 303 }, - }; + }, controlRules); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/static-radial-gradient/page.tsx b/docs/src/app/(shaders)/static-radial-gradient/page.tsx index 23de248bd..16c8942ba 100644 --- a/docs/src/app/(shaders)/static-radial-gradient/page.tsx +++ b/docs/src/app/(shaders)/static-radial-gradient/page.tsx @@ -12,9 +12,16 @@ import { ShaderDetails } from '@/components/shader-details'; import { staticRadialGradientDef } from '@/shader-defs/static-radial-gradient-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = staticRadialGradientPresets[0].params; +const controlRules: ControlRules = { + focalAngle: { showWhen: { focalDistance: { moreThan: 0 } } }, + distortionShift: { showWhen: { distortion: { moreThan: 0 } } }, + distortionFreq: { showWhen: { distortion: { moreThan: 0 } } }, +}; + const StaticRadialGradientWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -22,12 +29,12 @@ const StaticRadialGradientWithControls = () => { }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, radius: { value: defaults.radius, min: 0, max: 3, order: 200 }, focalDistance: { value: defaults.focalDistance, min: 0, max: 3, order: 201 }, - focalAngle: { value: defaults.focalAngle, min: 0, max: 360, order: 202 }, - falloff: { value: defaults.falloff, min: -1, max: 1, order: 201 }, + focalAngle: { value: defaults.focalAngle, min: 0, max: 360, order: 201.5 }, + falloff: { value: defaults.falloff, min: -1, max: 1, order: 202 }, mixing: { value: defaults.mixing, min: 0, max: 1, order: 204 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 205 }, distortionShift: { value: defaults.distortionShift, min: -1, max: 1, order: 206 }, @@ -36,7 +43,7 @@ const StaticRadialGradientWithControls = () => { grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 209 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 300 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 301 }, - }; + }, controlRules); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/swirl/page.tsx b/docs/src/app/(shaders)/swirl/page.tsx index cd7c4e32f..2658165cc 100644 --- a/docs/src/app/(shaders)/swirl/page.tsx +++ b/docs/src/app/(shaders)/swirl/page.tsx @@ -11,9 +11,14 @@ import { ShaderDetails } from '@/components/shader-details'; import { swirlDef } from '@/shader-defs/swirl-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = swirlPresets[0].params; +const controlRules: ControlRules = { + noiseFrequency: { showWhen: { noise: { moreThan: 0 } } }, +}; + const SwirlWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -21,7 +26,7 @@ const SwirlWithControls = () => { }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, bandCount: { value: defaults.bandCount, min: 0, max: 15, step: 1, order: 200 }, twist: { value: defaults.twist, min: 0, max: 1, order: 201 }, @@ -35,7 +40,7 @@ const SwirlWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - }; + }, controlRules); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/voronoi/page.tsx b/docs/src/app/(shaders)/voronoi/page.tsx index 223590bed..9038f2165 100644 --- a/docs/src/app/(shaders)/voronoi/page.tsx +++ b/docs/src/app/(shaders)/voronoi/page.tsx @@ -12,9 +12,15 @@ import { ShaderDetails } from '@/components/shader-details'; import { voronoiDef } from '@/shader-defs/voronoi-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = voronoiPresets[0].params; +const controlRules: ControlRules = { + colorGap: { showWhen: { gap: { moreThan: 0 } } }, + colorGlow: { showWhen: { glow: { moreThan: 0 } } }, +}; + const VoronoiWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -22,17 +28,20 @@ const VoronoiWithControls = () => { }); const [params, setParams] = useControls(() => { - return { - colorGlow: { value: toHsla(defaults.colorGlow), order: 100 }, - colorGap: { value: toHsla(defaults.colorGap), order: 101 }, - stepsPerColor: { value: defaults.stepsPerColor, min: 1, max: 3, step: 1, order: 200 }, - distortion: { value: defaults.distortion, min: 0, max: 0.5, order: 201 }, - gap: { value: defaults.gap, min: 0, max: 0.1, order: 202 }, - glow: { value: defaults.glow, min: 0, max: 1, order: 203 }, - speed: { value: defaults.speed, min: 0, max: 1, order: 300 }, - scale: { value: defaults.scale, min: 0.01, max: 4, order: 301 }, - rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - }; + return applyControlRules( + { + stepsPerColor: { value: defaults.stepsPerColor, min: 1, max: 3, step: 1, order: 200 }, + distortion: { value: defaults.distortion, min: 0, max: 0.5, order: 201 }, + gap: { value: defaults.gap, min: 0, max: 0.1, order: 202 }, + colorGap: { value: toHsla(defaults.colorGap), order: 202.5 }, + glow: { value: defaults.glow, min: 0, max: 1, order: 203 }, + colorGlow: { value: toHsla(defaults.colorGlow), order: 203.5 }, + speed: { value: defaults.speed, min: 0, max: 1, order: 300 }, + scale: { value: defaults.scale, min: 0.01, max: 4, order: 301 }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, + }, + controlRules + ); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/warp/page.tsx b/docs/src/app/(shaders)/warp/page.tsx index 6ec92ae40..182c5ca31 100644 --- a/docs/src/app/(shaders)/warp/page.tsx +++ b/docs/src/app/(shaders)/warp/page.tsx @@ -11,9 +11,14 @@ import { ShaderDetails } from '@/components/shader-details'; import { warpDef } from '@/shader-defs/warp-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = warpPresets[0].params; +const controlRules: ControlRules = { + swirlIterations: { showWhen: { swirl: { moreThan: 0 } } }, +}; + const WarpWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -21,18 +26,18 @@ const WarpWithControls = () => { }); const [params, setParams] = useControls(() => { - return { + return applyControlRules({ 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 }, swirl: { value: defaults.swirl, min: 0, max: 1, order: 103 }, - swirlIterations: { value: defaults.swirlIterations, min: 0, max: 20, order: 104 }, + swirlIterations: { value: defaults.swirlIterations, min: 1, max: 20, step: 1, order: 104 }, shape: { value: defaults.shape, options: Object.keys(WarpPatterns) as WarpPattern[], order: 105 }, shapeScale: { value: defaults.shapeScale, min: 0, max: 1, order: 106 }, speed: { value: defaults.speed, min: 0, max: 20, order: 300 }, scale: { value: defaults.scale, min: 0.01, max: 5, order: 301 }, rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - }; + }, controlRules); }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/water/page.tsx b/docs/src/app/(shaders)/water/page.tsx index 30c3f6759..6af5d7981 100644 --- a/docs/src/app/(shaders)/water/page.tsx +++ b/docs/src/app/(shaders)/water/page.tsx @@ -13,9 +13,14 @@ import { ShaderDetails } from '@/components/shader-details'; import { waterDef } from '@/shader-defs/water-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; +import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = waterPresets[0].params; +const controlRules: ControlRules = { + colorHighlight: { showWhen: { highlights: { moreThan: 0 } } }, +}; + const imageFiles = [ '001.webp', '002.webp', @@ -66,10 +71,10 @@ const WaterWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return { + return applyControlRules({ colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorHighlight: { value: toHsla(defaults.colorHighlight), order: 101 }, highlights: { value: defaults.highlights, min: 0, max: 1, order: 200 }, + colorHighlight: { value: toHsla(defaults.colorHighlight), order: 200.5 }, layering: { value: defaults.layering, min: 0, max: 1, order: 201 }, edges: { value: defaults.edges, min: 0, max: 1, order: 202 }, waves: { value: defaults.waves, min: 0, max: 1, order: 203 }, @@ -86,7 +91,7 @@ const WaterWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }; + }, controlRules); }, [image]); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/helpers/leva-control-rules.ts b/docs/src/helpers/leva-control-rules.ts new file mode 100644 index 000000000..a84909165 --- /dev/null +++ b/docs/src/helpers/leva-control-rules.ts @@ -0,0 +1,42 @@ +export type ControlCondition = + | { moreThan: number } + | { is: string | number | boolean } + | { isNot: string | number | boolean }; + +export type ControlRule = { + showWhen: Record; +}; + +export type ControlRules = Record; + +function evaluateCondition(value: unknown, condition: ControlCondition): boolean { + if ('moreThan' in condition) return typeof value === 'number' && value > condition.moreThan; + if ('is' in condition) return value === condition.is; + return value !== condition.isNot; +} + +/** Adds Leva `render` callbacks from `rules`; controls not in `rules` pass through. */ +export function applyControlRules( + schema: Record, + rules: ControlRules +): Record { + const result: Record = { ...schema }; + + for (const [name, rule] of Object.entries(rules)) { + const control = result[name]; + if (control == null || typeof control !== 'object') continue; + + const passesRule = (get: (key: string) => unknown) => + Object.entries(rule.showWhen).every(([dep, condition]) => evaluateCondition(get(dep), condition)); + + result[name] = { + ...control, + label: `↳ ${name}`, + render: control.render + ? (get: (key: string) => unknown) => control.render(get) && passesRule(get) + : passesRule, + }; + } + + return result; +} \ No newline at end of file diff --git a/docs/src/shader-defs/fluted-glass-def.ts b/docs/src/shader-defs/fluted-glass-def.ts index 2465f470e..83606f65f 100644 --- a/docs/src/shader-defs/fluted-glass-def.ts +++ b/docs/src/shader-defs/fluted-glass-def.ts @@ -26,14 +26,14 @@ export const flutedGlassDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorShadow, isColor: true, - description: 'Shadows color', + description: 'Shadows color (effective with shadows > 0)', }, { name: 'colorHighlight', type: 'string', defaultValue: defaultParams.colorHighlight, isColor: true, - description: 'Highlights color', + description: 'Highlights color (effective with highlights > 0)', }, { name: 'shadows', @@ -96,7 +96,7 @@ export const flutedGlassDef: ShaderDef = { min: -1, max: 1, defaultValue: defaultParams.shift, - description: 'Texture shift in direction opposite to the grid', + description: 'Texture shift in direction opposite to the grid (effective with distortion > 0)', }, { name: 'stretch', diff --git a/docs/src/shader-defs/gem-smoke-def.ts b/docs/src/shader-defs/gem-smoke-def.ts index 040487c1e..40571a6ff 100644 --- a/docs/src/shader-defs/gem-smoke-def.ts +++ b/docs/src/shader-defs/gem-smoke-def.ts @@ -41,7 +41,7 @@ export const gemSmokeDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.innerDistortion, - description: 'The power of smoke distortion inside the input shape (shape defined by alpha channel)', + description: 'The power of smoke distortion inside the input shape (shape defined by alpha channel) (effective with innerGlow > 0)', }, { name: 'outerDistortion', @@ -49,7 +49,7 @@ export const gemSmokeDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.outerDistortion, - description: 'The power of smoke distortion outside the input shape (shape defined by alpha channel)', + description: 'The power of smoke distortion outside the input shape (shape defined by alpha channel) (effective with outerGlow > 0)', }, { name: 'outerGlow', diff --git a/docs/src/shader-defs/god-rays-def.ts b/docs/src/shader-defs/god-rays-def.ts index aa037ef30..0f6ed6233 100644 --- a/docs/src/shader-defs/god-rays-def.ts +++ b/docs/src/shader-defs/god-rays-def.ts @@ -28,7 +28,7 @@ export const godRaysDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBloom, isColor: true, - description: 'Color overlay blended with the rays', + description: 'Color overlay blended with the rays (effective with bloom > 0)', }, { name: 'bloom', diff --git a/docs/src/shader-defs/halftone-dots-def.ts b/docs/src/shader-defs/halftone-dots-def.ts index 82c7a741e..994f3fd74 100644 --- a/docs/src/shader-defs/halftone-dots-def.ts +++ b/docs/src/shader-defs/halftone-dots-def.ts @@ -25,7 +25,7 @@ export const halftoneDotsDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorFront, isColor: true, - description: 'Foreground color', + description: 'Foreground color (effective with originalColors off)', }, { name: 'originalColors', diff --git a/docs/src/shader-defs/image-dithering-def.ts b/docs/src/shader-defs/image-dithering-def.ts index 868497d6e..cbba9d1d2 100644 --- a/docs/src/shader-defs/image-dithering-def.ts +++ b/docs/src/shader-defs/image-dithering-def.ts @@ -19,21 +19,21 @@ export const imageDitheringDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBack, isColor: true, - description: 'Background color', + description: 'Background color (effective with originalColors off)', }, { name: 'colorFront', type: 'string', defaultValue: defaultParams.colorFront, isColor: true, - description: 'Foreground color', + description: 'Foreground color (effective with originalColors off)', }, { name: 'colorHighlight', type: 'string', defaultValue: defaultParams.colorHighlight, isColor: true, - description: 'The secondary foreground color (set it same as colorFront to get a classic 2-color dithering)', + description: 'The secondary foreground color (set it same as colorFront to get a classic 2-color dithering) (effective with originalColors off)', }, { name: 'originalColors', diff --git a/docs/src/shader-defs/spiral-def.ts b/docs/src/shader-defs/spiral-def.ts index 50e4f86ef..b9e8bc42e 100644 --- a/docs/src/shader-defs/spiral-def.ts +++ b/docs/src/shader-defs/spiral-def.ts @@ -77,7 +77,7 @@ export const spiralDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.noiseFrequency, - description: 'Moise frequency (no effect with noise = 0)', + description: 'Noise frequency (effective with noise > 0)', }, { name: 'softness', diff --git a/docs/src/shader-defs/static-mesh-gradient-def.ts b/docs/src/shader-defs/static-mesh-gradient-def.ts index d415e6dd9..c43cc6032 100644 --- a/docs/src/shader-defs/static-mesh-gradient-def.ts +++ b/docs/src/shader-defs/static-mesh-gradient-def.ts @@ -38,7 +38,7 @@ export const staticMeshGradientDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.waveXShift, - description: 'Phase offset applied to the X-axis wave', + description: 'Phase offset applied to the X-axis wave (effective with waveX > 0)', }, { name: 'waveY', @@ -54,7 +54,7 @@ export const staticMeshGradientDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.waveYShift, - description: 'Phase offset applied to the Y-axis wave', + description: 'Phase offset applied to the Y-axis wave (effective with waveY > 0)', }, { name: 'mixing', diff --git a/docs/src/shader-defs/swirl-def.ts b/docs/src/shader-defs/swirl-def.ts index a0b7b8f5c..2cd284491 100644 --- a/docs/src/shader-defs/swirl-def.ts +++ b/docs/src/shader-defs/swirl-def.ts @@ -77,7 +77,7 @@ export const swirlDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.noiseFrequency, - description: 'Noise frequency (no effect with noise = 0)', + description: 'Noise frequency (effective with noise > 0)', }, ...animatedCommonParams, ], diff --git a/docs/src/shader-defs/voronoi-def.ts b/docs/src/shader-defs/voronoi-def.ts index ab445e37c..06cdba193 100644 --- a/docs/src/shader-defs/voronoi-def.ts +++ b/docs/src/shader-defs/voronoi-def.ts @@ -27,7 +27,7 @@ export const voronoiDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorGap, isColor: true, - description: 'Color used for cell borders/gaps', + description: 'Color used for cell borders/gaps (effective with gap > 0)', }, { name: 'stepsPerColor', diff --git a/docs/src/shader-defs/water-def.ts b/docs/src/shader-defs/water-def.ts index 6fa335b96..6a88f0bb0 100644 --- a/docs/src/shader-defs/water-def.ts +++ b/docs/src/shader-defs/water-def.ts @@ -26,7 +26,7 @@ export const waterDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorHighlight, isColor: true, - description: 'Highlight color', + description: 'Highlight color (effective with highlights > 0)', }, { name: 'highlights', From 803f0f5c31c748c10332c67698847cc2913978cb Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 12 Jul 2026 18:41:26 +0400 Subject: [PATCH 120/186] build fix --- docs/src/app/(shaders)/paper-texture/page.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 18de79cf0..0fabaf091 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -26,11 +26,11 @@ const controlRules: ControlRules = { roughnessSize: { showWhen: { roughness: { moreThan: 0 } } }, fiberSize: { showWhen: { fiber: { moreThan: 0 } } }, foldType: { showWhen: { folds: { moreThan: 0 } } }, - foldCount: { showWhen: { folds: { moreThan: 0 }, foldType: { is: 'folds' } }, label: '↳ count' }, - foldSize: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ size x' }, - foldY: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ horizontal' }, - foldOffset: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ offset' }, - foldsShape: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } }, label: '↳ shape' }, + foldCount: { showWhen: { folds: { moreThan: 0 }, foldType: { is: 'folds' } } }, + foldSize: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, + foldY: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, + foldOffset: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, + foldsShape: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, }; const imageFiles = [ From 5b272b2d533e4692c93ecbc5a8fa1910c53bc5e8 Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 14 Aug 2026 23:39:10 +0400 Subject: [PATCH 121/186] sync with main --- CHANGELOG.md | 18 + docs/public/images/image-filters/test.png | Bin 0 -> 663628 bytes docs/public/shaders/lens-distortion.webp | Bin 0 -> 13994 bytes docs/registry.json | 65 ++++ docs/registry/lens-distortion-example.tsx | 5 + docs/src/app/(shaders)/fluted-glass/page.tsx | 17 +- docs/src/app/(shaders)/gem-smoke/page.tsx | 18 +- docs/src/app/(shaders)/god-rays/page.tsx | 11 +- docs/src/app/(shaders)/halftone-dots/page.tsx | 13 +- .../app/(shaders)/image-dithering/page.tsx | 19 +- .../app/(shaders)/lens-distortion/layout.tsx | 9 + .../app/(shaders)/lens-distortion/page.tsx | 121 ++++++ .../src/app/(shaders)/pulsing-border/page.tsx | 9 +- docs/src/app/(shaders)/spiral/page.tsx | 9 +- .../(shaders)/static-mesh-gradient/page.tsx | 10 +- .../(shaders)/static-radial-gradient/page.tsx | 15 +- docs/src/app/(shaders)/swirl/page.tsx | 9 +- docs/src/app/(shaders)/voronoi/page.tsx | 31 +- docs/src/app/(shaders)/warp/page.tsx | 11 +- docs/src/app/(shaders)/water/page.tsx | 10 +- docs/src/app/home-thumbnails.ts | 14 + docs/src/shader-defs/fluted-glass-def.ts | 6 +- docs/src/shader-defs/gem-smoke-def.ts | 4 +- docs/src/shader-defs/god-rays-def.ts | 2 +- docs/src/shader-defs/halftone-dots-def.ts | 2 +- docs/src/shader-defs/image-dithering-def.ts | 6 +- docs/src/shader-defs/lens-distortion-def.ts | 195 ++++++++++ docs/src/shader-defs/shader-defs.ts | 8 + docs/src/shader-defs/spiral-def.ts | 2 +- .../shader-defs/static-mesh-gradient-def.ts | 4 +- docs/src/shader-defs/swirl-def.ts | 2 +- docs/src/shader-defs/voronoi-def.ts | 2 +- docs/src/shader-defs/water-def.ts | 2 +- packages/shaders-react/package.json | 2 +- packages/shaders-react/src/index.ts | 4 + packages/shaders-react/src/shader-mount.tsx | 7 +- .../src/shaders/lens-distortion.tsx | 231 +++++++++++ packages/shaders/package.json | 2 +- .../src/get-shader-color-from-string.test.ts | 22 +- .../src/get-shader-color-from-string.ts | 33 +- packages/shaders/src/index.ts | 3 + packages/shaders/src/shader-mount.ts | 34 +- packages/shaders/src/shader-utils.ts | 43 ++- .../shaders/src/shaders/lens-distortion.ts | 364 ++++++++++++++++++ 44 files changed, 1227 insertions(+), 167 deletions(-) create mode 100644 docs/public/images/image-filters/test.png create mode 100644 docs/public/shaders/lens-distortion.webp create mode 100644 docs/registry/lens-distortion-example.tsx create mode 100644 docs/src/app/(shaders)/lens-distortion/layout.tsx create mode 100644 docs/src/app/(shaders)/lens-distortion/page.tsx create mode 100644 docs/src/shader-defs/lens-distortion-def.ts create mode 100644 packages/shaders-react/src/shaders/lens-distortion.tsx create mode 100644 packages/shaders/src/shaders/lens-distortion.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d24772d0b..fbcfa6eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Paper Shaders +## Version 0.0.80 + +- **Lens Distortion** + - `noiseOffset` is now a normalized parameter accepting values from 0 to 1 + +## Version 0.0.79 + +New shader: **Lens Distortion** + +## Version 0.0.78 + +- **General** + - The animation loop is now paused while a shader is out of the viewport + - Malformed hex color strings fall back to a default color instead of resolving to `NaN`; the default color changed from black to gray + - `HTMLImageElement` uniforms now wait for the image to decode before being passed to the shader + - React components re-render when a new prop is added + - Docs: missing shaders added to the shadcn registry and the shader definitions list + ## Version 0.0.77 - **General** diff --git a/docs/public/images/image-filters/test.png b/docs/public/images/image-filters/test.png new file mode 100644 index 0000000000000000000000000000000000000000..5bacc19041fb1a4865a6057148808709dd6cfa55 GIT binary patch literal 663628 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 !U^WNz?C;i`p34KMuDQR`eP_kzxpdIdD#Lp{ z8RTS$YJrv{!oVRum5oN~{D&G?*hCJZM+b>CC9R17e`^l#L8paA_1?SH7R2>Rr9%n8 z;}hbFC&%Y5Qm&+1Lrxa6Br;RdcN85euU)`{oiwk%Tf{U?Tox*6UXB}N3<5xVdqU$Q zm=b?lrtY+EGJ#M`^`Z-%VLkx>z&_{{gk+ybt*V*WSdA2=g=2kx0MEwG#Eo%e?=09w z$Y{-!oVXT&P$WUQTQZ_652V|7ZwpOLqiUiQFQ7=?ceTU8N~S@)a)mG=h{`0Xp!2A1 z7s#WAESb=stg{A5#h^KovUC{YvFuzSS-BKJ1f|RB!=ewig2L{e>hy?q41D5P=9ATS z7KZA@0_F@<`$%xFFIDK z5?RpKL#jQB>S3kF)pCj(0NSfx(xs6i?}VnT2nbkLBRj#4Qu;eL6P!C1=R4JEycU-Z z2%ySI@v!a-PdFjJaC$LSGd+}22-=Jt!W_FtMOge&XdVKdg@eH~g;4(M(Ce+_RV130 z9-A)?umiiw)$ephz?l~8j&UC@b)Rr9bjm^=H4wCQgXI*J9$5iO>`-KS?>E>&_MlWE z42p8wmAFlgD4O58TtO$vjl^WxbOmqg0y*`3med`r7Qfqx;V1-WTxxQnIy4_j*!T-; zXg0Pes49{6E!rCdud~?nS~5-!1n|lOp3YpGb@C7mFe47q6Y>_os92k*xntzXd z(W$ugkF|Too5$+ncEi4W3c{nJ7`=uYgCUA9p+=Z9i+T)FmBPJKt=d&yFHFBS|HwX^ zbbgbDm`(&yMO0}C1#`1@=csi1yy0kz*49>OHV5k{I8OZ|wH-ur|AVFMIKs8qus_ey zgmZB-pt=$7qdBRg( z#2B31_d%Q;h}Nc5wX&bK$4@+?Bv z1$nN9SeuUu)3+KDG;fD)afY#z93|&Y+d?4V(Fp(s$2v{kTc5We4|K0shl;bj%FAl@ zfR(KG*ql}GG>9OlUR}qrI{7{Epi&p~S)d@XAq+c=fkHgD zJ4BY6##Kqx4^BJ0)I@7$*=%5(s4$i`DB;phq<0t|OTaqm$c18n75+(m zC2O!c^)0L1ALY2TyfHYWQl7Yn7C;ZHEkh5MH z1*8gFb6r0eBBd;*O0Y+f9gDC=lzK$L(<58tySv%*8I`9fJd+^W_`_7dvD27*#h_

3ZqHp)dGk#DEt|7f8t2-hcykqf{j`sB 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 z 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%0DjPc-n<;SSb7>MuMl#!Uk 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

  • 9r9;L&^~g|o^DNV z9cJ5_!KJ(6X>Uv&DBbkQU10fW!38f1N;x6)^>y_#5PqUU?A2@=NnDsi1AFC&1ir8I zJ(LOdzV!YoBIY;hiO;(ekNbIhM@!azx=!@t9h8|yqO!ZnOGqPdJZ_l zdOdN3_BvG8S>^7D_Y6LQSHl)Z^u&)3iooaPHU-fl21~SK2uZT6=5HLt?^%*aj9=JS zhl>kr^`{B}h@wSYmdoWJcoEjNL+V_kyUTsFL^@3%B-&_Y?el;Gw~LVyYjb{#LSUv+ zT^nd$HEdlX0$zwVfE8MZNlr4C1y~l&$UU)qy0`_ej{wk;6W6AVHrN0Lda-f|SoGSj z;is&gUexb~jtJCe)ymsO0I+Qw`4(}k;WT}N^M0HRwa>>a2n*s0fQW1^5pCPHEDI6A z286KK7g4O$sI+gC0RRzkCtyEeKrOB?c3~qWL#i79>JdwM7T&u0L1! zDk}{-yu*P&eF;H=zs+ZDPx(`{wBaVcT(_Zuu`?~(wzcMRSfW$g-bRjJHoLF1^h?=| z+HH+c0f?;#G1wPm$8}k77i*|y&E1ac)GmIC(Tpr^5CGd&2wJ^^W16hCK0Equs07IP zdRDPbo(-PC5L?@u4zv-W<=?oEv}{iF5^RsT*`xQSpAZENJvFl-kK(I2;nV23m7Wcy0#`3e|eWQ@Q;D(cP@E!(1vneUUPb%3&C<9Ape+#*!RnB5q9_lZXwoTez1Y>@zUauM%4kwZ z)F`{G4ZpiFH9R2q?kU<;)TxL_KzF2eNzl413*rKR+il&pZR71O5@Q;rrToX6Tsfj! z=kfHOD#)jQrYhBeA(JF%*w~P{m2^`($R>ehf%V=`p-jpXJ3MSiHA#uC003>Y(RN*L zE1*hb>(9Q%ONAjw0AO)+eU$bwmXL=Hn;yBoLgJZUY z!%=A>;xR6tVDiDi+hl@nP26aqGEn7f2|4d^1%ViTJzT$!lEz7dN#Y{ky z695HA4wNUcpRyi0{6n5C`UeJ5vD|hg=l?F^;55$TuQJ_kD|6#3If1_7rd<7+QwMvwyoQC+17O>XKuO|(tZ1ELUIU)AmQs- zWmDyf2mqrY%-(VuV%u84Vq?7z)Q(SVh6NFA8{(y@9GO!zRi+Y;tJ^;mSxfOa@R!hP z?4i{d+^8*8xGa~;<#M~-0Dy>Yw|4&2wywPU0T*mSiK73|aVz3tAd?ds=DtRNH;Ji_ za<<@BGHoQ&4h3_a%PKGC31(>9+N&q+*h)EM%TBVPZ3D|x32T-s9BxOB^hm4K;pleX zz(4OG@Q)&;(1OddTowzD3S`0*rfzo1@VH*Wm#xh02$gg{bnM29&)pu8Hlp%7U0+|8l!AzCeg~O zmk$;tIXO=jBq)ePbh|aQ!pjTcf&h5AT-J3}ua)!ZV~~5fr+%D0@62KWuINjRWKpc1)M293qiVL;@d@J zM$j~@T3j$zsU}Ax#j3xFwsl=!Z#O`EkrRn8mn&_wA+CbZ;R4RIVkbZcvNWpZQWV!k z;{)tm1X_x{5CG7!EZ6H55!cI#8v?@Zwq7^j45V}+X4AT=lq1v+pp=EwbpFW25#hGw zooGZv>&nEtO1f2^7iMc$F#kvlnKw_GdnNs{B^`%}JtfW3D;n2*m>^@l@f#s5NUTAL z2sYZ*+k!wGO?0c{ZU10r7KNe=wWw97RGQenkLmBKG?&!Ft8u|?!*yMu?S@rFq1uAt zN>PDmv!v8fC`B#f!lQ4lk;X4o&Uad=NvOZ6{huW|!3MN4G$JYelC$Pg^7b}KwcZJ> z8G{3ap55cu_HX#oaFlp;+cv~S;cVC;qNa*ss#YuTm91-r*R-j_XCe;QZF@rCoNU#s zL|y$(SGN|LXbHNCV`>X7F}An9!nU#x(BkW>bXu8+RPi1^nlj+S#e!CY=U|H@(q;29 z2k~-SH&px4J*doAy$m{SZ-!X>45FZac&ecLf9sEcV1b1bBVrkgVX-yj17eHJ`Mw62 zBG!43Ic5 zi|bm-O@;_U-cha^s-jrif3R&Wn8!MZWIhP6!3Nz?*b#Wjvf8T@3Qw%fqb%*YMl?NO z)2z&DoB+V3?K~XN%mNGJ()k&L>Yu6s6#oRDF!zayPI*{i76;dLSp+zt zW8TiQG*2BQ8wqqxBawtFy3y-RxFfe{QUaQ()5}MCa4XiVCItT6Qd28L8zNlrqW)VW zYCQV+WSOwUNBOSjqq3vHO>|b0+HA((SwOIbrd5)Ai2{tokKpxn{qXv_ZYvP!*)AFp z$VIk~3};U>#b>pg3pA3?;E}thQl%fKLm3e$%(^N6E=xPaXcx>vE9ywW0^RF_yxgq* zsf$ockBaO|ArSGY*P3Wu76hW}>+7vWQVD56eX|$9#cWhM=bsLYeqQJmnKL{B&C$*r z`#Q!z@|4&(Cg3m~YXl;AeZ9Tj)^%HPfy?D`xhzBr;FYL-R2sm&=Ah$gL^WGElb^fu zQEBY`T2L$s!X<7E?jJYN4(q(KhzkN;U*5f2uFGw^y{@+nXd_y-6*X9ijpG-4Z;rck z;s+h(yNs;kZ6%YV_CmJhGX=>A_D^z7G&qIZw%u;8u;I(Q&!A27vRu~1>x^J7MKz=7 zO{3QWxYkZ=P3L;4vgk@73nBpW@tvkcm-F;fdj0I>bAW{-!2oi0nhW^@ z0&)%q9b~9ujByl_(p;7D#5iie-T5E}2BJsNpPfO5q9|Q+WGfJGq1RX1w(WMiU9Ojx zckjOQ#TT!)*KMP%d3DVm*qn7F+Dpvj6kc>zVIbnsLIjy+5T_mv2rOhS)7_ClEjD4h zAa2`6h#y{G3F!6p11<|9UY3h($^%|&({)4%N-+TJ@QEQ%HuseJmLEz4>~x4_ft4~u z06iIqg9?%2TxjKXyWMWLFF*gzhuiD*a&1{D(ocLVU&f$Gcr-0!Fp|!)b|LnZ)=rU) z-NyU`4p&Iq_W9@UHrQT2yxwjrEX>j^%WA=VCyI=NGhRVg#B*rE5{P*UMDZzj%yoPh zR*2zG6w5ZaP%pD8(Ms!fBgFUb-y_21a=Biv%VkkHpt+kE3sg5vq`1d{ZQPONp-@4! zfm0HtsgzW9R!!*EH0ic+N#AzTR%HudnTug*KF=qLGjmTKLN-?Pk;@ zLe3h#*2Q-~80-e~fM7d1iiOCIzY3P9CLYWPLjc;g4H34D*7f%F*WbK*`RwK8`ts6p zRwQI#9BL6_!Ld+^OR%RMXF(A#0eaX3HRFcD=^gVn03&$qVp-6}TiTm{cv)VqFMx2t z<#M^azH%;ZT~`&i;^i<`dVSbMxD|W{D>^WZl9T!gCI`k7RTSvnzEWd4QcJ)GO4z4i zYF*cj)|YoLFYjJj7Jb`R;s9%Ne-4Bt6Rd99D!61)UX~tIBq+K`HWJhgQ4Fdm z{uCn4<^h7Fdr_!n5N#DRYbt9 zq-PwkaF%u;;=}jf2%He1ZTJ9y%k@G)2+L);T)+F>?^J8|6H>nO-S1p4m(M=?{KNYX z>$?8NZ~VPa4bj_{AN}TUBI0K+pZ)C5{oM5pUT^zWmCrx_{6|0f(T5KoJ~be3QW_&) zuh-k{_Wr{+pMUYi^?LpO_kZxI;dB?LEBz)Udp5`T9rOXYby9_sj2oYCzt$tlM^3mW^Ot*Yziz zPXXZX|HiMsyI$Y@;0K=?kT);C^bh{x*I#}8qaS_ssR4PD((LlfvTWOmxU9GJ<>mU* zKmF668m>1l-~ayiKfHhccYpoww)Yo)BFk4_ebxS6F7Wy1pMU=O=bsv)H!t7&?sr~4 zy#Cs+{o1G6==#yh0?RM_mS6n-55AB8^1uAA!18iMqJ@CkHd3UP4fpv_4GpkM%^`;t zO(T2H%7yVbz@rT1*gR&Qb!E%y54T-GBXH-6*S|Jq;utKWR}H4!e$1(${RHTSk#&xOBbvYSL+I5(|xmY6_G`w5-o{oNr- z($-;-g-NSqyIT3CTMh&t9U%bXgp=+-TsOV}1ayG~;J5t3&;HUc{SE+ZXCAg^Eo6LI zuFGY)ESH6kACL?H3Q{2Ngzp2IHenyzk_A8x##=01X-4PkLV+RjhCIc#QF5bo+W-*w zTpqAk7%m9!KKtT7{*V9aul}uH-DpFCmUrD&f<;MABC;rpiVp6!<6@%?pwX1WKN-1d zIN5?hLZ2QWj!-#Tb_>b604z&X@USD6o``s^o_wWAQKlSDJ zZ|iNjTo_*qw)fH3ZQItBHX_&nn4$rpk=z1&o{O%$kK%=_xuF?tR(!+df60?O>6+*t z4SX8&0#Q3A3lMC3;nt|6y*mW}3obKbMUsDZs)E(Cx`NJv1}>&xHy&;Isb`ipJ+0$7-(NuUnd5Dn-GAV|I%Oh)&KNw zUvVMWmg^Py!0+WE|L1pH^_%Y?Cp|XJ?c`6RA35o>X?&@M0D8(%jQ1n3{NUfRSyh%% zk~m7qy0*OEuQ4xzDl$MoBBFI$*W0>o+p;Xn0-t^M?hpKde+0K>xh~7H5KznG0JLw8 ziR4kDY!YQ@`!Ml>i>7B6xj?1`+DJ%0C3}>64W;rAU?vr29o`Gt`rPDjI}yfm*mHXU z`+B)tE^d!N1A6;+|1ba7ZKHSZuC&3ruKYUdvH;=&j&C?`Le2Ve0y#3Ts7xcoW@JJT z!Kl^_DNiesQ*MW;Ryju~g~wQc^Q)IjI}`z%(qy{=k+v0R`ReN*{_-#XL%;M7{POGi z0TC|Cby+}=xNRG4ysdNtmH`Wsn%IPljD~rqWn-3A)&!hfQH(kpz;KRn)^06SxsoVR zF%uZa&2WMRknnPOSuP8~hIskW5C8uE?ce+NKYRB%AuWKgTm(g1!+Rqc6F4ee!Nh~v zJ7U>;2(=zhPE0P5%oHtZT#`#3pqQw@PS#E<+5DVa0N87zOA=*{n;0vzkdCr zU-*Sz{J{@?=5lS{9$1#^0*K4fHZiewUF2A`{yPRIvikvRszsf(Rgb69BKv@}K^H|JMKKum6qjeCIpsdb_RbdRygO z4DCmRK_hVo^7eu^BHX79$S8@}hS4dWgTrEBD{+S2?0Enj=2!pUr zHMJ^?gA2Ap5pY2QSU38?4}S3Xf8#fP`FH;E@A;j-7r*@SOE$#NbzLp3T#+MbdE`7# zN7fYk5SK^^li^3-z141Aq3w_g7P7L2s(aw`E(AIPfCZOz+kW@&`dxqGPyC7B_xpeU z*I#{gy}pRpS-_6Qi({(?gKGM0dH{6D(h zv~AmRS^n1F{F{IDkN!{p?4SL!*X4TSH;&BOHqE!Fc69@w>U-a;F4PQm*F#V?a%`mF zS?;Tp_WIU(VJ2bXhr)(yQ)mJHfA+uk-~Au_bN}3D@7}erNfX_T4;^**d3)fqMQD0~ z8zP!0CNKo*>NqT$(P&J7C_08Q%}Rh&@{uk)ANXeB5ivq;>w39d{@kDYbASAg{focy zD}VjnXD{1E22fKrERME_X; z5H$kRR#+9k0P5OUaL#=}H?8F=TIlum`o$Mt{L%mM|K#8PlYdg?z&(54Ti@jf8lfsF z&ZTI^jyBy?3M_+=wNNg?Mq>7-=8ols@qM62PJG7OZT%V&s}U58!Yt5m^bN%hNdP!`7Cr*O%Y;yT9>Y{{6r2yI*|o`okao z5Pm$1^Lae^g|aNmwr#f$>o5GmZ~etz{KePXjXCxwv~0HrbjyMO-eI?RNX(yI=g& z_rCYDKl?ME8lX2XUw-*rpzVi0`r!un`r*AA*7{M2A1`@ZmStJ6g-W;W=YQcBJ~cRR zTi(Ba|Ji4s{qzrha9dyBe|XBjt=oFLT`vEBv8^2d zzWnZ&+q(YRum7EE+eHWv!nu8*s1^S7UiAoFbQFM7NL^i&Lpj|+*AEm8*=f#t3&&LA zyLa#2zkmPo^3q;6|N5)1US6&o1D0$6mVq&F;Z_Z!4=CKZ&K4ANV3dICGFfFzZ=$0F zYq7?N&uVSkdcD4U_02c0>kSd#y?fat{Z@?*m8Ulk1n!r#Kt=M}0Z_mcO}9pD{#HxV z1JU(zc_kv+zW(~_ckf=dZC#d2L#GSdg2a>?imm6cB<}1f{;Z%sy%s@F`{eUAeEd*R zWTAiIf~shum7Dtw3$hyng`OLI-fp++_4?t%2VC%axm=gawyb~vz@Hz03lPz^EU<1= z$8FruaUCdZs^D97dx#;TqFS?fb4+?zhzJojKmcmHQx;fPx-83$KTP$yaA0vE=d)?# z(b6qGlVyWvPLhZxCf?Qx<;*R{Z;$)zN<#qi)+xGtb z`-}K&Xc<+hS zynFYeC-gXYtoWW`S*TlLUnq%_UR&!D9p}D5NvVv+b7*|W%Zi8>5lXMGuWgHLLu-5G zWI?n&=;fp=2k0gmrjt~O$Z1kqkxK5gUXsdDH%c-{b0Q+RNX z^8Wq%>-EZ;*j+A+r%5r@=m#{`5W6aTAE6!6Q`x9(7~5Z9sm)YvHof-BL6A3E-HRSfxbh>C(l*@aCXmEz@t9g@*fuWh!KDW<* zd6V?zdZEjtfUGAu93ARjOtdvdwO4A~;pp#+IapRZ>u1s?WJc zJ+QQD?XtZGwyx{T%ga|^e|@=Lzx(p;D#9!p;*)U(_ILqWR<_cZ_sFzg6T+y}*|~z3 zOM7h78p`bTp)*!$VZCZ`3B;UW?dP+Gr(!?RvSQ?lUxP=2%XS zOTVCd#IMn{_kLknSlb#OF0kO@`ze4^UgKuU>Lri+o&-=2Fhbk=db#kM)bMJl}#74o+5w2I~ z*P->yuA+)#*1_JCQ4EAQhDvV)hl<&*Aoq#tic>L2#isdCcK(^JQ2!h8vNfJ%$+Iw) z`3-ua&HFY-L&Eb4y)1qx#vIAW6hF@$+jgS`uzlH*=ZU->h~%vqZiKmGSyE@$BDRnvvuKF3Bm1i+8LP zwTt$5BRAWHQc&#v<+6ymNS8}ftQ!Kq6^R;s@v;Roo|Ee?HH~}8oYW>zae=H5A1xiC zy&cr#>bkBMxZo8@?1XLGb_AxF()Xd@sBv}R(V^j7u$7g{N-gfc(YAmb1TDOJ39W1P zAZl+O0g*%3(it%OJ!y|1A10rdg;A+GV95q`bH({1v;_R(A8oL$o1^xiRSy*JuFNnp zk4}YJ{mm1I-e5y{=?oEp+Lz?;cDrrc_U_#~);PfLS}aKpdJSs7eZc}}6W)~6OfyVJ z{;d+5d3EYVqj8qh<{p-1LBQL3Ytj`jm-eltq(E0%=fR_Fzl>*7)vFa^?3_ZCVUMZ* zX3aM#NcLBbP%twN6sr|5J&!!u z2`K#i`UYC#m}ahqXlO1%-spa$XT`#mkRSJC#~qqmq!)Z%0)o8zxzV<*TdTAnwp5*c z>KdFah!t=XD{Ze3*{6G(is=Mi3@ov98g*BAe4tXRMc0~Hv}(kG02dXGR)mBlRnk2& zpQ8P(kbB^FzFqn@`3bLAu<-bN5_42;bX& zzSjwA$yHH1L`@%WazwhC%f3l7F2gdalB+OazReJcM^rF zNtpH<1fO*W$q%rw_TU|jvn6H`McOwc5;R)}5z+6U@T=z#QgLc`L0ugTttXSHCpe-A z&mBhOA>8Rbv1Xa6>eT>C5%;0UCI^>2m=t?l37KIO{K*K8eNcRIGbrT=t58HytX{5{ zC>#Q8?@aqm5(RBMrF4i67LK|uj`=iJ>|ds#Ot)%Y|o+`beGQWSIM>vZ6h*On6zzM}>DG?w2K&z9i!lMo807vC5cfVu)g{6L^ zBn>nu=?<=}$YOy=18(7pXkr1$bZ+KZQ zE&reqG!?(DX%Jy|PYawwH|r)zJm>rzA#Im+Cw&{Z{gk&hHX>}X;ygy^NIHvxj^Ae0 zM%ke)K%8jK%nEx3iSwut9H9b}s~$7PI|!~8W(%f)p)oja@$dNTv5u(F0I@z@&FaWRIZGWR7riTboV@&d-T zvN(3Z!Vq7vI98+*v27yNO;P4Z!s|PpRWQBtO_Eqh5SG^AfChw5*=27BEwl}3eG8JQ zo|R2e%!Rfc(ba@8@mX9Q*($HjcC&Q>5s^Q&x^m(|q7Oi{HLni!B&i-^pOfr<`dI{g zh9e(x6F+5VO1h(${8RE*^{!(0O12&`)}zyQfU$Ki3-JwJK=8qn(x* zV8oA2b)x?mtkm3ymLNbbDp!NQIz^_PtL1(MJgNff4Aj_jUd~jxFGn?ioa(gj3%anb zH-HvYBOuZvuxi$b_K)Y>#7)X#0MOG3!@N(I7WfN52)fls2nK)~i->@l2e+;3O%5fM zm!}3340xQFK)X)^#AWT7$eI+lj3$u~Q4aRAVE48WY#R{qt17{4nk~OGSOZ9PdO=-jmj#FlIsK&o;1px5KN{Fqx#qD2wYp)4eB>YYQa3$qf@qvWa3GU> zDWMS~vbH56a@lgCBGSatyK+@L@a|r{_{|o@{-#=nWR{ZNJ%U9Zzu8eM(Un7K+q%Kp z42cDo1qeB9gtk^g7EYbZ%xoQIUMi)ZF&GzqCZntLH>GB)i~JNBHDU_3=G)cxl@JFN zb|*hG1QQF)59&ubZ!eO4@*ebGRVRc5@`8<7N)d}`eZ>}L(><+hAG*}=gqaE|3Y-XV z8Av-_&AZ%j=Ei$rvOxMeWs?zNTaZ;iTa8{vrebm-4HM)@Y7({SoCXfuq$s{8G~n%a zuFmgJAi&a2neXQ-F7XcE_8}W*{JAxiJKC#O4vQA@1dcW!075{xwNHUa58B5f#smO~ zKO@3&13oBzChiV$>xAWxBG)O!vLCgXCIu1sJ2+BtSv=E*y3moOS14wy$+|mx*Wxim zS@MF?&t0L{?6}769kWyOYD^dB*xzAA#vMPQ-k3a2%Va6pjHZsaLwa?(JqI9?azcTh+eMJ}!3fr7^;(Qt*JpKoI!@ zv@RE15I18|-i1y6m6r~@4$=0xpLo}J-yz2&v&Wc=0tojLIHx=IT)<``LacZ6ukNp_ zPrUh|hGib1+r8;V*)OFn-X8FMuYd+UNI+z(0>4EM{#7?cR#2VfnbCx5v$7!b(d5;P z!n$>@=auLL09qDo4hs?3Iq_O&6ZJ}f*do8gGw4m>LFtW|8WFM1q>Cf+mVCT+TF>e) zYWcQfh*%e<_)&6J$unkRPw$+&OlVNVHNS(?;5&wyTCy%W=`X&j_kU zIp(T}Z8BMWFFN}-K%m~1d#p$!`8X8BV5kkzYUy$XL#T&Wu_M{Zp`}r{@#liLc<;)A9Wl>}ThgaZ&d~&0PzU-Jv6pytn zWiS*ag*AtZ{E(=>h(s9A6$61*!pPtrmQh=pYDy$DqCJ41!;-mb%YTQ197JoVHgy&- zunKHM`hinmF1FQoKX=5U)F)|`?do84hv=C3jEvcdfTY1qFq7^4lu}768&TGPlLor4NTk`^O5! zQq9FpF_1~<1i&I@QNL2#sfB5+^6zhZk(emDMTWKs4*+asUB>d522exScz`*3xI`k+ z$uKf=GZba=pU9A^-_Xv43|djDK8L>%r#x7Aoeo3eNf=T8LqDl@y2sm(t0P8ICcb#hgR#LM<}W={mS1xvq0oH?50Sq0HDPKAgoWt!ZfwBq9T zgV&N?7SCZ;zN>1mAtd_xKq+hhB3elf+Q(3j5Juk;+bt7icUMn?X9t*AokD1`cDVq^ zdLf%RdvRm}#of>cl)F(XYsCSmTFdoYtU0+!%$7uS3N6qw$o9_lDeH zbUpUB5AR7M@G2P*wzip_&v;!>kEsLzTmToBpii!Uv~1e0O&h}*Sh96WLQ$k2e^jLn zu4a}GBw25-Fv*$k;MiKTZOGrx&Dz5o!Tu4ZdTHGbtnRQ5N6BQ;vDy*d=7z~xci$Q7 z8nl%SQwvZ!+}T&;J6#p-sR11}BnOfx6XDJvYJxTH_%bH3Dn#Iq%2tuiG2=!e3*d&E zemAz^T|PFiaBO33Gsn2rX@(b9_3uDTX#x%Dt!=R6CJ<4q;=E0fw=K(RQkLxu#RVDO z`G36WK+Oa>UJIPR>L+CZJ-cf`8#&=X;_Pmr3~CHhmepu;b13n2m!mMGDrQ-h3%X-6 zjCA8N5j*oGX6_C-9WRNOk4a?FL+xVYR~E>QNEmyWI%apSJmXjAEVRKb0{}o=+8IRvKpJ=^C&uTctnmaS2XTGWj1xuAVF@SD6R{FhsUct=>fD?A6@OOEnZ{}`ZV$DgpBs=-vtY#)dVf_ zeo@O0h$%*TsbfEG{`3}3p6N4+qv1)vvJ@hba%?VkU}+rXh#>w%Zp#|!dWx&b=g)zp z{G*Up9o;hUL5~WU!DdrhAr6|zshOx@>o9!+AbAx~v>}{0aH%UYyPCC<3M(tb)vgu1 zt)W9l)-Ev-(WZ7$@#cJe-pyNBNv#e!a0(AS6MLm$! zQCM;kJD9r!0L@`lF@?GshgfP4O<@hjwHQI70|S7QPMh``>GVR6g_olcH_~(3S!z48 zuE8RZA&=)JW9yM!45nByJG`~$Hbk^XQBVtWY4k*7#-%m?0fkYz%fb0wgfUGtpdcAEbF6?hzNMzMS@~VpPZH~xl zt+Tt)H#evjry_V4o>X<^Y)l8eZw030wbj7B!MKP{FJfQY%u}?d{pc|Xj3nP`P-Cw1 zyNLT-79`!_UsNl*pgcl3;uXd%T>6R0&rFx4x0vqe?O%3O$Kfg@%Z3MUdtA7rU0A9K2Hl_AO zd$kL>DneX&87~M7w{E^;&;V#y_fC&_Gg*6+s4lneckiX1Lr1E#9qI6%QlLH%{C0)) z$$^2qDb<@*vFZupLqHU1lOgM?s;q0jvu5*kLnZHpvbAQ4S2d~vpU2J?Eh>DNR*$(4 z0lJTuZ|h3#n~3^si-y&~k|N>V(~}4tjPIg0?xr*Wm7ie+sTr46c?G&a<0}fMW|HzR zb>TwmOzm4TNS%HRy3fUaq*a1jNg}w5n8%iMVj_m*3O z3NnJM)x@`OK-v&T3X;jVNDI0VCP3w+-@qXp-ZNH}K|;VPSvLleMS_JvQ9g?6Nw!u8 zsqkJ6Oy^R2-7~+*-HePvjx{8Yqmi0xT)O zqSSF!(>O(2q7Iy5c@X(q@TxTikj%>j>AB{9R}*Fs7`x%9qpKi;0HJ*_6;;~Cp3%?$ z5FsT1^u*&Bi6;v=Nw*#U3t91=aSk3Nw~L$GO$35=#IrL3xI+7wC#c-sdy^$36_1s{ zcGtez@h)@0L<*=kA>34qbvqOCLfbl^H*8h04H|orNNiiJSkw5f_k)Er+?3(qE;zcx z8LX;jPYA3p&olF0;rQmH6T?`~70%reg#Va< z^LAo#XlZ^|n$bhpZL1gEAXjVM6^fGox{AhpkBM<$wc+%$u zaN@u%YUKNZ!VY+4ks_LV>yoSjH*dcMEzUvYM4Vu&LMBust5_`?fAA8~;=dipR!H_@5x_Y!6d)Dx ziMZfpffFn+EW4=ns%$jgfTqB1$A4n~>Uak0)}c%BM7{y%G>oqqS_wYcvX|3oeKO=u z1SR2^M1@Pqn(k20_?>yxP@-h@IcqqA4fxDL8HC!YIyNsUhJjo&;Zx_!_9iWAq#qgIY9YD3&;j6H6RKElE*ten zpgB`p9z6|7ccfT%TUeh5`X)t^!y9d3J7#zq)=xGrfi9no+B_D55S#FQwsb2=D%I^u zOcTXGG>~OJ*cPq0p2D{v^2B>hy$JLY@;(Ij4Q)WS!U}{Jdit)>`wkM9&9rFE zCkz1vuUjdEV`WQrsa0H(sgKIVpeB&@@757LK?Gsq;0;2l9iJPLAS|GJ4LxC{F=7N`JdBZp zTPsmCPSI{cl(^uwp|eUzH#L#CamRUOuJXM#N+6YI)7<4Cqyd1>#^i|FoAO z-RYi+BS7DUb}IX8!Vtb#d?9LW77=oosC3KL8Yx1lu-Q;fem2EXFW>1k<%xDkbRs|9 z9}clQb~Y^hBNjYr@-opjZjzI&z4pp&4d$_Y);@xLYi5;+Oh;3^-#6y-F>wQ`1t>&XfDeb^=%fRS$S979ynzt-yz9ejjd zzU)9eM&h_ZwtNcMAt`He$S2TT5H3qb-yMw95%n9EBiX-+Wd(WlAaaFUquiJxPlf6w zY!T4#OSv1!XLu-G3}#T2W`nhe8I`)uqLzQ*i{h=iZWWs9)PIMyKzPn&f@;>?fO^|69>$b;UDcX zYt7QgyKm6GXupNOdZP2bTS^bmNuTx_^0J6Wgp$RIAU&%L_B)&y2Pe88BTwdr%A4K> z?cf;oUST7P*SgnSwGV~VJyO13g3!dya$#@W8R&9p{wmbVf4H1-dW+`n`Z3eltbaah z$!&qQbjwg!+TReCb-@FO&PwPYIzO{D?UZarcVBtf8|Nv!Oy&-!`#wj7a+Ljn@(fcf zdkZ;;*6!KX9$Wv)DC+y|tgT>H)jOQ{Uq>$B5jh!k07-QKC2-_|r+kHd^Zj%o5o`FU zPCe~+k;MZ6_Rpd4PC@$RC-B4-^Wl~ih%Z`=z6kv%~0^4 z%HVXSl4I@pCIpnJnSsRd2gwPu0766n+BQI3RDNFu8^d`LQDiare}C99?9T&sdsCDvT34>NYeO#2my0$7K}iv^TMQvNu$2Rf!;{ZkRY_ z(!l~ACS;!KpkS6@;n97LhwdLx&WN^1E;R2hu8wY~K_Q$Z?PjL*{o0Q=%V7$NpM{k}|Z zQ*5KL*aqZlLlq*R|3Z<9VE?b%>&H|biQC(M9x`vZ3r4*Q6fT?@Y;`<8^-Cu*8O2Fa z1CE-(i*i@gG1h2(r0jPv&$lN9cnY5Oaw^7?^ZAi+udIsv&kd(WQLur*%oUd>SF6fl zDi}B1W0dCZKpKJ5cd{3L}EH@#c8Jx>e~H@o-5J+suQ}P}480i3Lmf z7C`s94LYiK0H7Xc=@_S1N#2DaiglQMEDs1G%KZnYKf*$)?IXnVgqM2ML;}WBlb&XWN-zd_-q5lpl zAb{u_Cb?q~rlees8&^pay+Lu-W;sr82g?L>IVXsSiJW3&C$;CLlS-=D?H(lzHc_Nv zz)gZxf$V5qPN2jAbgz+YB`bi(s>LSyH~l}lF!`d>>i(C1xgQApe&j$tSlk1~MN0ai z3(ZTNdUO>)ln@=QGWNBCetk;@I$s|5g;eR7qwlO<&U(0zuxpsU9%7~zGj(T5c(&;& z-kW_LuJv-jDCpXIgWn7*ohq|`!>3~h$L2q7McDw;Zz&tU3TX6P^%6`Flsa^oV4w{-O(`TS~;MpFHR#9W#Si6Wo$i zcKARrYyoeUTz2#FSf#fp^&ELyl z_>HKNTpffsvGZ5+Nc>;e!y*i;hDx<>sOG^*F-cG*V>+`LbUFwJIJWHctqTi?v%`8X zjV#=|lOj`BZPZ z{|sxI4Y4O2FLVThJu5wTF)8vk*>$CD^#rfWaGZUdJ9G{|cSITHV6?$&|69FN1-GIo zO~>_3tqQNLh6lWLBAv5-4xC4>xyuy@&8HlG#Ij2sx;i#@HW6s0t{8UuRXJ52HZjXi z_p6XNLOrpxy-T+674DPG+RN25z$yVH{HmiCbfk_VsHmx38}gx|=>7x#d$Dw!eAKcF zfc$tB3gS^NvI~HYL5lWZ&Db@p7XqDX#Mnq%&FzoD`u3DWAiE2@nru@6K>L8w852dt zGVGbewH1!{SVXB?^|=Q%*ro^ZErjJz;RgUfST6bnjakOh_1w5^5FME3W&)FT(se{F zd&Pk*NNEH`u)6O762PiA1y#q4!t0QEoLtocCHW zDIVvs5_Vz$rT58Pi6YbvB(cM%2C`WS?48 z64^F#--b5h!;DM!5cLZ=(yhR1P|IH)rfI`WE73?bgHG;H|II*%O?pF0Wi%zYe&OW~Hnwk-nX_$vUNae`&U1okH@u$y+m!cV#=~^vf1DY*4N{GIAI^v4XA=-mOvu_OL`6-J-txB9}oRHnB0U2yhQ<3 z4)+kI8yaO~eeQn2g6e+CU7QN`D(aMh^8hbTNvZN*9bH|9$7?42SG!`?sekXWNDQGJa!uizX^Ad7Nf0 zk~UIF@OZ(|=Nj)WhdWaVAgj@Cg?36{`F^Ks-HIprOT z1-d@NA+L55(D6d_9M$X@bkZbk$E9QWCmiTz&1968!fS+)IYCFVtS~@83Eb23FsF)8 zZg+p%dgW?XQ1sM>!PufWo;Rn!K|}H9u!C_am3G;a8Txmk;1k%nT@hhwcgo3(YayFojd!@rS(u0a6t2#w1RA(ks$l-H z!;mRa<9GK;A2BABuv3=K1+pjB{aq_xh3yZ@>bJ<5?+j?yUf3SjEY;oIbN|%UO9<%I zVG;%-*(*LvpYKh%eZddbce?vKkA zi!52Nl)}!4LtVZ}c?%Y%YDT9tgSiloEse$pOV`p|&I;nu$V$Y>4s7kBA1R21)*gg> zpILmqH^4I)xPK2a$-<|qe&@y9Ww9JHl}E&X`q0)-SEio(PK9iH5NMc>Z-m`jPT}sk z2t6@$cd)n`-Vq()8wXZvQILpq z*w5*%PY~@UQI8A2wor+?!WJ60qh)BdwvI6HgrB~1MU*CVcyI`GiTAQ2X9lH=jmC$- zMne6oPPRwOu6jeG;lnR8RppbU-A)&!?s4&2Dov`$oNy2^^cwdla$c2?NY8!o(!;rw zDSp|(o=3N?FbpctIM`rL1>-^(3H2dFD?lgu7z+aGC6GyDSIuFKC?1TYY$Yj@kU;hg zMebUr1@qE|yf+9^oVFNmx69bUcJx6LQ`PK?C|JlHox}D7r>9iyyt1xNS;eUyxzjM8 z5C3pF7h+mFwFgyNs5(r!65Jx^=E(R4`EXc^X0W<6>Tu3#T5(c352)XB|42HnOuA{y zvn6AyOP)Np(BL{kq|fDyDZhqPSFKnf+2ZK6a-Rs=g?qT~K!#5#K^;p3-f90PW+U63 zZeXi$=Q(zJFmP5a$kM7}c{xll3v1Mh48o%#9weO?mHSV`9q@Im2U7w(v@l#Td5m(?%Yn29gYQWW z*mxWOjD$pkD_28up)6#2^zHM*g*uq;EBE%GwrTK!D%lBOX0i~yIT+3PR#h(7sEt}U zUCwt#Ct?MW!98*sH26|kW01D!!PH;wYud@{71t)5lKJdH4>?eW`nTepltKYwQE|lY zWL*=uXdDZ}LuNrCrZWc8pikO~5>@?ki?YF+^nS2-Q|Dx<1$0u_RmZwF#=A7Ty9D=j zRUA&_?;Qrx?l3yUIg7SmG1C5G_5g>=U=8XgVFl_=@*X6xi0JpwHNKJ6V5h z6lpoR3{fU)e^g>n^E~cR;qexLCF^(BLZcvTAHu*hETVqjH9QX3lS)Hi5gANZA&I1I zs~$Zk#wbRh7TVwE6}@_EJI=B`7Tdc!2oZgdysvt{d8O0<5@!km>Rw|C)E)b(MvBQf z{wdxpz#=~j9SpG00*!koqB1>dL+A<^70JqUv=n~KV>yGTmiIWh{-c8YooNfF=O{`B zY@EUocF*}wjg^q21B>gZt=Q}&anl%$4J711ggP)~m?}&xu6MXrx}XLJNOaSi<~ITc1_fe|}qlyqaw6Ez@Ijv0hR`+;p+IEXT1Urbs zTJGCcPHvHdv4$?utRuNyZ!tZPUu6-IlAqIPWNCT~D@TPJh99i!@>PH+U3>* zd@S=A6RK_eQn^QQ+M&C|u0uGD5|VMi?5pP2SaVi+_JiSn-|%tEwVc`>04zOgOXwkR z&h2BZx)_*Djvt#7V-sKr+<()A*_!Kme^oj)G?M!ZzI*D74(&(ex_dfwS?h0mF+db0#B_! zh;<-JkTKUhg*oW(39$(?6Ow2;g8fC=+W`~9XjeXs%;IEOGKisSzv01qj}Gi|F5O2L zeM8ql*yboZW0W~kz*>}hTvOtHj@gC}A>YTqPW7e^zHHd>H)p{0lZyMw4vubLQwV!h zkYP#u=r<{mw|=E+c)1{-u{DrH)L6W-jNN$>+4%l8q_y`J`amsRPx~-sW=)(qKqwy; z;;k+{D|J#nQL6S6`Xo_^x(9};kc3yTUCIu6Gzs(e=tHPzI)k}S+XpR@Q~Ni{KIcj~ zJ&DU{qPwZXM*j;gKCEM*KyM_aCGwv$t=L~|Vt<>}h}gWOW>eaiXuu@xxn&IYyWqlD z{URM({UfG_%VWREKGJig%equS#Yp@VmpF!aNH7;xJNAI?ovGg72?81IuCFs#Xfmrh zdA|(kzKJpHyH$u?j1AS(Ws|4nlbtDr>oS|7 zU+Ec;;wS|FA#{_++%KYl9rq3z2KzPY+0+A9K>%0)(6}=CQ+n|b^54o!DwKP*og_9% zazFetF4XaLFkFiElV#`oRIPPgrtqbpb-E#iH`Ad8dNB*J z$zeM1*s_C3^_da+G_CE7nQAXXc1;7EzK1)?BU3;Gie5S-t2aCWs}L^C1W26+tqP0i z*b1{x?FMVM*%iSStAts@DRLIQHML$3h*2Spm zQ4DT0zVuJ6g7&X&mqfyRy52%im8{&wAav-!?j)pYDsf)bG>4VT@E8@~9pS+dyX9?Q zu4@wr#$kcpMx%2ZoNYi#hAa>y!M zZz^9Pz+hv|#EfyB3_=C2a@J$r4#1El6QO5S9W+kn)-mrS(|lV@L12QCsnJYm>BP1_ zI%f&v!4(I$rlhfAs%CDdKF!ds#yaj?3mb`@)iHu?S_wvLX{Cmu3Agq`Z(}^hj3Dgl z53vuS8e_vMyW7jOTt7TLz3}q}o}?+>*zb8mCUB&}Ui-~Co0EN-0 zRrHD+NX4Gckr2>@jr>JohqjGkNkC^#7j@}HO2C4^Z~GeJY|K9%gpu&7i^}-{$H(oO31FLEeGjb#Djez2&jxuMP6fUp)i} z`F}vWIAJ0h3%GTPyp_7?eG1k!&O52b(XWUd4l}e$P6}bq^=HWGh`)Mw+20}eVE+S~ zwcLoSKFOp*TT1Td9!O@sN;FmI*a~4IpSP5`-x6^y3M>39_SRDVbp%m;_(<-178*0r z-WuJL5`0tkcv2~avwQvQKwgNmRL9mw9Y_(}qwcY`z`CBgyTq?ro`5G5Qwx-7kD$K? z0P+&fGx*GFmHPS4D4H{TxDYJI`Vmj)vB82%6g?jKmjJMn4({neq^G3_Zf)FTBcdJC7p&c9(rdVQ#YV*SiBJv1-@PMme|Pko=k+C9w$mlU47Q;6JMVg zZ86{3k)LcA-(l`QjWz=Y-TKt@gq*!Xd6P;ur))tRhwb{(JhS9kAs6U{pj zvNPK1u#^{1l;4`xkWk!L76PSWb~~)LM60Bd0rUxfNS4l448Pbp`?8#RePpMdj3cqzrrj4EjM_J%g8{pxI~ zGrdd3hQ+rR#g47{X4+mZeX;Oa7VZ&LlVzUJmhv>dFnDV9QYa1eoxiSH_vWyh1<9*_ zQ{+Y9tt5ed0O6hnPJ1+LtO?&uu;dN*ZT-Y4>GwXpJz)AqCl0NrSO?mjhUJ8;-~jc`sG~6{(--46X+; zchq32kG$iF)qW{A*&~U=0Puyp7bs;wPhRVpBatkbx`((`W(HN_J(Dn_Wbt5ckLPZZ zaSyYe8Ed_{@R19-i-!?!D$zWLUVx;hJMfVjc3w$~vlREeAOOjhq~5YptF9YYX-+pl z0v27!NY<*@43jE#Vr5dYH=Vxr=9I3e@V>*-VXsG!J`y8x(E@}mPB$B*-m#_^dJwK2 z4#emo&O;c|gIMnghV00nFz*R)AD=U`Vbqh9uRMD=DBj$3SH+$pRa-tUL^He10qf{*8_DXD4#z_PjMibQgS^F&+eOqT1HA-0Y=uz8($1xW zxT!h}1Wm>TE-Qi=66HbO-mAxo*nVTM_>pqHvwU%m!`@KY5tZeW=h!S=uXn{1o~6y> z)+O%ST~Q+U0?#6R&l>SvmR>wpRO?5;K}UNIGF;#ORmN>Yc^y9(pn*>m@xrO>ghD0{$T4aj_hzG-wo->~e zvT817A9Q8T$Mfa)NS;=_r_4|b4F?T7jj~KY z`?nsZ@I>pU-UR4L9nt=N3yV*YRgdSbYlF$T|H`nLr>ee;yUnbL43d)nX~TABFV8_g zu_=tYnNqQF;0fhEfAx{Ey7?A9s9kzdVdu+Iy4Gdg zU{nZf8P`7hTK9ScgC8?z5Tsds-|h`XKbX86_?)7iO|S%dlU@vqB zjfY)+az#8;L({Swt-wxkIkp|TK+WK(P4f}W<_W|fDjQU)53PG?5YRyYJlpYiSq~JK zPuQ>A?|+E7=0^aT4cq>fU*yA{_hKjp{U*p1igB{rNl%XXq^;EWY<$k(s&X>yvLq)J9hy92ynsLVs?gzPmqS!A|gqckc7^P zB=VahxCa?RV$WG1w9&&3y{2fTSvkX<TF zP3L1wMv~5l4||F|RHnX7PM+)LpcIB6uoqBOiu%tIxqu7yK$qp{ndhJQ(!dK`^?% zKPDxoJ-drRv(yQI^6G%8STY^3PKMy6?PSjJ0O+J%89A;&lj^1P{a6veV03g3&-61g zh7LfWCXTaZ&-D-sj-&4<3Ql=y4?ATnf*9d6^@UECr_c(~lLODublIP(v!V#-(4-1I zluU^+4DO9cgk2dsDF}%gzBRGIhf9}T&*Tj0xAXJ+dKx~E-u=2E@H4?*?0n9QWf@`N zAaVv3Pu5DIbk0pS` zE(?G+VC}*)E(X0rx1vAm=rq)M-O|%;u=-ts)SA7`J9sL*Y$^Msw9`9y6idhWjhVgl zNY1cnYj!}B_8%UL{CZ^6%$@}^wA-V;f(Spn4K9v{2zO*zB~lim@b`t?u?sUC*q3;3 z<^DG%w6|!UJN(AGKmdXh3rZmXxQIf?#akcF?3TO%8Uje|EYe+qWJ8B|B!Paq0|vy= zg-608HpV`R4K>-|)Fs%9w=Sn3vRqoSb*ShhTSnyAl5-45|M->~z9=E@ZywoQ%wRZ5 zb68W)*(duIwlFEnJv2XOj=}pPel{LlTcxCX4tH8G^%`9bRSIad%-wK$Blvo`K|OOLKUZbJmY(&zyV?R z6UC)Z;@zw7zK2n9Wxab&|VFi^xn@~ma}%VI_HPF)mF`O zIYJ=Cndwyj7|2A0P_XGNEFptL336rbiA9#s@3rFCP0`lh&crt?*p4B2a*z-o;q>Fh zMj&M!w^D5R0u=I5-BgI#8!p9eg)>>i#68W!+3kctgr(ha9KNZyHsj@!~3jKhj3$7*fBMCu+YeY zQUp<_91p=To|L(7Hn6d7U@J3F&=GMM*cl1u7Nt!gZk$U&aM==;nz@1R@TsG>0S&S&7ml+MUMxnpx&l+93Og^ zjxBbI{22()UI~S#lbBAnJIpZZcx!i^>TwU~eI4;ww_bBKf2XSe?(9#m=4?vJ{-LLK z)s5mBx&|d=1re4j&}!_vtmXv`Mfcm5Ck#lm^}V2@sETRgW)L+lrDi=&NpMZA#+w)e z^{vAV)UHdm5e2TGbn`?ZQmsB=i+jyw9i-#k=n9_b-*ipF1<0!QW|J728^9)NV_;k* zP?rj@u;9KUi4M5g`G-DS5T(E{EP`QWP{Sh2x!4kkm3>xZzD;2h5wSP&j!;3gLF>-* zElJ50Jxn^c)_%b3tn$@jHOk_JdyxCdGm6O=Kgt~p{zUowTxTip4gZ-9AmTUM+$C(x z#ieYl29Xp!0)(#vhAj;2&9V!ctlvrW?~q|ND*{;Xf(KR42t;7Rf#UkyDbQym`T>^Fy?Y=Cyk1T*EIC{Ym8`h8#*tI1~tu&iiQgWxNIz z88Iy2T1|((SIR*jn*i9hEmmO$1~9WH^Z?I9^&{oPI>;o>s`erZlX}G@l`BQ-NdN&5 z2{A2fGN^_^MhLAjZW*}P8f`_>J4F(UUPpd!2h4oAXXsFQH4aHMcI>Hix3nsLuh2X&5qMkU0p5s$)C~0EU3^dN$UGXP=11Ezeoo0Xu85 zair5fGD;qCU2xKnH7y%e|v~%bV7uTL$!@s1g{xRg==u za)%9kgp1YO^c69lvW^ANeo5y*G;a^ZN(w7TzLF|A|5aoHwbZbB?aTiJ_d{o;!xZk2 z!X`a9@Xkd^`bAE^=jh9>_!MVE$UK)fA5wep!B9M4U2omBs zNbtZbD2T_%5h0NR2nmEl1m+b>iK9q~-88m|6fDP%Lqx!!?sVV2e`oKt=HQ{me~qeH zbFKC5eeQAHd-gZiob@xRYW$BeYYs89_hf?sw`hgIXhvrzIRNr`Wt~P-ie{>Lk}=ij z=-&Qq5IzW)pYi7k@GO+6C!y4}#!g?OM3~^A7N>B@owDZ{%QRr9S-2yUkJx~t2PqOU zy%;#DD)98T=j`zbR+nz@sy_I4s)&>CAn^}+@L>|p?NH_xtK3pkp-q5t> zv*jg3T1%xzx^5~>`Jh)MgHgIzMjo_Op~UOxPO6)yY9eQ2OViFv4GIoOXiy$lZyZqZ zs60ly2XG|@oK|JpxZM7c4HJe)-OM+!5)p%<&_765>&+G*dUK_0hy9?&gu}&0&be3V z!#<+Z%StRSm&)4PidN^TkKA;=b7}%)iLPYctLYHTA&)&$b^B2vRw%_7vg43Kd-Hs= zCy85kW-1z!48EnIt4XW!dlM%F!0Rsk@$od=4j!V_|DgeZo^|glrId14*pRg=Qm_B zAMEw9V&Q7WA$p~=9>SW`+M$2k9ggMKkUaGXr9@Udg4W|?qd|`9G<6FDb(g15=zfMc zznhRpOhx#0;tGi;N6j>6p|>qe=V%Ac)m^l4XcJ>ng0QADWt%)3kXGa483tt2v zLU5!bL}JsxaDiX~6TBX!Hoq6s0V~8}X}Npl`&X!1^y4N>C?yG#uy~-A~`PrH|*d=sgO6V)h!C=Lc)z1DSD1m5X{grAR-f|9wV>==L z1xVhl;N45#hjK?fp>85Kcf57>R-^%bOsg}BTQgwhIZD1F_#<7%O%_GGIt}yvV#j3J zb?*`<<$YAD>;RXB>6Hpc=G+5{g~kN$=@t^R2~5D}F}0gzl+}a%{6}s=k55N0S~n5q zH#M!Wyz~{Q;}u-mVsouE4_hCM9aWomIP^@AKN*N2>pOQ+~jUAOfeI9UR8$9^0WRVidWR$J|2{#IXKOWiZ`$P z^*~|jFUtD9Q;YrMR^b58U7G-V8Qgq(K*Rgz02hURkR-5a(Tw zPKu|Y#-;N!cGsfqKf^#yw0&-%R2DJI!v*ytw%k5S#0~hVsbbVMV!JZj=YEZkX>uLz?T!VFhV5G#j_f%F{aH&*| z>PlMBp~*M3&nLGK5y?J`lH7YN0J1upqDc;1?DVG+a;~FTv%Vs{wb|a;g8EdNNU4*j zN*Ur%+;Xae{hR?)LWAt_48f}Qq{Quh7+ojRT68}t?Th+}vI~=T8@D3?f3k@0FMv$Y ze1`}&9db`m2-7-8_Zq$zF^8cT@NQy*Bp}+L50o++K-SZ@;(nhkG{o7VWX0lnrtr%fwsf`Y4+DTc@Uoh^{MDYfQnl>3gnM3@4Bv_=gnho2q{#$p$5A2*?6Nz$iD1MAvZ|`-+$394eAu zb?o$L*%|Vc73lcRD{r|(N26My%*gdDA^>gid+lYB(;6sGB?hNbhriM%regsBiB^9|&d#E&c*Y?=pS&Noa^*vSXLFxqF zJ?VKU&g)g3-9yR!^EbPnsVSw^wB|6dc=5xVEu3&YKe_uN+tq;5d8W z7Kq{)#f>qXATS;-fsRvGE2&VU((kYIgXvDl{!(UVJ7OZ&A0Ck0MhWabiz(NaT`%_q zRVVU`A2DsV@X^lKZt#K4N^Lqjq56%7Lq+FHjH6_XJA`nE)u<+mkKiSP(B(FvQ`P&i zbz9q(k#^mgCC!KB{JcggG1`M6`w<9fsA9fGSe+>z(I%spYydHAERG!_I;fG zOhED??nF1&ZXGZ76+7}Ba8Rw$IH=lMnTGQH{*5n4a5ybmV4^|I$-ULmH`N_+nMY?d#HSO(8%s z2$Q|&@iIJEEyqE5KTOk2i-*IF4vykSitb+?)d}wq$#GgZ%i}6E7od07&v4X7+TvI$ zpBr<1a80T>0q;onpY3Z_6{f?9by|{$Z(StfcoVA})YUnd5;$TJW0H!E``!Rk79*8l*pht)7^C%;le@Bm$|QKV{0!`fb<%FSG<)tElC z%FP2$)3{kx*N<5h7s#%wC}d(bg_e}=#nG|oS-l&^qN9^#+cu~EX7~&BKB%dWmK4DN zDmJMqsn$>=566uiO@JezxqA933!wC^kE6FNjN_VfP)@WQ&#!pnW+E@MI1b*1e2Vd`tCG0o)T=)4$N^c5a@=Rf znIEBB)AF&{UIZjP*QG}|c@h4pC+?AlZ zqF6xa9fghuO4pXuE$1*5(>x&6h|!dFo5PYwbtMKkJm>xVzS4>#@Z5B=#_W|}6x-u>-x;YvM zW{wy|W3JF21ERGGM%VcvhNBw>xXp&5^-7Y;tOn&8B^#!LUb^~A{9t`I4$25L)q5r= zdg@j3+^}p)p_0F2=#j8S?O1TwQ`)P`YnCc{MeJ1?&uTd|(~e5!hrCWs(I{9*=JKQL zwf?6XBsDN&baLqfTy=;fyg%fA+MkDUGIvxyDvSs%71MeIh{d=(>YqcI0GLs?oI=uk zkH#+)<)UBD&=tP<{G#cYs?H^5)~{y64ErcroxrJ#ma~z>It?M|&{h5Z8#i9q`QxYa zS0@lLoR;i(;mU{ubbh&h0;_U!RgG9f|A*!=G94xj`dfotn=FFV$6^zuv0b7ZfYor#g3Ztor0DV#q#7L z5wsPqmML01txgI)OhUV-_A3oOhovfmy=?lDvp||xPhZpElkdev(lP9YiGhmK3m$b` zuKgo4&dC(wB8;x|U|Y2fm)zv=&6UxCK-}!KULc<@2rI&b*G+yrlH^5C> zkB#V7BYORLFAz7dsNf`BFSD0QQnCV#@)cCldV$+}r74F2XJx_mV;O2>*jGs7*5g#b zJRY_^EOQ;Y$7d%}8&l*LgNM3OH=I)=%nYRh_g#24p@1_%$ML=S{w}#>j<)P@Uo2$U z^V?&rO163!ae}keP*v|N=fvQNih)F5L%v+eW_!VLtUM@P>dstxQY$z8| zj9`fl5TAHG4qi%hNgZ8PvAnm&FS4)Vxog^vKx!D21M3arFutK5#q`{?-RLS9KX=OV z2-lrE3F}c`QPN7UtbAX?(m()|D7=~}F9c4%f7y4t#0m_CL!>kv)j9D&=-mZB8{nai zuoaXng&;i{aMC=dkPGL1*Y}(!|z}!YI-C3t}Kx{|E7@lKJ)bF*g zyj7=UAwVHirezzgVJf3+YTgFROJs#0EUj~70Qd~FfeDH&VYs()Bd8;WN!`U@2@{9& zOZN=tiviC8Y;RRcmW64tbk_ht{0)_X0s7o4NmNfT?hX;*=H)x87FgJDexQ=@->j5+ z)5@r)EUps&oTa8Gw3iI3W;Jx)UgvN+;#Qe0mH^q;MS8yG`r(+x!Tp{gJ$9C!RY_-ZKj|o6RbzHSY&wR|RiC}b} z#+j~(3!`zj3bhit>f4k>`>6MU8lW>S1h%^y!H#3poBRR_D`o0BZ&%$uEBW0iDK?xx zbFO4o7Oo%B4LaTNpii=&!_#1d5RA&viuuVx)zwm8fufxAz#Cb*vrKI#q>#K21>%6G2rpZ+bzKyRTzyID-+Jc z1M`$z41?+tFwy&?CncxVRSL?yu&I!pZMh%c^8g156>gS+4DDf>_9sNZq_J&dW&DW^t9ok#U0 z*B*#G14Kl0pNE>`n2L7E#L6R=8=PMT;SV^HZlx*?HQ-&L!2(XoQa0K=M$}WBn}6B# zwed#v;8>qg#w6E!&IB8axwPrvJ-m)phNKCR?`0+h*Aq`EeTXkRwADJ0BWvN%Pu_6>;-5a5@*QW*< zkACVf?n7)O*geUxBdNWrrW1gYL*_yH&mC391}eotAk{z%qDYmSszy-Lx;3(7Lv%wo z3|vuxk2F#U(Z}32{3<|?k7ux)F;-WK7~8qaGS;HWFW|huU5`K{uhd~`X=2j%X{6V$ zvuMAJ80g3rGV+wbz{=-ee0gkf>7gTFMolJ>nF3kbt!+Sv<`h7Gp<2#hk4>+gv%xlT zO5HdT&KO_zMyXZQj4?Brv>JzuIwMrRd0bA)f(iy7$=s< z{c^LGdr2$@iA6fua@cd&i(#p2L-DxJI(XY};dlkRY|^6YYw2=LQD5#Y*^`|j1kqri zAjE>>ujlyv;&!~ZXQAA<-{Mk)1ZewSP0==Ncu=zqJ{STR23op?tL3!eS`L>L8fulj zSoAe8DVV)!S`SlnH2^eNgo!AL`Ru%x7vg)>d(hI42P$jK7YrYb-X1_Q6lEhD2s@oL z*9hAYH_ooTIpox#k#pboUyL~>r0bz$b#YC74gxpKj{j#jsB^;=r4%eUmf#w(VHYfl zZKFep=EqcvPQWH``mr_RrEzrwf^8^=2Qr9LG&M`vgW=#Y>$+cqkzG};U@4*(i}Oe+ z1~^5W>r?_SEuJ1jRMi~7mnCcR*@VX`)zRs&(p%r9&-`(VOny}Ivy*6e)=yjdhgwG3 z(4hmOL4g2BxXU)5(yw0avl|vH&n$0`fH6S0l~AqxJmNOP^cD4M7ttFYT933*FP?N8 zwW5$w3}N9oub83Lth6e$j*=gt2sMDpPmjw60KxP9cp3LWdXAbfltvY9S;}@O@eJ}D z_c>TGB$1BMx~aStIq`gtWBUyU_?|LO|A)y533ivCf;RiiXGX{VRT~;whK_^*@`nLe z<=tX(#Cq`=mEeKk7hb}+iGef?^`y-#jMZ(4uuc(1WcMKDSF2>zBkTki1>b{Y5f?<| z3Oa;}PH=43Wk`lyR*fo$ZCz=<+~nIQsXB#DL$)ijGkEC`LeNAMK#DC`34n; zYtK^S&VMjyV(c=pQ}Q^?*$(HGL1X@mbPFX{I=j$^TQhQJpY7SH9Il%f-U?NZM?sKR zlr3_e*l7HVEXX3|C}bi%ilL}ceqdVx_?!~cqJvI@Eax=EjZVRSfzAMdKz_eVrR#dJ zZPyhPJ3K4cWepT#Ou8Z6rnStTK!$-M$TFx507%-y(QLfmv8(&+PDus_B!qdAk1nZk z4;2d`hvlCkTXYrKng_LPfH1)*(m|6JG{>dXVP(y_j!Y4*z`-*^JymeRqg)xCx_&_W zo?djWNh=`LI}a&@^`;}5Z*@hR>lHl~u04^}0E94ag@Gwb3IOi4N3GOV84Oldc`Y)$ z%GnXr(^TsF%xb0aut8hpBTa%EqS>oG8aWO%>hf zm`T^$FuBqzOGa?(vl>CrmEMx5UeI{7Jnb(?w~rfEARMsLVJ}D7o1saT{!zQ>92=xT zY`#;H(77lkc1jAz@kPz`2Wlt}7^sj@^w{ zhyW0F+5te{bwP0e#Q3#)8!aDo?glL3h;Ol3j;V7oX7`KF^Y+Z1awlmv^V4NjrGL4| z%v6T9o+TL2AW27?@=foM)eZ~W`xNFY&c<{FqNv1h=UZt2>dO`O?hSGzi=yNXB1erJ zM5JcfvNx;E3>isO1>ED<3y3W6WmGz@eWsZEZ4hRq@CO1o1c&1tDz6sE2(AocDY{PN zYGJIZn_}I#cZaG(aX+eO&9WM9Ju-i5gpj83O7bfth(i`bdad3?-?U{g$+Z#M!j;XnR)=0CZMs22kjF+&InE0jM4%BgnNc0)po6TGd2^526k8p_+o_7MBI#2?83E+a z8yA?35SJQywK;(oqJKpBCIZT_FtVH8zN)u1fTRb!DN!%&h%Ed06qA!>*mn+R)^IdO zOyy;3eQyw$c_6$rws2@pW96V;tbJ&n{7^q5vwNxYy{DXsv^e9yaT5YJ0&-OT7P(cHA9?`{V@*fRR9^D$vx|C)rd~{dEhlLmWS`@me70J|7?5hDx14u5ZdPmVWVGPE7CO8M!$}XZVLqZV`AT4F zI*|X!O{sqrx~%&zHSr11vvjW`-*5**R2G+uW4*%@S8W&DFAS^c7^MQ~yVncx5d`7a z{KBSP_4^Qw+rd6Xj-QSW9ty+(o$445bKBLjW~-E&URso+QM4rO#O~YIKEgI}BS*H5 z9rM2wH?Y^0gOEvVd3JxbA`2PQz}MKvfo(P%2t0pldV z!S>drXSNhBG)`63kREvKBUPx!heNB?maY)&p}AfMPN{pwk`4Q#l3XY8D`X0IfRG#~ zkO8^?radH9Q&o7IOVQ2Ei5hulS9~6$l{s0uIPsX#4ZDz*Rqvx9N^2L zALVlljmEOJ)2U(7)6fB-C9D)~06W;m?&=6mD_U-ROR__WEh-3ScjFn=>r}%^`b~O1 z_y~%0#K*lV+e`k1K1T^EO_i!dO;O<3yN zGfJBd4gsd!q`W*uKSkO|T5Da*fFsSnEwd(H&9~!++A3@Xirn*Es755SuvmiRP8Asi z{R9EPo2+G~?xel?1G@Wq2>5GUfF|m)rUlIYjz+2H5mgHG2ex>`OW(^jyp(UcVqH0m z>5O@;X_W?~p0=6mr^s&4YB#Nz=PP1>p?`~VpueW+pXs;U#REap5*|A9#VN;jKNxAU z*k~WraGOg&$bm-xVc%#4$sX})TSE}3=eQ%u6o)$8UIkgRo%0aAk(e~t%k2&UBT&xT zaj8C{sw}Ej5$AD0tx^~5m^25aNvH!Ofmvy18fEVr+egZ@S%)Gae<57Qy8W-n#1+hP z&WEOAUoc98;|9cemE^I}Nw|*fEfrnHkt`5k6=s5t%V~ana0ii_9D*K+Zw%X1M(iXs zr6gK*>ZeUtgQnw^OzAvy7s~VXyS51IbBVd)!D5TaltWK(!NaFz4U1mM_emI|WEPN| z&fj>0F%C8FC}!aEAMfs0v5Mr^(DCND-L}o`E`x-)87q%Cqa}E#1`g8ji2yrvO}7O$kbv9qlv(>uMFXifC=e$uH%$H7~Yb=u%RQHKX_(| z#b4T{4j7@;noQfjXkw!IreuG);n8BVFYD|_ZssMe*TW^~Qvip`&`R0LnPU2dr7hTe z8?GJ-t+o1Y=|!%%w1;IdU&E?!w4;H)zs~vp1xsZ^6*rh?%2v%ZlEm0Ygf!{^-81`( zJ$E1suBl|SOdy=^+TxxTy}N}HkZ6U9Um_a(4}ZMtBOP=U9e>xo`XHo7fmFL&q*!$k zVsw*q=G@t$aIaN=U9d3>4 zS!K-iSI{IhS~i~+OZFE$*<0FGZ#`B4ho(lUG8%NFCQqFfQ`oB^N$HD>xO>HJP_Q5~YSw9Lvp;AbQYj0b zE^mlO@dIk>CG1@l_p_gmK)(PJ2DWgv^+^G_REFH;lq7c0zsgBL@pH2Mq8-E_OBc<^ zG2y!%{BF+Z$&Krz2F1jlmEx^dL{z8;5L9z$pv8N+O7yRyMydM|F zTQ(`vNQ>5aDgW5_`5Czsc39SR07LJ2qDrc|9tePTx8NIWt#(S*_jg1~OZ|ESB+2cD zFsmPYTPgkgr%Bw?O(PD2HGb(;*0~9z=vr0X>OTAp8)>htB~C`rZ`8D=93>4tKb@h$ zv?)OgvDlN#k z)t;u^l>A^0XQv{84{uK)F}Yv>@#`sC)9AkA%kaV5@1 z?#11~+_yoE@gN7ToyriMlu5o~W3oo|07zwgV7Ua9>3q`R60|V=NRcf%S^?P& z{y)n9L2mINp%c%GJsw02&K>FW`AB9~IveH@%$T1yfrIN3g7H0Vzl6O02<|xgAhD_& zIRSR5{jdLC7yH>(I!GVJ30L z3yjg)f;>se3h)u&{NwEiSmAzH1%JQ`F>)o8WgW2eIvjR%*>Op`J1nJkX5!b0S_CY< zHaqLSP(bI@fly0tTqCQuDt;1Mc~=RBAN47t?RAbSogvo}d(}D*eYip4jQY`S#)~5# z%cTge^_B3@%0yV6!V6KSpjO)gjxW~LgtLzJOket_a#3=NklMXmJW|b~tS5RbnPcgK zM%&M40VWO~L0X&x-nvx0@*ZIu_-MMH-QLLC*i)2G4nI|b>`W%_CbAcf zJE5Z2vh@C}B6SMd26c-TOl4P>l(0 zC$nTwf(4kV)zO9%cZJDO#9j5&cY}f>#0Eiqi^)llN0P zsHB}uE$-AUj5%Bq#`8(>c!6T0@C&R3u`*R+Zkv?ZfKULewzOb6$RFSsJ$rfs~ zWf7M&LuH{3(c>Ee6!FkyFuEOzb9d-4PeQbq<*nVmK7Fj^KBUKnlJm&}9uhkc?|Hq4MU-+N%@1ST^A4kNR^OSVUCTOe5r0*E@Wf|W5C z;RuL6)h92&p!@#z3Sll5p$i&FuH}NRH?*`jpU34Blyd7l9aLSpN2II)vunQ%IXd)0 z6d71pvZK6!Qzhzj210`3kpPr(#U5BcPJ4xWuJ-j_SfK!LxZTV5VJNT6bHwgKuakj$ z5)XCDC6a(Wv<>16=c!@Fqo)gJ)1{i_ie)L9Fj%o=vWI9$o5AT(YrMP;4camyHsg(A z8d7jNqzi%hu$&W!r0SrDRvC5ZN=5%@(7B?){Y2`GK!R#KV zkUky1N!7I4SwQaf`23mnz?39vYKms4~DDyn)l|t z=V!}A`%5ICd1qY@iN#BE`#V%*oun+%3q_+QF{pm0!LEW%4 z7yyX;ijC2yOb7S2!6_QHL?s7WmkxjUv?7Cke}k-{0!9=Kt+!X9Ks|eW#A7|I4;52! zgsgPr)-`U;A*ywqS|l=qzj~v*z#5N+(?JPogAH6po^fVhQ}uP>9=$dx+MvGdpBw81V>6ry?7-AwvJ9E{fQO0b$B5lNm+3OR3p0=W#&* zpnc!>2A_6GyAwqJAR27HSV{D`3&Zw{&B*LL^G(0ip0VO#&|ItGh$9Pl3)gMZcS1KnRA@Y zPdr`bc_{j5GUK;q0jK!XfNQr^Ho=QUljj!N4T}R00|D(o0QI+aPY-x{-6tsjNI|hn zWys};EY`l^kD_z7T- z%aK}frX%g}Ky0s4K88Po4!hPlxolts9S|eBe@lG;0h`BAJpOd>t&i7hRSy&32@UU1 z?Zj?!?nWWklDlVc4Nyq3lyPv{nDk&WEt*rq4Mjj4IloHXZ=%ilsvHUSyNsTyWv{GO zIUeXbx)d@{I$)8@@^_>r>!q_)%^Ry>Sh8wLy_hmdkG%3`f*rA_c_|#jgMsCY8Hu1g zPO(Ga)O4ⅇ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 +} diff --git a/docs/src/app/(shaders)/fluted-glass/page.tsx b/docs/src/app/(shaders)/fluted-glass/page.tsx index 996e5ebcb..1852fa7b8 100644 --- a/docs/src/app/(shaders)/fluted-glass/page.tsx +++ b/docs/src/app/(shaders)/fluted-glass/page.tsx @@ -14,16 +14,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { flutedGlassDef } from '@/shader-defs/fluted-glass-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = flutedGlassPresets[0].params; -const controlRules: ControlRules = { - colorShadow: { showWhen: { shadows: { moreThan: 0 } } }, - colorHighlight: { showWhen: { highlights: { moreThan: 0 } } }, - shift: { showWhen: { distortion: { moreThan: 0 } } }, -}; - const imageFiles = [ '001.webp', '002.webp', @@ -74,13 +67,13 @@ const FlutedGlassWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorShadow: { value: toHsla(defaults.colorShadow), order: 101 }, + colorHighlight: { value: toHsla(defaults.colorHighlight), order: 102 }, + size: { value: defaults.size, min: 0.01, max: 1, step: 0.01, order: 210 }, shadows: { value: defaults.shadows, min: 0, max: 1, order: 200 }, - colorShadow: { value: toHsla(defaults.colorShadow), order: 200.5 }, highlights: { value: defaults.highlights, min: 0, max: 1, order: 201 }, - colorHighlight: { value: toHsla(defaults.colorHighlight), order: 201.5 }, - size: { value: defaults.size, min: 0.01, max: 1, step: 0.01, order: 210 }, shape: { value: defaults.shape, options: Object.keys(GlassGridShapes) as GlassGridShape[], @@ -117,7 +110,7 @@ const FlutedGlassWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }, controlRules); + }; }); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/(shaders)/gem-smoke/page.tsx b/docs/src/app/(shaders)/gem-smoke/page.tsx index fd4b78edf..7312f38e6 100644 --- a/docs/src/app/(shaders)/gem-smoke/page.tsx +++ b/docs/src/app/(shaders)/gem-smoke/page.tsx @@ -15,15 +15,9 @@ import { useUrlParams } from '@/helpers/use-url-params'; import { gemSmokeDef } from '@/shader-defs/gem-smoke-def'; import { toHsla } from '@/helpers/color-utils'; import { useColors } from '@/helpers/use-colors'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = gemSmokePresets[0].params; -const controlRules: ControlRules = { - outerDistortion: { showWhen: { outerGlow: { moreThan: 0 } } }, - innerDistortion: { showWhen: { innerGlow: { moreThan: 0 } } }, -}; - const imageFiles = [ 'contra.svg', 'apple.svg', @@ -72,7 +66,7 @@ const GemSmokeWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorInner: { value: toHsla(defaults.colorInner), order: 101 }, shape: { @@ -81,10 +75,10 @@ const GemSmokeWithControls = () => { order: 102, disabled: Boolean(image), }, - outerGlow: { value: defaults.outerGlow, min: 0, max: 1, order: 201 }, - outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 201.5 }, - innerGlow: { value: defaults.innerGlow, min: 0, max: 1, order: 202 }, - innerDistortion: { value: defaults.innerDistortion, min: 0, max: 1, order: 202.5 }, + innerDistortion: { value: defaults.innerDistortion, min: 0, max: 1, order: 201 }, + outerDistortion: { value: defaults.outerDistortion, min: 0, max: 1, order: 202 }, + outerGlow: { value: defaults.outerGlow, min: 0, max: 1, order: 203 }, + innerGlow: { value: defaults.innerGlow, min: 0, max: 1, order: 204 }, offset: { value: defaults.offset, min: -1, max: 1, order: 205 }, angle: { value: defaults.angle, min: 0, max: 360, order: 250 }, size: { value: defaults.angle, min: 0.1, max: 1, order: 251 }, @@ -98,7 +92,7 @@ const GemSmokeWithControls = () => { 'Upload image': levaImageButton((img?: HTMLImageElement) => setImage(img ?? '')), ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), }, { order: -1 }), - }, controlRules); + }; }, [colors.length, image]); useControls(() => { diff --git a/docs/src/app/(shaders)/god-rays/page.tsx b/docs/src/app/(shaders)/god-rays/page.tsx index 7a47cfc47..219ed2f66 100644 --- a/docs/src/app/(shaders)/god-rays/page.tsx +++ b/docs/src/app/(shaders)/god-rays/page.tsx @@ -12,14 +12,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { godRaysDef } from '@/shader-defs/god-rays-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = godRaysPresets[0].params; -const controlRules: ControlRules = { - colorBloom: { showWhen: { bloom: { moreThan: 0 } } }, -}; - const GodRaysWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -27,10 +22,10 @@ const GodRaysWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorBloom: { value: toHsla(defaults.colorBloom), order: 101 }, bloom: { value: defaults.bloom, min: 0, max: 1, order: 200 }, - colorBloom: { value: toHsla(defaults.colorBloom), order: 200.5 }, intensity: { value: defaults.intensity, min: 0, max: 1, order: 201 }, density: { value: defaults.density, min: 0, max: 1, order: 204 }, spotty: { value: defaults.spotty, min: 0, max: 1, order: 205 }, @@ -41,7 +36,7 @@ const GodRaysWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - }, controlRules); + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/halftone-dots/page.tsx b/docs/src/app/(shaders)/halftone-dots/page.tsx index 3cc764fdf..978ba5de5 100644 --- a/docs/src/app/(shaders)/halftone-dots/page.tsx +++ b/docs/src/app/(shaders)/halftone-dots/page.tsx @@ -19,14 +19,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { halftoneDotsDef } from '@/shader-defs/halftone-dots-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = halftoneDotsPresets[0].params; -const controlRules: ControlRules = { - colorFront: { showWhen: { originalColors: { is: false } } }, -}; - const imageFiles = [ '001.webp', '002.webp', @@ -77,10 +72,10 @@ const HalftoneDotsWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - originalColors: { value: defaults.originalColors, order: 101 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101.5 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + originalColors: { value: defaults.originalColors, order: 102 }, type: { value: defaults.type, options: Object.keys(HalftoneDotsTypes) as HalftoneDotsType[], @@ -112,7 +107,7 @@ const HalftoneDotsWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }, controlRules); + }; }); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/(shaders)/image-dithering/page.tsx b/docs/src/app/(shaders)/image-dithering/page.tsx index e8fa0f5be..bb9e81dec 100644 --- a/docs/src/app/(shaders)/image-dithering/page.tsx +++ b/docs/src/app/(shaders)/image-dithering/page.tsx @@ -13,16 +13,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { imageDitheringDef } from '@/shader-defs/image-dithering-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = imageDitheringPresets[0].params; -const controlRules: ControlRules = { - colorBack: { showWhen: { originalColors: { is: false } } }, - colorFront: { showWhen: { originalColors: { is: false } } }, - colorHighlight: { showWhen: { originalColors: { is: false } } }, -}; - const imageFiles = [ '001.webp', '002.webp', @@ -73,11 +66,11 @@ const ImageDitheringWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return applyControlRules({ - originalColors: { value: defaults.originalColors, order: 100 }, - colorBack: { value: toHsla(defaults.colorBack), order: 100.1 }, - colorFront: { value: toHsla(defaults.colorFront), order: 100.2 }, - colorHighlight: { value: toHsla(defaults.colorHighlight), order: 100.3 }, + return { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFront: { value: toHsla(defaults.colorFront), order: 102 }, + colorHighlight: { value: toHsla(defaults.colorHighlight), order: 103 }, + originalColors: { value: defaults.originalColors, order: 104 }, inverted: { value: defaults.inverted, order: 105 }, type: { value: defaults.type, options: Object.keys(DitheringTypes) as DitheringType[], order: 200 }, size: { value: defaults.size, min: 1, max: 20, order: 201 }, @@ -91,7 +84,7 @@ const ImageDitheringWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }, controlRules); + }; }); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/(shaders)/lens-distortion/layout.tsx b/docs/src/app/(shaders)/lens-distortion/layout.tsx new file mode 100644 index 000000000..d7d050661 --- /dev/null +++ b/docs/src/app/(shaders)/lens-distortion/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Lens Distortion • Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/(shaders)/lens-distortion/page.tsx b/docs/src/app/(shaders)/lens-distortion/page.tsx new file mode 100644 index 000000000..da05b3c2a --- /dev/null +++ b/docs/src/app/(shaders)/lens-distortion/page.tsx @@ -0,0 +1,121 @@ +'use client'; + +import { LensDistortion, lensDistortionPresets } from '@paper-design/shaders-react'; +import { useControls, button, folder } from 'leva'; +import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; +import { usePresetHighlight } from '@/helpers/use-preset-highlight'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { ShaderFit, lensDistortionMeta } from '@paper-design/shaders'; +import { levaImageButton } from '@/helpers/leva-image-button'; +import { useState, useEffect, useCallback } from 'react'; +import { ShaderDetails } from '@/components/shader-details'; +import { lensDistortionDef } from '@/shader-defs/lens-distortion-def'; +import { ShaderContainer } from '@/components/shader-container'; +import { useUrlParams } from '@/helpers/use-url-params'; + +const { worldWidth, worldHeight, ...defaults } = lensDistortionPresets[0].params; + +const imageFiles = [ + '001.webp', + '002.webp', + '003.webp', + '004.webp', + '005.webp', + '006.webp', + '007.webp', + '008.webp', + '009.webp', + '0010.webp', + '0011.webp', + '0012.webp', + '0013.webp', + '0014.webp', + '0015.webp', + '0016.webp', + '0017.webp', + '0018.webp', +] as const; + +const LensDistortionWithControls = () => { + const [imageIdx, setImageIdx] = useState(-1); + const [image, setImage] = useState('/images/image-filters/0018.webp'); + + useEffect(() => { + if (imageIdx >= 0) { + const name = imageFiles[imageIdx]; + const img = new Image(); + img.src = `/images/image-filters/${name}`; + img.onload = () => setImage(img); + } + }, [imageIdx]); + + const handleClick = useCallback(() => { + setImageIdx((prev) => (prev + 1) % imageFiles.length); + }, []); + + const setImageWithoutStatus = useCallback((img?: HTMLImageElement) => { + setImage(img ?? ''); + setImageIdx(-1); + }, []); + + const [params, setParams] = useControls(() => { + const presets = Object.fromEntries( + lensDistortionPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => setParamsSafe(params, setParams, preset)), + ]) + ); + return { + spread: { value: defaults.spread, min: 0, max: 1, order: 100 }, + bias: { value: defaults.bias, min: -1, max: 1, order: 101 }, + angle: { value: defaults.angle, min: 0, max: 360, order: 102 }, + perspective: { value: defaults.perspective, min: 0, max: 1, order: 103 }, + count: { value: defaults.count, min: 2, max: lensDistortionMeta.maxSamples, step: 1, order: 104 }, + dispersion: { value: defaults.dispersion, min: 0, max: 1, order: 105 }, + dispersionShift: { value: defaults.dispersionShift, min: -1, max: 1, order: 106 }, + dispersionColor: { value: defaults.dispersionColor, min: 0, max: 1, order: 107 }, + focusCenter: { value: defaults.focusCenter, min: 0, max: 1, order: 204 }, + focusEdges: { value: defaults.focusEdges, min: 0, max: 1, order: 205 }, + swirl: { value: defaults.swirl, min: -1, max: 1, order: 280 }, + noise: { value: defaults.noise, min: 0, max: 1, order: 300 }, + noiseFrequency: { value: defaults.noiseFrequency, min: 0, max: 1, order: 301 }, + noiseOffset: { value: defaults.noiseOffset, min: 0, max: 1, order: 302 }, + lensBulge: { value: defaults.lensBulge, min: -1, max: 1, order: 400 }, + lensCircle: { value: defaults.lensCircle, min: 0, max: 1, order: 402 }, + grainMixer: { value: defaults.grainMixer, min: 0, max: 1, order: 409 }, + grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 410 }, + imageX: { value: defaults.imageX, min: -1, max: 1, order: 411 }, + imageY: { value: defaults.imageY, min: -1, max: 1, order: 412 }, + scale: { value: defaults.scale, min: 0.5, max: 4, order: 450 }, + fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 451 }, + Image: folder( + { + 'Upload image': levaImageButton(setImageWithoutStatus), + }, + { order: 0 } + ), + Presets: folder(presets, { order: -1 }), + }; + }); + + // Reset to defaults on mount, so that Leva doesn't show values from other + // shaders when navigating (if two shaders have a color1 param for example) + useResetLevaParams(params, setParams, defaults); + useUrlParams(params, setParams, lensDistortionDef); + usePresetHighlight(lensDistortionPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + +
    + Click to change the sample image +
    + + + ); +}; + +export default LensDistortionWithControls; diff --git a/docs/src/app/(shaders)/pulsing-border/page.tsx b/docs/src/app/(shaders)/pulsing-border/page.tsx index 1bf16ec44..81e20750d 100644 --- a/docs/src/app/(shaders)/pulsing-border/page.tsx +++ b/docs/src/app/(shaders)/pulsing-border/page.tsx @@ -12,21 +12,16 @@ import { ShaderDetails } from '@/components/shader-details'; import { pulsingBorderDef } from '@/shader-defs/pulsing-border-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = pulsingBorderPresets[0].params; -const controlRules: ControlRules = { - smokeSize: { showWhen: { smoke: { moreThan: 0 } } }, -}; - const PulsingBorderWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, maxColorCount: pulsingBorderMeta.maxColorCount, }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, roundness: { value: defaults.roundness, min: 0, max: 1, order: 200 }, thickness: { value: defaults.thickness, min: 0, max: 1, order: 201 }, @@ -59,7 +54,7 @@ const PulsingBorderWithControls = () => { marginRight: { value: defaults.marginRight, min: 0, max: 0.5, order: 403 }, marginTop: { value: defaults.marginTop, min: 0, max: 0.5, order: 403 }, marginBottom: { value: defaults.marginBottom, min: 0, max: 0.5, order: 403 }, - }, controlRules); + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/spiral/page.tsx b/docs/src/app/(shaders)/spiral/page.tsx index 21307379c..79e1b127b 100644 --- a/docs/src/app/(shaders)/spiral/page.tsx +++ b/docs/src/app/(shaders)/spiral/page.tsx @@ -10,11 +10,6 @@ import { ShaderDetails } from '@/components/shader-details'; import { spiralDef } from '@/shader-defs/spiral-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; - -const controlRules: ControlRules = { - noiseFrequency: { showWhen: { noise: { moreThan: 0 } } }, -}; const firstPresetParams = spiralPresets[0].params; const { worldWidth, worldHeight, ...defaults } = { @@ -26,7 +21,7 @@ const { worldWidth, worldHeight, ...defaults } = { const SpiralWithControls = () => { const [params, setParams] = useControls(() => { - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, density: { value: defaults.density, min: 0, max: 1, order: 200 }, @@ -42,7 +37,7 @@ const SpiralWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - }, controlRules); + }; }); useControls(() => { diff --git a/docs/src/app/(shaders)/static-mesh-gradient/page.tsx b/docs/src/app/(shaders)/static-mesh-gradient/page.tsx index 42d219087..e156d43bd 100644 --- a/docs/src/app/(shaders)/static-mesh-gradient/page.tsx +++ b/docs/src/app/(shaders)/static-mesh-gradient/page.tsx @@ -11,15 +11,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { staticMeshGradientDef } from '@/shader-defs/static-mesh-gradient-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = staticMeshGradientPresets[0].params; -const controlRules: ControlRules = { - waveXShift: { showWhen: { waveX: { moreThan: 0 } } }, - waveYShift: { showWhen: { waveY: { moreThan: 0 } } }, -}; - const StaticMeshGradientWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -27,7 +21,7 @@ const StaticMeshGradientWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { positions: { value: defaults.positions, min: 0, max: 100, order: 200 }, waveX: { value: defaults.waveX, min: 0, max: 1, order: 201 }, waveXShift: { value: defaults.waveXShift, min: 0, max: 1, order: 202 }, @@ -40,7 +34,7 @@ const StaticMeshGradientWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 301 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 302 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 303 }, - }, controlRules); + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/static-radial-gradient/page.tsx b/docs/src/app/(shaders)/static-radial-gradient/page.tsx index 16c8942ba..23de248bd 100644 --- a/docs/src/app/(shaders)/static-radial-gradient/page.tsx +++ b/docs/src/app/(shaders)/static-radial-gradient/page.tsx @@ -12,16 +12,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { staticRadialGradientDef } from '@/shader-defs/static-radial-gradient-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = staticRadialGradientPresets[0].params; -const controlRules: ControlRules = { - focalAngle: { showWhen: { focalDistance: { moreThan: 0 } } }, - distortionShift: { showWhen: { distortion: { moreThan: 0 } } }, - distortionFreq: { showWhen: { distortion: { moreThan: 0 } } }, -}; - const StaticRadialGradientWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -29,12 +22,12 @@ const StaticRadialGradientWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, radius: { value: defaults.radius, min: 0, max: 3, order: 200 }, focalDistance: { value: defaults.focalDistance, min: 0, max: 3, order: 201 }, - focalAngle: { value: defaults.focalAngle, min: 0, max: 360, order: 201.5 }, - falloff: { value: defaults.falloff, min: -1, max: 1, order: 202 }, + focalAngle: { value: defaults.focalAngle, min: 0, max: 360, order: 202 }, + falloff: { value: defaults.falloff, min: -1, max: 1, order: 201 }, mixing: { value: defaults.mixing, min: 0, max: 1, order: 204 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 205 }, distortionShift: { value: defaults.distortionShift, min: -1, max: 1, order: 206 }, @@ -43,7 +36,7 @@ const StaticRadialGradientWithControls = () => { grainOverlay: { value: defaults.grainOverlay, min: 0, max: 1, order: 209 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 300 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 301 }, - }, controlRules); + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/swirl/page.tsx b/docs/src/app/(shaders)/swirl/page.tsx index 2658165cc..cd7c4e32f 100644 --- a/docs/src/app/(shaders)/swirl/page.tsx +++ b/docs/src/app/(shaders)/swirl/page.tsx @@ -11,14 +11,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { swirlDef } from '@/shader-defs/swirl-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = swirlPresets[0].params; -const controlRules: ControlRules = { - noiseFrequency: { showWhen: { noise: { moreThan: 0 } } }, -}; - const SwirlWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -26,7 +21,7 @@ const SwirlWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, bandCount: { value: defaults.bandCount, min: 0, max: 15, step: 1, order: 200 }, twist: { value: defaults.twist, min: 0, max: 1, order: 201 }, @@ -40,7 +35,7 @@ const SwirlWithControls = () => { rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, offsetX: { value: defaults.offsetX, min: -1, max: 1, order: 303 }, offsetY: { value: defaults.offsetY, min: -1, max: 1, order: 304 }, - }, controlRules); + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/voronoi/page.tsx b/docs/src/app/(shaders)/voronoi/page.tsx index 9038f2165..223590bed 100644 --- a/docs/src/app/(shaders)/voronoi/page.tsx +++ b/docs/src/app/(shaders)/voronoi/page.tsx @@ -12,15 +12,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { voronoiDef } from '@/shader-defs/voronoi-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = voronoiPresets[0].params; -const controlRules: ControlRules = { - colorGap: { showWhen: { gap: { moreThan: 0 } } }, - colorGlow: { showWhen: { glow: { moreThan: 0 } } }, -}; - const VoronoiWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -28,20 +22,17 @@ const VoronoiWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules( - { - stepsPerColor: { value: defaults.stepsPerColor, min: 1, max: 3, step: 1, order: 200 }, - distortion: { value: defaults.distortion, min: 0, max: 0.5, order: 201 }, - gap: { value: defaults.gap, min: 0, max: 0.1, order: 202 }, - colorGap: { value: toHsla(defaults.colorGap), order: 202.5 }, - glow: { value: defaults.glow, min: 0, max: 1, order: 203 }, - colorGlow: { value: toHsla(defaults.colorGlow), order: 203.5 }, - speed: { value: defaults.speed, min: 0, max: 1, order: 300 }, - scale: { value: defaults.scale, min: 0.01, max: 4, order: 301 }, - rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - }, - controlRules - ); + return { + colorGlow: { value: toHsla(defaults.colorGlow), order: 100 }, + colorGap: { value: toHsla(defaults.colorGap), order: 101 }, + stepsPerColor: { value: defaults.stepsPerColor, min: 1, max: 3, step: 1, order: 200 }, + distortion: { value: defaults.distortion, min: 0, max: 0.5, order: 201 }, + gap: { value: defaults.gap, min: 0, max: 0.1, order: 202 }, + glow: { value: defaults.glow, min: 0, max: 1, order: 203 }, + speed: { value: defaults.speed, min: 0, max: 1, order: 300 }, + scale: { value: defaults.scale, min: 0.01, max: 4, order: 301 }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/warp/page.tsx b/docs/src/app/(shaders)/warp/page.tsx index 182c5ca31..6ec92ae40 100644 --- a/docs/src/app/(shaders)/warp/page.tsx +++ b/docs/src/app/(shaders)/warp/page.tsx @@ -11,14 +11,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { warpDef } from '@/shader-defs/warp-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = warpPresets[0].params; -const controlRules: ControlRules = { - swirlIterations: { showWhen: { swirl: { moreThan: 0 } } }, -}; - const WarpWithControls = () => { const { colors, setColors } = useColors({ defaultColors: defaults.colors, @@ -26,18 +21,18 @@ const WarpWithControls = () => { }); const [params, setParams] = useControls(() => { - return applyControlRules({ + return { 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 }, swirl: { value: defaults.swirl, min: 0, max: 1, order: 103 }, - swirlIterations: { value: defaults.swirlIterations, min: 1, max: 20, step: 1, order: 104 }, + swirlIterations: { value: defaults.swirlIterations, min: 0, max: 20, order: 104 }, shape: { value: defaults.shape, options: Object.keys(WarpPatterns) as WarpPattern[], order: 105 }, shapeScale: { value: defaults.shapeScale, min: 0, max: 1, order: 106 }, speed: { value: defaults.speed, min: 0, max: 20, order: 300 }, scale: { value: defaults.scale, min: 0.01, max: 5, order: 301 }, rotation: { value: defaults.rotation, min: 0, max: 360, order: 302 }, - }, controlRules); + }; }, [colors.length]); useControls(() => { diff --git a/docs/src/app/(shaders)/water/page.tsx b/docs/src/app/(shaders)/water/page.tsx index 6af5d7981..eb3796001 100644 --- a/docs/src/app/(shaders)/water/page.tsx +++ b/docs/src/app/(shaders)/water/page.tsx @@ -13,14 +13,9 @@ import { ShaderDetails } from '@/components/shader-details'; import { waterDef } from '@/shader-defs/water-def'; import { ShaderContainer } from '@/components/shader-container'; import { useUrlParams } from '@/helpers/use-url-params'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; const { worldWidth, worldHeight, ...defaults } = waterPresets[0].params; -const controlRules: ControlRules = { - colorHighlight: { showWhen: { highlights: { moreThan: 0 } } }, -}; - const imageFiles = [ '001.webp', '002.webp', @@ -71,10 +66,9 @@ const WaterWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - return applyControlRules({ + return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, highlights: { value: defaults.highlights, min: 0, max: 1, order: 200 }, - colorHighlight: { value: toHsla(defaults.colorHighlight), order: 200.5 }, layering: { value: defaults.layering, min: 0, max: 1, order: 201 }, edges: { value: defaults.edges, min: 0, max: 1, order: 202 }, waves: { value: defaults.waves, min: 0, max: 1, order: 203 }, @@ -91,7 +85,7 @@ const WaterWithControls = () => { { order: 0 } ), Presets: folder(presets, { order: -1 }), - }, controlRules); + }; }, [image]); // Reset to defaults on mount, so that Leva doesn't show values from other diff --git a/docs/src/app/home-thumbnails.ts b/docs/src/app/home-thumbnails.ts index e8c94e9ce..ad5d7043b 100644 --- a/docs/src/app/home-thumbnails.ts +++ b/docs/src/app/home-thumbnails.ts @@ -27,6 +27,7 @@ import liquidMetalImg from '../../public/shaders/liquid-metal.webp'; import halftoneDotsImg from '../../public/shaders/halftone-dots.webp'; import halftoneCmykImg from '../../public/shaders/halftone-cmyk.webp'; import gemSmokeImg from '../../public/shaders/gem-smoke.webp'; +import lensDistortionImg from '../../public/shaders/lens-distortion.webp'; import { DotGrid, dotGridPresets, @@ -86,6 +87,8 @@ import { halftoneCmykPresets, GemSmoke, gemSmokePresets, + LensDistortion, + lensDistortionPresets, } from '@paper-design/shaders-react'; import { StaticImageData } from 'next/image'; @@ -170,6 +173,17 @@ export const homeThumbnails = [ speed: 0, }, }, + { + name: 'lens distortion', + url: '/lens-distortion', + ShaderComponent: LensDistortion, + image: lensDistortionImg, + alwaysLivePreview: false, + shaderConfig: { + ...lensDistortionPresets[0].params, + image: '/images/image-filters/0018.webp', + }, + }, ], }, { diff --git a/docs/src/shader-defs/fluted-glass-def.ts b/docs/src/shader-defs/fluted-glass-def.ts index 83606f65f..2465f470e 100644 --- a/docs/src/shader-defs/fluted-glass-def.ts +++ b/docs/src/shader-defs/fluted-glass-def.ts @@ -26,14 +26,14 @@ export const flutedGlassDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorShadow, isColor: true, - description: 'Shadows color (effective with shadows > 0)', + description: 'Shadows color', }, { name: 'colorHighlight', type: 'string', defaultValue: defaultParams.colorHighlight, isColor: true, - description: 'Highlights color (effective with highlights > 0)', + description: 'Highlights color', }, { name: 'shadows', @@ -96,7 +96,7 @@ export const flutedGlassDef: ShaderDef = { min: -1, max: 1, defaultValue: defaultParams.shift, - description: 'Texture shift in direction opposite to the grid (effective with distortion > 0)', + description: 'Texture shift in direction opposite to the grid', }, { name: 'stretch', diff --git a/docs/src/shader-defs/gem-smoke-def.ts b/docs/src/shader-defs/gem-smoke-def.ts index 40571a6ff..040487c1e 100644 --- a/docs/src/shader-defs/gem-smoke-def.ts +++ b/docs/src/shader-defs/gem-smoke-def.ts @@ -41,7 +41,7 @@ export const gemSmokeDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.innerDistortion, - description: 'The power of smoke distortion inside the input shape (shape defined by alpha channel) (effective with innerGlow > 0)', + description: 'The power of smoke distortion inside the input shape (shape defined by alpha channel)', }, { name: 'outerDistortion', @@ -49,7 +49,7 @@ export const gemSmokeDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.outerDistortion, - description: 'The power of smoke distortion outside the input shape (shape defined by alpha channel) (effective with outerGlow > 0)', + description: 'The power of smoke distortion outside the input shape (shape defined by alpha channel)', }, { name: 'outerGlow', diff --git a/docs/src/shader-defs/god-rays-def.ts b/docs/src/shader-defs/god-rays-def.ts index 0f6ed6233..aa037ef30 100644 --- a/docs/src/shader-defs/god-rays-def.ts +++ b/docs/src/shader-defs/god-rays-def.ts @@ -28,7 +28,7 @@ export const godRaysDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBloom, isColor: true, - description: 'Color overlay blended with the rays (effective with bloom > 0)', + description: 'Color overlay blended with the rays', }, { name: 'bloom', diff --git a/docs/src/shader-defs/halftone-dots-def.ts b/docs/src/shader-defs/halftone-dots-def.ts index 994f3fd74..82c7a741e 100644 --- a/docs/src/shader-defs/halftone-dots-def.ts +++ b/docs/src/shader-defs/halftone-dots-def.ts @@ -25,7 +25,7 @@ export const halftoneDotsDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorFront, isColor: true, - description: 'Foreground color (effective with originalColors off)', + description: 'Foreground color', }, { name: 'originalColors', diff --git a/docs/src/shader-defs/image-dithering-def.ts b/docs/src/shader-defs/image-dithering-def.ts index cbba9d1d2..868497d6e 100644 --- a/docs/src/shader-defs/image-dithering-def.ts +++ b/docs/src/shader-defs/image-dithering-def.ts @@ -19,21 +19,21 @@ export const imageDitheringDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBack, isColor: true, - description: 'Background color (effective with originalColors off)', + description: 'Background color', }, { name: 'colorFront', type: 'string', defaultValue: defaultParams.colorFront, isColor: true, - description: 'Foreground color (effective with originalColors off)', + description: 'Foreground color', }, { name: 'colorHighlight', type: 'string', defaultValue: defaultParams.colorHighlight, isColor: true, - description: 'The secondary foreground color (set it same as colorFront to get a classic 2-color dithering) (effective with originalColors off)', + description: 'The secondary foreground color (set it same as colorFront to get a classic 2-color dithering)', }, { name: 'originalColors', diff --git a/docs/src/shader-defs/lens-distortion-def.ts b/docs/src/shader-defs/lens-distortion-def.ts new file mode 100644 index 000000000..c5a2e094e --- /dev/null +++ b/docs/src/shader-defs/lens-distortion-def.ts @@ -0,0 +1,195 @@ +import { lensDistortionPresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; +import { staticImageCommonParams } from './common-param-def'; + +const defaultParams = lensDistortionPresets[0].params; + +export const lensDistortionDef: ShaderDef = { + name: 'Lens Distortion', + description: + 'Lens Distortion image filter separates an image into shifting color layers (recreating the chromatic aberration of a lens) and warps the image geometry itself, curving it outward or inward like barrel and pincushion distortion.', + params: [ + { + name: 'image', + type: 'HTMLImageElement | string', + description: 'The image to use for the effect', + }, + { + name: 'spread', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.spread, + description: + 'Strength of the color split - how far the color layers are pushed apart. This is the master dial for the chromatic aberration; at 0 there is no split and all the other color controls do nothing', + }, + { + name: 'bias', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.bias, + description: + 'Bias of the color spread: warps how the colors distribute along it. 0 spaces them evenly; toward +/-1 they bunch toward one end of the fan and spread out at the other (no effect with spread = 0)', + }, + { + name: 'angle', + type: 'number', + min: 0, + max: 360, + defaultValue: defaultParams.angle, + description: 'Angle of the color spread in degrees (no effect with spread = 0 or perspective = 1)', + }, + { + name: 'perspective', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.perspective, + description: + 'Perspective of the spread: shapes its direction from a straight line to a radial burst. At 0 all layers spread along the straight line set by angle; at 1 they radiate outward from the centre (no effect with spread = 0)', + }, + { + name: 'count', + type: 'number', + min: 2, + max: 50, + step: 1, + defaultValue: defaultParams.count, + description: + 'Number of colored layers making the spread - more layers blend into a blur, fewer stay as separate ghosted copies (no effect with spread = 0)', + }, + { + name: 'dispersion', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.dispersion, + description: + 'Overall amount of color dispersion: at 1 each layer takes its own color from the spectrum, giving the chromatic aberration look; at 0 every layer keeps the original image color, so the spread reads as a plain blur (no effect with spread = 0)', + }, + { + name: 'dispersionShift', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.dispersionShift, + description: + 'Balances the dispersion between a soft circular zone at the centre of the image and the rest of it: 0 applies it evenly over the whole image, -1 keeps it in the centre only, 1 keeps it at the edges only (no effect with spread = 0 or dispersion = 0)', + }, + { + name: 'dispersionColor', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.dispersionColor, + description: + 'Rotates the colors around the hue wheel, 0 to 1 for a full turn (no effect with spread = 0 or dispersion = 0)', + }, + { + name: 'focusCenter', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.focusCenter, + description: + 'Reduces the spread distance in a circular zone at the centre of the image. 0 keeps the full layers spread in the middle, higher values move the layers closer building an illusion of focus (no effect with spread = 0)', + }, + { + name: 'focusEdges', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.focusEdges, + description: + 'Reduces the spread distance along the image edges. 0 keeps the full layers spread at the edges, 1 restores the original image at the edges regardless of the chosen spread distance (no effect with spread = 0)', + }, + { + name: 'swirl', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.swirl, + description: + 'Bends the spread around the centre of the image, so the layers trail along circles like a vortex. Each layer is rotated around the centre by an angle growing along the spread, and the effect fades out beyond the inscribed circle (no effect with spread = 0)', + }, + { + name: 'noise', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.noise, + description: 'Noise distortion over the spread direction within the spread distance (no effect with spread = 0)', + }, + { + name: 'noiseFrequency', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.noiseFrequency, + description: + 'Noise frequency (scale), higher value gives more detailed distortion (no effect with spread = 0 or noise = 0)', + }, + { + name: 'noiseOffset', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.noiseOffset, + description: + 'Shifts the noise pattern diagonally, can be used as a seed to get a different scatter (no effect with spread = 0 or noise = 0)', + }, + { + name: 'lensBulge', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.lensBulge, + description: + 'Lens warp of the image geometry, applied before the color spread. 0 is flat, positive bulges out like a fisheye, negative pinches in like a pincushion. Strong positive values fade out the corners of the image', + }, + { + name: 'lensCircle', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.lensCircle, + description: + 'Squeezes pixels outside the circle inward. 0 is off, 1 gives the image exact circular shape with any lensBulge value. Near the rim it also turns the spread radial and damps it, keeping the circular edge clean', + }, + { + name: 'grainMixer', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.grainMixer, + description: + 'Scatters the spread with grain noise, breaking up the edges of the colored layers (no effect with spread = 0)', + }, + { + name: 'grainOverlay', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.grainOverlay, + description: 'Post-processing b/w grain overlay', + }, + { + name: 'imageX', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.imageX, + description: 'Pans the image horizontally behind the effect, without moving the effect itself. 0 is centred', + }, + { + name: 'imageY', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.imageY, + description: 'Pans the image vertically behind the effect, without moving the effect itself. 0 is centred', + }, + ...staticImageCommonParams, + ], +}; diff --git a/docs/src/shader-defs/shader-defs.ts b/docs/src/shader-defs/shader-defs.ts index ed0d680ae..7ae4ceedf 100644 --- a/docs/src/shader-defs/shader-defs.ts +++ b/docs/src/shader-defs/shader-defs.ts @@ -26,6 +26,10 @@ import { warpDef } from './warp-def'; import { waterDef } from './water-def'; import { wavesDef } from './waves-def'; import { halftoneDotsDef } from './halftone-dots-def'; +import { halftoneCmykDef } from './halftone-cmyk-def'; +import { heatmapDef } from './heatmap-def'; +import { gemSmokeDef } from './gem-smoke-def'; +import { lensDistortionDef } from './lens-distortion-def'; export const shaderDefs: ShaderDef[] = [ grainGradientDef, @@ -54,4 +58,8 @@ export const shaderDefs: ShaderDef[] = [ imageDitheringDef, waterDef, halftoneDotsDef, + halftoneCmykDef, + heatmapDef, + gemSmokeDef, + lensDistortionDef, ]; diff --git a/docs/src/shader-defs/spiral-def.ts b/docs/src/shader-defs/spiral-def.ts index b9e8bc42e..50e4f86ef 100644 --- a/docs/src/shader-defs/spiral-def.ts +++ b/docs/src/shader-defs/spiral-def.ts @@ -77,7 +77,7 @@ export const spiralDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.noiseFrequency, - description: 'Noise frequency (effective with noise > 0)', + description: 'Moise frequency (no effect with noise = 0)', }, { name: 'softness', diff --git a/docs/src/shader-defs/static-mesh-gradient-def.ts b/docs/src/shader-defs/static-mesh-gradient-def.ts index c43cc6032..d415e6dd9 100644 --- a/docs/src/shader-defs/static-mesh-gradient-def.ts +++ b/docs/src/shader-defs/static-mesh-gradient-def.ts @@ -38,7 +38,7 @@ export const staticMeshGradientDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.waveXShift, - description: 'Phase offset applied to the X-axis wave (effective with waveX > 0)', + description: 'Phase offset applied to the X-axis wave', }, { name: 'waveY', @@ -54,7 +54,7 @@ export const staticMeshGradientDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.waveYShift, - description: 'Phase offset applied to the Y-axis wave (effective with waveY > 0)', + description: 'Phase offset applied to the Y-axis wave', }, { name: 'mixing', diff --git a/docs/src/shader-defs/swirl-def.ts b/docs/src/shader-defs/swirl-def.ts index 2cd284491..a0b7b8f5c 100644 --- a/docs/src/shader-defs/swirl-def.ts +++ b/docs/src/shader-defs/swirl-def.ts @@ -77,7 +77,7 @@ export const swirlDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.noiseFrequency, - description: 'Noise frequency (effective with noise > 0)', + description: 'Noise frequency (no effect with noise = 0)', }, ...animatedCommonParams, ], diff --git a/docs/src/shader-defs/voronoi-def.ts b/docs/src/shader-defs/voronoi-def.ts index 06cdba193..ab445e37c 100644 --- a/docs/src/shader-defs/voronoi-def.ts +++ b/docs/src/shader-defs/voronoi-def.ts @@ -27,7 +27,7 @@ export const voronoiDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorGap, isColor: true, - description: 'Color used for cell borders/gaps (effective with gap > 0)', + description: 'Color used for cell borders/gaps', }, { name: 'stepsPerColor', diff --git a/docs/src/shader-defs/water-def.ts b/docs/src/shader-defs/water-def.ts index 6a88f0bb0..6fa335b96 100644 --- a/docs/src/shader-defs/water-def.ts +++ b/docs/src/shader-defs/water-def.ts @@ -26,7 +26,7 @@ export const waterDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorHighlight, isColor: true, - description: 'Highlight color (effective with highlights > 0)', + description: 'Highlight color', }, { name: 'highlights', diff --git a/packages/shaders-react/package.json b/packages/shaders-react/package.json index 3b79bb02d..dd38cce51 100644 --- a/packages/shaders-react/package.json +++ b/packages/shaders-react/package.json @@ -1,6 +1,6 @@ { "name": "@paper-design/shaders-react", - "version": "0.0.77", + "version": "0.0.80", "license": "Apache-2.0", "type": "module", "sideEffects": false, diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 705df4e1d..3fd7b7a2f 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -99,6 +99,10 @@ export { ImageDithering, imageDitheringPresets } from './shaders/image-dithering export type { ImageDitheringProps } from './shaders/image-dithering.js'; export type { ImageDitheringUniforms, ImageDitheringParams } from '@paper-design/shaders'; +export { LensDistortion, lensDistortionPresets } from './shaders/lens-distortion.js'; +export type { LensDistortionProps } from './shaders/lens-distortion.js'; +export type { LensDistortionUniforms, LensDistortionParams } from '@paper-design/shaders'; + export { Heatmap, heatmapPresets } from './shaders/heatmap.js'; export type { HeatmapProps } from './shaders/heatmap.js'; export type { HeatmapUniforms, HeatmapParams } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shader-mount.tsx b/packages/shaders-react/src/shader-mount.tsx index fc43e87f7..46f47c215 100644 --- a/packages/shaders-react/src/shader-mount.tsx +++ b/packages/shaders-react/src/shader-mount.tsx @@ -105,8 +105,11 @@ async function processUniforms(uniformsProp: ShaderMountUniformsReact): Promise< imageLoadPromises.push(imagePromise); } else if (value instanceof HTMLImageElement) { - setMinImageSize(value); - processedUniforms[key] = value; + const imagePromise = value.decode().then(() => { + setMinImageSize(value); + processedUniforms[key] = value; + }); + imageLoadPromises.push(imagePromise); } else { processedUniforms[key] = value; } diff --git a/packages/shaders-react/src/shaders/lens-distortion.tsx b/packages/shaders-react/src/shaders/lens-distortion.tsx new file mode 100644 index 000000000..2db06d057 --- /dev/null +++ b/packages/shaders-react/src/shaders/lens-distortion.tsx @@ -0,0 +1,231 @@ +import { memo } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; +import { + lensDistortionFragmentShader, + ShaderFitOptions, + type LensDistortionUniforms, + type LensDistortionParams, + defaultObjectSizing, + type ImageShaderPreset, +} from '@paper-design/shaders'; + +export interface LensDistortionProps extends ShaderComponentProps, LensDistortionParams {} + +type LensDistortionPreset = ImageShaderPreset; + +export const defaultPreset: LensDistortionPreset = { + name: 'Default', + params: { + ...defaultObjectSizing, + scale: 1, + fit: 'contain', + speed: 0, + frame: 0, + spread: 0.6, + bias: 1, + angle: 0, + perspective: 0.1, + count: 35, + dispersion: 1, + dispersionShift: 0, + dispersionColor: 0.6, + focusCenter: 0.8, + focusEdges: 1, + swirl: 0.35, + noise: 0, + noiseFrequency: 0.25, + noiseOffset: 0, + lensBulge: 0, + lensCircle: 0, + grainMixer: 0, + grainOverlay: 0, + imageX: 0, + imageY: 0, + }, +} as const; + +export const fisheyePreset: LensDistortionPreset = { + name: 'Fisheye', + params: { + ...defaultObjectSizing, + scale: 1, + fit: 'contain', + speed: 0, + frame: 0, + spread: 0.7, + bias: 1, + angle: 0, + perspective: 0.75, + count: 15, + dispersion: 1, + dispersionShift: 0, + dispersionColor: 0.7, + focusCenter: 0.9, + focusEdges: 1, + swirl: 1, + noise: 0, + noiseFrequency: 0.25, + noiseOffset: 0, + lensBulge: 0.95, + lensCircle: 1, + grainMixer: 0.5, + grainOverlay: 0, + imageX: 0, + imageY: 0, + }, +} as const; + +export const duotonePreset: LensDistortionPreset = { + name: 'Duotone', + params: { + ...defaultObjectSizing, + fit: 'contain', + speed: 0, + frame: 0, + spread: 0.1, + bias: 0, + angle: 0, + perspective: 0, + count: 2, + dispersion: 1, + dispersionShift: 0, + dispersionColor: 0.1, + focusCenter: 0, + focusEdges: 0, + swirl: 0, + noise: 0, + noiseFrequency: 0.25, + noiseOffset: 0, + lensBulge: 0, + lensCircle: 0, + grainMixer: 0.1, + grainOverlay: 0.3, + imageX: 0, + imageY: 0, + }, +} as const; + +export const smokePreset: LensDistortionPreset = { + name: 'Smoke', + params: { + ...defaultObjectSizing, + scale: 1, + fit: 'contain', + speed: 0, + frame: 0, + spread: 0.65, + bias: -1, + angle: 0, + perspective: 0.4, + count: 50, + dispersion: 0, + dispersionShift: 0, + dispersionColor: 0.31, + focusCenter: 0, + focusEdges: 0.6, + swirl: 0, + noise: 1, + noiseFrequency: 0.35, + noiseOffset: 0.3, + lensBulge: 0, + lensCircle: 0, + grainMixer: 0, + grainOverlay: 0, + imageX: 0, + imageY: 0, + }, +} as const; + +export const lensDistortionPresets: LensDistortionPreset[] = [ + defaultPreset, + fisheyePreset, + duotonePreset, + smokePreset, +]; + +export const LensDistortion: React.FC = memo(function LensDistortionImpl({ + // Own props + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + image = '', + spread = defaultPreset.params.spread, + bias = defaultPreset.params.bias, + angle = defaultPreset.params.angle, + perspective = defaultPreset.params.perspective, + count = defaultPreset.params.count, + dispersion = defaultPreset.params.dispersion, + dispersionShift = defaultPreset.params.dispersionShift, + dispersionColor = defaultPreset.params.dispersionColor, + focusCenter = defaultPreset.params.focusCenter, + focusEdges = defaultPreset.params.focusEdges, + swirl = defaultPreset.params.swirl, + noise = defaultPreset.params.noise, + noiseFrequency = defaultPreset.params.noiseFrequency, + noiseOffset = defaultPreset.params.noiseOffset, + lensBulge = defaultPreset.params.lensBulge, + lensCircle = defaultPreset.params.lensCircle, + grainMixer = defaultPreset.params.grainMixer, + grainOverlay = defaultPreset.params.grainOverlay, + imageX = defaultPreset.params.imageX, + imageY = defaultPreset.params.imageY, + + // Sizing props + fit = defaultPreset.params.fit, + scale = defaultPreset.params.scale, + rotation = defaultPreset.params.rotation, + originX = defaultPreset.params.originX, + originY = defaultPreset.params.originY, + offsetX = defaultPreset.params.offsetX, + offsetY = defaultPreset.params.offsetY, + worldWidth = defaultPreset.params.worldWidth, + worldHeight = defaultPreset.params.worldHeight, + ...props +}: LensDistortionProps) { + const uniforms = { + // Own uniforms + u_image: image, + u_spread: spread, + u_bias: bias, + u_angle: angle, + u_perspective: perspective, + u_count: count, + u_dispersion: dispersion, + u_dispersionShift: dispersionShift, + u_dispersionColor: dispersionColor, + u_focusCenter: focusCenter, + u_focusEdges: focusEdges, + u_swirl: swirl, + u_noise: noise, + u_noiseFrequency: noiseFrequency, + u_noiseOffset: noiseOffset, + u_lensBulge: lensBulge, + u_lensCircle: lensCircle, + u_grainMixer: grainMixer, + u_grainOverlay: grainOverlay, + u_imageX: imageX, + u_imageY: imageY, + + // Sizing uniforms + u_fit: ShaderFitOptions[fit], + u_rotation: rotation, + u_scale: scale, + u_offsetX: offsetX, + u_offsetY: offsetY, + u_originX: originX, + u_originY: originY, + u_worldWidth: worldWidth, + u_worldHeight: worldHeight, + } satisfies LensDistortionUniforms; + + return ( + + ); +}, colorPropsAreEqual); diff --git a/packages/shaders/package.json b/packages/shaders/package.json index cf7a0f9bc..52080c523 100644 --- a/packages/shaders/package.json +++ b/packages/shaders/package.json @@ -1,6 +1,6 @@ { "name": "@paper-design/shaders", - "version": "0.0.77", + "version": "0.0.80", "license": "Apache-2.0", "type": "module", "sideEffects": false, diff --git a/packages/shaders/src/get-shader-color-from-string.test.ts b/packages/shaders/src/get-shader-color-from-string.test.ts index 2443816b3..9424cae44 100644 --- a/packages/shaders/src/get-shader-color-from-string.test.ts +++ b/packages/shaders/src/get-shader-color-from-string.test.ts @@ -48,11 +48,29 @@ describe('getShaderColorFromString', () => { // Test edge cases test('handles undefined input', () => { - expect(getShaderColorFromString(undefined)).toEqual([0, 0, 0, 1]); + expect(getShaderColorFromString(undefined)).toEqual([0.5, 0.5, 0.5, 1]); }); test('handles invalid color string', () => { - expect(getShaderColorFromString('not-a-color')).toEqual([0, 0, 0, 1]); + expect(getShaderColorFromString('not-a-color')).toEqual([0.5, 0.5, 0.5, 1]); + }); + + test('handles 4-digit hex with alpha', () => { + expect(getShaderColorFromString('#ff0c')).toEqual([1, 1, 0, 0.8]); + }); + + test('returns fallback for hex with invalid characters', () => { + expect(getShaderColorFromString('#gggggg')).toEqual([0.5, 0.5, 0.5, 1]); + }); + + test('returns fallback for hex with invalid length', () => { + expect(getShaderColorFromString('#12345')).toEqual([0.5, 0.5, 0.5, 1]); + expect(getShaderColorFromString('#ff')).toEqual([0.5, 0.5, 0.5, 1]); + }); + + test('returns fallback for malformed rgb/hsl', () => { + expect(getShaderColorFromString('rgb(nope)')).toEqual([0.5, 0.5, 0.5, 1]); + expect(getShaderColorFromString('hsl(nope)')).toEqual([0.5, 0.5, 0.5, 1]); }); // Test color value ranges diff --git a/packages/shaders/src/get-shader-color-from-string.ts b/packages/shaders/src/get-shader-color-from-string.ts index aa9d3fe8b..7e107650c 100644 --- a/packages/shaders/src/get-shader-color-from-string.ts +++ b/packages/shaders/src/get-shader-color-from-string.ts @@ -21,9 +21,13 @@ export function getShaderColorFromString( if (colorString.startsWith('#')) { [r, g, b, a] = hexToRgba(colorString); } else if (colorString.startsWith('rgb')) { - [r, g, b, a] = parseRgba(colorString); + const rgba = parseRgba(colorString); + if (rgba === null) return fallbackColor; + [r, g, b, a] = rgba; } else if (colorString.startsWith('hsl')) { - [r, g, b, a] = hslaToRgba(parseHsla(colorString)); + const hsla = parseHsla(colorString); + if (hsla === null) return fallbackColor; + [r, g, b, a] = hslaToRgba(hsla); } else { console.error('Unsupported color format', colorString); return fallbackColor; @@ -37,8 +41,8 @@ function hexToRgba(hex: string): [number, number, number, number] { // Remove # if present hex = hex.replace(/^#/, ''); - // Expand three-letter hex to six-letter - if (hex.length === 3) { + // Expand shorthand hex (3-letter #rgb or 4-letter #rgba) by doubling each digit + if (hex.length === 3 || hex.length === 4) { hex = hex .split('') .map((char) => char + char) @@ -49,6 +53,13 @@ function hexToRgba(hex: string): [number, number, number, number] { hex = hex + 'ff'; } + // Bail out on malformed hex (wrong length or non-hex characters) so callers + // get the fallback color instead of NaN channels leaking into the shader. + if (!/^[0-9a-f]{8}$/i.test(hex)) { + console.warn('Invalid hex color'); + return fallbackColor; + } + // Parse the components const r = parseInt(hex.slice(0, 2), 16) / 255; const g = parseInt(hex.slice(2, 4), 16) / 255; @@ -58,11 +69,11 @@ function hexToRgba(hex: string): [number, number, number, number] { return [r, g, b, a]; } -/** Parse RGBA string to RGBA (0 to 1 range) */ -function parseRgba(rgba: string): [number, number, number, number] { +/** Parse RGBA string to RGBA (0 to 1 range), or null if unparseable */ +function parseRgba(rgba: string): [number, number, number, number] | null { // Match both rgb and rgba patterns const match = rgba.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([0-9.]+))?\s*\)$/i); - if (!match) return [0, 0, 0, 1]; + if (!match) return null; return [ parseInt(match[1] ?? '0') / 255, @@ -72,10 +83,10 @@ function parseRgba(rgba: string): [number, number, number, number] { ]; } -/** Parse HSLA string */ -function parseHsla(hsla: string): [number, number, number, number] { +/** Parse HSLA string, or null if unparseable */ +function parseHsla(hsla: string): [number, number, number, number] | null { const match = hsla.match(/^hsla?\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([0-9.]+))?\s*\)$/i); - if (!match) return [0, 0, 0, 1]; + if (!match) return null; return [ parseInt(match[1] ?? '0'), @@ -117,4 +128,4 @@ function hslaToRgba(hsla: [number, number, number, number]): [number, number, nu export const clamp = (n: number, min: number, max: number): number => Math.min(Math.max(n, min), max); -const fallbackColor = [0, 0, 0, 1] as [0, 0, 0, 1]; +const fallbackColor = [0.5, 0.5, 0.5, 1] as [number, number, number, number]; diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index bdb352d9b..b01a4ae2a 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -184,6 +184,9 @@ export { type ImageDitheringUniforms, } from './shaders/image-dithering.js'; +/** Lens Distortion image filter that separates an image into shifting color layers, recreating the chromatic aberration of a lens, and warps the image geometry with barrel or pincushion curvature */ +export { lensDistortionFragmentShader, lensDistortionMeta, type LensDistortionParams, type LensDistortionUniforms } from './shaders/lens-distortion.js'; + /** A glowing gradient of colors flowing through an input image. The effect creates a smoothly animated wave of intensity across the image */ export { heatmapMeta, diff --git a/packages/shaders/src/shader-mount.ts b/packages/shaders/src/shader-mount.ts index b7c1183df..3a6f1be7c 100644 --- a/packages/shaders/src/shader-mount.ts +++ b/packages/shaders/src/shader-mount.ts @@ -18,7 +18,7 @@ export class ShaderMount { private currentFrame = 0; /** The speed that we progress through animation time (multiplies by delta time every update). Allows negatives to play in reverse. If set to 0, rAF will stop entirely so static shaders have no recurring performance costs */ private speed = 0; - /** Actual speed used that accounts for document visibility (we pause the shader if the tab is hidden) */ + /** Actual speed used that accounts for visibility: we pause the shader if the tab is hidden or the element out of the viewport */ private currentSpeed = 0; /** Uniforms that are provided by the user for the specific shader being mounted (not including uniforms that this Mount adds, like time and resolution) */ private providedUniforms: ShaderMountUniforms; @@ -107,6 +107,8 @@ export class ShaderMount { this.setupResizeObserver(); // Set up the visual viewport change listener to handle zoom changes (pinch zoom and classic browser zoom) visualViewport?.addEventListener('resize', this.handleVisualViewportChange); + // Set up the intersection observer to pause animation when the element is scrolled out of view + this.setupIntersectionObserver(); // Set the animation speed after everything is ready to go this.setSpeed(speed); @@ -172,6 +174,9 @@ export class ShaderMount { private parentDevicePixelHeight = 0; private devicePixelsSupported = false; + private intersectionObserver: IntersectionObserver | null = null; + private isInViewport = true; + private resizeObserver: ResizeObserver | null = null; private setupResizeObserver = () => { this.resizeObserver = new ResizeObserver(([entry]) => { @@ -194,6 +199,19 @@ export class ShaderMount { this.resizeObserver.observe(this.parentElement); }; + private setupIntersectionObserver = () => { + // we check element's own window to support shaders used within iframe/PiP elements + const view = this.ownerDocument.defaultView; + if (!view?.IntersectionObserver) return; + + this.intersectionObserver = new view.IntersectionObserver(([entry]) => { + this.isInViewport = entry?.isIntersecting ?? true; + this.updateCurrentSpeed(); + }); + + this.intersectionObserver.observe(this.parentElement); + }; + // Visual viewport resize handler, mainly used to react to browser zoom changes. // Resize observer by itself does not react to pinch zoom, and although it usually // reacts to classic browser zoom, it's not guaranteed in edge cases. @@ -490,7 +508,12 @@ export class ShaderMount { public setSpeed = (newSpeed = 1): void => { // Set the new animation speed this.speed = newSpeed; - this.setCurrentSpeed(this.ownerDocument.hidden ? 0 : newSpeed); + this.updateCurrentSpeed(); + }; + + /** Apply the target speed, pausing (0) while the tab is hidden or the element is out of the viewport */ + private updateCurrentSpeed = (): void => { + this.setCurrentSpeed(this.ownerDocument.hidden || !this.isInViewport ? 0 : this.speed); }; private setCurrentSpeed = (newSpeed: number): void => { @@ -532,7 +555,7 @@ export class ShaderMount { }; private handleDocumentVisibilityChange = () => { - this.setCurrentSpeed(this.ownerDocument.hidden ? 0 : this.speed); + this.updateCurrentSpeed(); }; /** Dispose of the shader mount, cleaning up all of the WebGL resources */ @@ -571,6 +594,11 @@ export class ShaderMount { this.resizeObserver = null; } + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + this.intersectionObserver = null; + } + visualViewport?.removeEventListener('resize', this.handleVisualViewportChange); this.ownerDocument.removeEventListener('visibilitychange', this.handleDocumentVisibilityChange); diff --git a/packages/shaders/src/shader-utils.ts b/packages/shaders/src/shader-utils.ts index 124aea9cb..c816fc716 100644 --- a/packages/shaders/src/shader-utils.ts +++ b/packages/shaders/src/shader-utils.ts @@ -88,4 +88,45 @@ float snoise(vec2 v) { g.yz = a0.yz * x12.xz + h.yz * x12.yw; return 130.0 * dot(m, g); } -`; \ No newline at end of file +`; + +// language=GLSL +export const fiberNoise = ` +float fiberRandom(vec2 p) { + vec2 uv = floor(p) / 100.; + return texture(u_noiseTexture, fract(uv)).b; +} + +float fiberValueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = fiberRandom(i); + float b = fiberRandom(i + vec2(1.0, 0.0)); + float c = fiberRandom(i + vec2(0.0, 1.0)); + float d = fiberRandom(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} + +float fiberNoiseFbm(in vec2 n, vec2 seedOffset) { + float total = 0.0, amplitude = 1.; + for (int i = 0; i < 4; i++) { + n = rotate(n, .7); + total += fiberValueNoise(n + seedOffset) * amplitude; + n *= 2.; + amplitude *= 0.6; + } + return total; +} + +float fiberNoise(vec2 uv, vec2 seedOffset) { + float epsilon = 0.001; + float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset); + float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset); + float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset); + float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset); + return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon); +} +`; diff --git a/packages/shaders/src/shaders/lens-distortion.ts b/packages/shaders/src/shaders/lens-distortion.ts new file mode 100644 index 000000000..567658e38 --- /dev/null +++ b/packages/shaders/src/shaders/lens-distortion.ts @@ -0,0 +1,364 @@ +import type { ShaderMotionParams } from '../shader-mount.js'; +import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, proceduralHash21, rotation2 } from '../shader-utils.js'; + +export const lensDistortionMeta = { + maxSamples: 50, +} as const; + +/** + * + * Lens Distortion image filter separates an image into shifting color layers + * (recreating the chromatic aberration of a lens) and warps the image geometry, + * curving it outward or inward like barrel and pincushion distortion. + * + * Fragment shader uniforms: + * - u_image (sampler2D): Source image texture + * - u_spread (float): Strength of the color split; how far the color layers are pushed apart; 0 is off (0 to 1) + * - u_bias (float): Shifts the colors toward one end of the spread; 0 spaces them evenly (-1 to 1) + * - u_angle (float): Direction of the spread in degrees (0 to 360) + * - u_perspective (float): Shapes the spread direction from a straight line (0) to a radial burst out from the centre (1) (0 to 1) + * - u_count (float): Number of sampled color layers along the spread; higher is smoother and costlier (2 to 50) + * - u_dispersion (float): Overall amount of color dispersion; 1 gives each layer its own color from the spectrum, 0 keeps the original image color (0 to 1) + * - u_dispersionShift (float): Balance of the dispersion between a soft circular zone at the centre and the rest of the image; 0 applies it evenly, -1 keeps it in the centre only, 1 keeps it at the edges only (-1 to 1) + * - u_dispersionColor (float): Rotates the colors around the hue wheel, 0 to 1 for a full turn + * - u_focusCenter (float): Reduces the spread in a circular zone at the centre; 0 keeps it full to the centre (0 to 1) + * - u_focusEdges (float): Reduces the spread toward the edges; 0 keeps it full to the edges, 1 restores the original image there (0 to 1) + * - u_swirl (float): Rotates the color layers around the centre by an angle growing along the spread; 0 is off (-1 to 1) + * - u_noise (float): Scatters the spread direction with noise; 0 is off (0 to 1) + * - u_noiseFrequency (float): Frequency of the noise, 0 to 1 mapped internally to 0 to 18; higher is finer (no effect with noise = 0) + * - u_noiseOffset (float): Offsets the noise pattern for a different seed, 0 to 1 mapped internally to 0 to 30 (no effect with noise = 0) + * - u_lensBulge (float): Radial lens warp of the image geometry; positive bulges out like a fisheye/barrel, negative pinches in like a pincushion; strong positive values fade out the corners (-1 to 1) + * - u_lensCircle (float): Squeezes pixels outside the inscribed circle inward so the outline becomes a circle; also turns the spread radial and damps it near the rim; 0 is off, 1 is full (0 to 1) + * - u_grainMixer (float): Scatters the spread with grain noise, breaking up the edges of the color layers (0 to 1) + * - u_grainOverlay (float): Post-processing black/white grain overlay (0 to 1) + * - u_imageX (float): Pans the image horizontally behind the effect; 0 is centred (-1 to 1) + * - u_imageY (float): Pans the image vertically behind the effect; 0 is centred (-1 to 1) + * + * Vertex shader outputs (used in fragment shader): + * - v_imageUV (vec2): Image UV coordinates with global sizing (rotation, scale, offset, etc) applied + * + * Vertex shader uniforms: + * - u_resolution (vec2): Canvas resolution in pixels + * - u_pixelRatio (float): Device pixel ratio + * - u_originX (float): Reference point for positioning world width in the canvas (0 to 1) + * - u_originY (float): Reference point for positioning world height in the canvas (0 to 1) + * - u_fit (float): How to fit the rendered shader into the canvas dimensions (0 = none, 1 = contain, 2 = cover) + * - u_scale (float): Overall zoom level of the graphics (0.01 to 4) + * - u_rotation (float): Overall rotation angle of the graphics in degrees (0 to 360) + * - u_offsetX (float): Horizontal offset of the graphics center (-1 to 1) + * - u_offsetY (float): Vertical offset of the graphics center (-1 to 1) + * - u_imageAspectRatio (float): Aspect ratio of the source image + * + */ + +// language=GLSL +export const lensDistortionFragmentShader: string = `#version 300 es +precision mediump float; + +uniform sampler2D u_image; +uniform float u_imageAspectRatio; +uniform float u_spread; +uniform float u_bias; +uniform float u_angle; +uniform float u_perspective; +uniform float u_count; +uniform float u_dispersion; +uniform float u_dispersionShift; +uniform float u_dispersionColor; +uniform float u_focusCenter; +uniform float u_focusEdges; +uniform float u_swirl; +uniform float u_noise; +uniform float u_noiseFrequency; +uniform float u_noiseOffset; +uniform float u_lensBulge; +uniform float u_lensCircle; +uniform float u_grainMixer; +uniform float u_grainOverlay; +uniform float u_imageX; +uniform float u_imageY; + +in vec2 v_imageUV; + +out vec4 fragColor; + +${declarePI} +${proceduralHash21} +${rotation2} + +float valueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = hash21(i); + float b = hash21(i + vec2(1., 0.)); + float c = hash21(i + vec2(0., 1.)); + float d = hash21(i + vec2(1., 1.)); + vec2 u = f * f * (3. - 2. * f); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float getUvFrame(vec2 uv) { + vec2 invAA = 1. / clamp(fwidth(uv), 1e-5, .02); + vec2 lo = clamp(uv * invAA + .5, 0., 1.); + vec2 hi = clamp((1. - uv) * invAA + .5, 0., 1.); + return lo.x * hi.x * lo.y * hi.y; +} + +vec4 sampleOverWhite(vec2 uv) { + vec4 img = texture(u_image, uv); + float cover = img.a * getUvFrame(uv); + vec3 colorOverWhite = mix(vec3(1.), img.rgb, cover); + return vec4(colorOverWhite, cover); +} + +vec3 hueColor(float hue) { + vec3 rgb = clamp(abs(mod(hue * 6. + vec3(0., 4., 2.), 6.) - 3.) - 1., 0., 1.); + rgb = rgb * rgb * (3. - 2. * rgb); + return rgb; +} + +float boxInradius() { + return .5 * min(u_imageAspectRatio, 1.); +} + +float boxOutradius() { + return .5 * length(vec2(u_imageAspectRatio, 1.)); +} + +float spreadReach() { + return .7 * pow(u_spread, 1.3 + 2.7 * u_spread); +} + +float innerCircleMask(float radius, float inradius) { + return 1. - smoothstep(.5 * inradius, 1.1 * inradius, radius); +} + +float dispersionCurve(float t, float biasPow) { + float mirroredT = u_bias < 0. ? 1. - t : t; + float curved = pow(mirroredT, biasPow); + return u_bias < 0. ? 1. - curved : curved; +} + +vec2 getSpread(vec2 fromCenter, float radius, vec2 warpedUV, float edgeAA, float inradius, float outradius, float reach, out float outStrength) { + float angleRad = radians(u_angle); + vec2 uniformDir = vec2(cos(angleRad), sin(angleRad)); + + float invInradius = 1. / inradius; + vec2 radialDir = fromCenter * invInradius; + float maxLen = (outradius + reach) * invInradius; + float radialLen = radius * invInradius; + if (radialLen > maxLen) radialDir *= maxLen / radialLen; + vec2 spreadDir = mix(uniformDir, radialDir, u_perspective); + + float bandProximity = smoothstep(inradius * .8, inradius, radius); + float lensCircleMaxing = bandProximity * u_lensCircle * u_lensCircle * u_lensCircle; + spreadDir = mix(spreadDir, radialDir, lensCircleMaxing); + + vec2 warpedAbs = abs(warpedUV - .5); + float inner = mix(1., smoothstep(0., mix(inradius, outradius, u_focusCenter), radius), u_focusCenter); + float boxDist = max(warpedAbs.x, warpedAbs.y) * 2.; + float outer = mix(1., 1. - min(boxDist, 1.), u_focusEdges); + float strength = inner * outer; + + strength *= mix(1., mix(.15, .03, max(-u_lensBulge, 0.)), lensCircleMaxing); + + vec2 outside = max(warpedAbs - .5, 0.); + float margin = max(reach * length(spreadDir) * (1. - u_lensCircle), edgeAA); + strength *= 1. - smoothstep(0., margin, length(outside)); + + outStrength = strength; + vec2 axis = spreadDir * (reach * strength); + + if (u_noise > 0.) { + float turn = (valueNoise(fromCenter * u_noiseFrequency * 18. + u_noiseOffset * 30.) - .5) * 2. * u_noise; + float cs = cos(turn), sn = sin(turn); + axis = mat2(cs, -sn, sn, cs) * axis; + } + + axis.x /= u_imageAspectRatio; + return axis; +} + +vec2 lensWarp(vec2 fromCenter, float radius, float inradius, out float bulgeFade) { + bulgeFade = 1.; + if (u_lensBulge == 0. && u_lensCircle <= 0.) return fromCenter; + if (radius < 1e-5) return fromCenter; + + float r = radius; + + if (u_lensBulge != 0.) { + float rn = radius / inradius; + float bulge = abs(u_lensBulge) * (u_lensBulge > 0. ? 1.4 : 1.2); + if (u_lensBulge > 0.) bulgeFade = 1. - smoothstep(1.45, 1.53, rn * bulge); + float map = u_lensBulge > 0. + ? tan(min(rn * bulge, 1.53)) / tan(bulge) + : atan(rn * tan(bulge)) / bulge; + float bulgeScale = map / rn; + fromCenter *= bulgeScale; + r *= bulgeScale; + } + + if (u_lensCircle > 0.) { + vec2 dir = fromCenter / max(r, 1e-5); + vec2 halfBox = vec2(u_imageAspectRatio, 1.) * .5; + float rBox = min(halfBox.x / max(abs(dir.x), 1e-4), halfBox.y / max(abs(dir.y), 1e-4)); + float band = inradius * mix(.03, .2, .5 * (u_lensBulge + 1.)); + float innerEdge = inradius - band; + float over = smoothstep(0., 1., (r - innerEdge) / band); + float g = r + (rBox - inradius) * pow(over, 14.); + fromCenter = dir * mix(r, g, u_lensCircle); + } + + return fromCenter; +} + +void main() { + vec2 uv = v_imageUV; + + float invAspect = 1. / u_imageAspectRatio; + float inradius = boxInradius(); + float outradius = boxOutradius(); + float reach = spreadReach(); + + vec2 fromCenter = uv - .5; + fromCenter.x *= u_imageAspectRatio; + float radius = length(fromCenter); + + float bulgeFade; + vec2 warpedFromCenter = lensWarp(fromCenter, radius, inradius, bulgeFade); + vec2 baseUV = vec2(warpedFromCenter.x * invAspect, warpedFromCenter.y) + .5; + + vec2 baseDerivative = fwidth(baseUV); + float edgeAA = clamp(2. * max(baseDerivative.x, baseDerivative.y), .001, .02); + + float spreadStrength; + vec2 spreadAxis = getSpread(fromCenter, radius, baseUV, edgeAA, inradius, outradius, reach, spreadStrength); + + vec2 grainUV = vec2(0.); + if (u_grainMixer > 0. || u_grainOverlay > 0.) { + vec2 dudx = dFdx(v_imageUV); + vec2 dudy = dFdy(v_imageUV); + grainUV = (v_imageUV - .5) * (.8 / vec2(length(dudx), length(dudy))) + .5; + } + + if (u_grainMixer > 0.) { + float grainSeed = valueNoise(grainUV); + vec2 jit = fract(grainSeed * vec2(157.31, 113.57)) * 2. - 1.; + spreadAxis += jit * .3 * u_grainMixer * length(spreadAxis); + } + + float count = floor(u_count); + int countInteger = int(count); + + float invCount = 1. / count; + float invSpan = 1. / max(count - 1., 1.); + float hueBase = u_dispersionColor + .5; + float biasPower = 1. + 2. * abs(u_bias); + vec2 tapBias = .5 - vec2(u_imageX, u_imageY); + + float centerAmount = clamp(1. - u_dispersionShift, 0., 1.); + float edgesAmount = clamp(1. + u_dispersionShift, 0., 1.); + float dispersion = u_dispersion * mix(edgesAmount, centerAmount, innerCircleMask(radius, inradius)); + float dispersionPower = pow(dispersion, .8); + + float warpedRadius = length(warpedFromCenter); + float swirlAngle = u_swirl * .4 * PI * spreadStrength * u_spread * min(1., inradius / max(warpedRadius, 1e-4)); + + vec3 colorSum = vec3(0.); + vec3 weightSum = vec3(0.); + float coverSum = 0.; + + for (int i = 0; i < ${lensDistortionMeta.maxSamples}; i++) { + if (i >= countInteger) break; + + float layer = float(i); + float hue = hueBase + layer * invCount; + + float spread = layer * invSpan; + if (u_bias != 0.) spread = dispersionCurve(spread, biasPower); + + // mix(axis, -axis, spread) is axis scaled by the signed position along the fan + float fanPos = 1. - 2. * spread; + vec2 tapUV = baseUV + spreadAxis * fanPos - .5; + + if (u_swirl != 0.) { + tapUV.x *= u_imageAspectRatio; + tapUV = rotate(tapUV, swirlAngle * fanPos); + tapUV.x *= invAspect; + } + + vec4 tap = sampleOverWhite(tapUV + tapBias); + vec3 weight = 1. - dispersionPower * hueColor(hue); + + colorSum += tap.rgb * weight; + weightSum += weight; + coverSum += tap.a; + } + + vec3 color = colorSum / max(weightSum, 1e-4); + float coverAvg = coverSum * invCount; + + float ground = min(color.r, min(color.g, color.b)); + float alpha = max(coverAvg, 1. - ground); + vec3 premult = max(color - (1. - alpha), 0.); + fragColor = vec4(premult, alpha) * bulgeFade; + + if (u_grainOverlay > 0.) { + float grain = valueNoise(rotate(grainUV, 1.) + vec2(3.)); + grain = mix(grain, valueNoise(rotate(grainUV, 2.) + vec2(-1.)), .5); + grain = pow(grain, 1.3); + float grainV = grain * 2. - 1.; + float grainStrength = pow(u_grainOverlay * abs(grainV), .8) * fragColor.a; + fragColor.rgb = mix(fragColor.rgb, vec3(step(0., grainV)) * fragColor.a, .35 * grainStrength); + } +} +`; + +export interface LensDistortionUniforms extends ShaderSizingUniforms { + u_image: HTMLImageElement | string | undefined; + u_spread: number; + u_bias: number; + u_angle: number; + u_perspective: number; + u_count: number; + u_dispersion: number; + u_dispersionShift: number; + u_dispersionColor: number; + u_focusCenter: number; + u_focusEdges: number; + u_swirl: number; + u_noise: number; + u_noiseFrequency: number; + u_noiseOffset: number; + u_lensBulge: number; + u_lensCircle: number; + u_grainMixer: number; + u_grainOverlay: number; + u_imageX: number; + u_imageY: number; +} + +export interface LensDistortionParams extends ShaderSizingParams, ShaderMotionParams { + image?: HTMLImageElement | string; + spread?: number; + bias?: number; + angle?: number; + perspective?: number; + count?: number; + dispersion?: number; + dispersionShift?: number; + dispersionColor?: number; + focusCenter?: number; + focusEdges?: number; + swirl?: number; + noise?: number; + noiseFrequency?: number; + noiseOffset?: number; + lensBulge?: number; + lensCircle?: number; + grainMixer?: number; + grainOverlay?: number; + imageX?: number; + imageY?: number; +} From bdcb9998663390c1cfa303be989355cd176d45b6 Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 14 Aug 2026 23:40:38 +0400 Subject: [PATCH 122/186] ControlRules removal --- docs/src/app/(shaders)/paper-texture/page.tsx | 93 +++++++------------ 1 file changed, 35 insertions(+), 58 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 0fabaf091..10e4e9976 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -5,14 +5,8 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { - ShaderFit, - PaperTextureFoldTypes, - paperTextureMeta, - type PaperTextureFoldType -} from '@paper-design/shaders'; +import { ShaderFit, PaperTextureFoldTypes, paperTextureMeta, type PaperTextureFoldType } from '@paper-design/shaders'; import { levaImageButton, levaDeleteImageButton } from '@/helpers/leva-image-button'; -import { applyControlRules, type ControlRules } from '@/helpers/leva-control-rules'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; import { ShaderDetails } from '@/components/shader-details'; @@ -22,17 +16,6 @@ import { useUrlParams } from '@/helpers/use-url-params'; const { worldWidth, worldHeight, ...defaults } = paperTexturePresets[0].params; -const controlRules: ControlRules = { - roughnessSize: { showWhen: { roughness: { moreThan: 0 } } }, - fiberSize: { showWhen: { fiber: { moreThan: 0 } } }, - foldType: { showWhen: { folds: { moreThan: 0 } } }, - foldCount: { showWhen: { folds: { moreThan: 0 }, foldType: { is: 'folds' } } }, - foldSize: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, - foldY: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, - foldOffset: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, - foldsShape: { showWhen: { folds: { moreThan: 0 }, foldType: { isNot: 'folds' } } }, -}; - const imageFiles = [ '001.webp', '002.webp', @@ -83,47 +66,41 @@ const PaperTextureWithControls = () => { button(() => setParamsSafe(params, setParams, preset)), ]) ); - const hasImage = image !== ''; - return applyControlRules( - { - colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - ...(hasImage && { - blending: { value: defaults.blending, min: 0, max: 1, order: 150 }, - distortion: { value: defaults.distortion, min: -1, max: 1, order: 151 }, - background: { value: defaults.background, order: 152 }, - }), - roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, - roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, - fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, - fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, - folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, - foldType: { - value: defaults.foldType, - options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], - order: 231, - }, - foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, - foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, - foldY: { value: defaults.foldY, order: 232.6 }, - foldOffset: { value: defaults.foldOffset, min: -0.5, max: 0.5, order: 233 }, - foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 234 }, - drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, - fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, - scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, - fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, - Image: folder( - { - 'Upload image': levaImageButton(setImageWithoutStatus), - ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), - }, - { order: 0 } - ), - Presets: folder(presets, { order: -1 }), + return { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + blending: { value: defaults.blending, min: 0, max: 1, order: 150 }, + distortion: { value: defaults.distortion, min: -1, max: 1, order: 151 }, + background: { value: defaults.background, order: 152 }, + roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, + roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, + fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, + fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, + folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, + foldType: { + value: defaults.foldType, + options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], + order: 231, }, - controlRules - ); + foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, + foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, + foldY: { value: defaults.foldY, order: 232.6 }, + foldOffset: { value: defaults.foldOffset, min: -0.5, max: 0.5, order: 233 }, + foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 234 }, + drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, + seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, + fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, + scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, + fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, + Image: folder( + { + 'Upload image': levaImageButton(setImageWithoutStatus), + ...(image && { 'Delete image': levaDeleteImageButton(() => setImage('')) }), + }, + { order: 0 } + ), + Presets: folder(presets, { order: -1 }), + }; }, [image]); // Reset to defaults on mount, so that Leva doesn't show values from other From d00439c55c9e2d85b019bbfc043292c0c9720d30 Mon Sep 17 00:00:00 2001 From: ksenia Date: Fri, 14 Aug 2026 23:52:16 +0400 Subject: [PATCH 123/186] foldsShape backed to 1 --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 - packages/shaders-react/src/shaders/paper-texture.tsx | 6 ------ packages/shaders/src/shaders/paper-texture.ts | 10 +++------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 10e4e9976..756658a2c 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -86,7 +86,6 @@ const PaperTextureWithControls = () => { foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, foldY: { value: defaults.foldY, order: 232.6 }, foldOffset: { value: defaults.foldOffset, min: -0.5, max: 0.5, order: 233 }, - foldsShape: { value: defaults.foldsShape, min: 0, max: 1, order: 234 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 3da9be578..4bbce5882 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -46,7 +46,6 @@ export const defaultPreset: PaperTexturePreset = { foldSize: 0.79, foldY: false, foldOffset: 0, - foldsShape: 1, fade: 0, drops: 0, seed: 354, @@ -76,7 +75,6 @@ export const cardboardPreset: PaperTexturePreset = { foldSize: 0.45, foldY: false, foldOffset: 0, - foldsShape: 0.55, fade: 0, drops: 0.2, seed: 1.6, @@ -103,7 +101,6 @@ export const spreadPreset: PaperTexturePreset = { foldSize: 1, foldY: true, foldOffset: 0, - foldsShape: 1, drops: 0, fade: 0.68, scale: 0.95, @@ -137,7 +134,6 @@ export const coloredPreset: PaperTexturePreset = { foldSize: 0.79, foldY: false, foldOffset: 0, - foldsShape: 1, fade: 1, drops: 1, seed: 784, @@ -167,7 +163,6 @@ export const PaperTexture: React.FC = memo(function PaperText foldSize = defaultPreset.params.foldSize, foldY = defaultPreset.params.foldY, foldOffset = defaultPreset.params.foldOffset, - foldsShape = defaultPreset.params.foldsShape, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, @@ -211,7 +206,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_foldOffset: foldOffset, u_folds: folds, u_foldType: PaperTextureFoldTypes[foldType], - u_foldsShape: foldsShape, u_fade: fade, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 27cc25509..73f0823d7 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -22,7 +22,6 @@ export const paperTextureMeta = { * - u_folds (float): Depth of the folds (0 to 1) * - u_foldType (float): Type of folds pattern (0 = radial folds, 1 = creases) * - u_foldCount (float): Size/frequency of folds or creases (0 to 1) - * - u_foldsShape (float): Shape/width of the folds (0 to 1) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds and dots (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) @@ -67,7 +66,6 @@ uniform float u_foldType; uniform float u_foldCount; uniform float u_foldSize; uniform bool u_foldY; -uniform float u_foldsShape; uniform float u_foldOffset; uniform float u_drops; uniform float u_seed; @@ -272,7 +270,7 @@ vec4 getCrease(float coord, float offset, float count) { float creaseIdx = floor(g); float depthMod = .7 + .3 * fract(sin(creaseIdx * 127.1 + u_seed * 3.7) * 43758.5453); float dx = crX - .5; - float foldWidth = mix(.1, .5, u_foldsShape); + float foldWidth = .5; float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * depthMod; float slope = sign(dx) * foldAmount; float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); @@ -333,7 +331,7 @@ void main() { float crLightX = max(-.5 * sin(angleX) + .5 * cos(angleX), 0.) * mix(.9, 1., h.y); drops *= mix(1., h.y, u_folds); pattern += u_folds * crLightX; - float distortBaseX = mix(pow(h.y, .2), abs(h.z), .5 * u_foldsShape); + float distortBaseX = mix(pow(h.y, .2), abs(h.z), .5); yShift += .022 * u_folds * (1. - distortBaseX); patternUV.y += yShift; @@ -343,7 +341,7 @@ void main() { float crLightY = max(-.5 * sin(angleY) + .5 * cos(angleY), 0.) * mix(.9, 1., v.y); drops *= mix(1., v.y, u_folds); pattern += u_folds * crLightY; - float distortBaseY = mix(pow(v.y, .2), abs(v.z), .5 * u_foldsShape); + float distortBaseY = mix(pow(v.y, .2), abs(v.z), .5); float xFan = 2. * (imageUV.x - .5); xDistortion -= .02 * u_folds * (1. - distortBaseY) * xFan; patternUV.x -= .022 * u_folds * (1. - distortBaseY) * xFan; @@ -444,7 +442,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_foldSize: number; u_foldY: boolean; u_foldOffset: number; - u_foldsShape: number; u_fade: number; u_drops: number; u_seed: number; @@ -467,7 +464,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara foldSize?: number; foldY?: boolean; foldOffset?: number; - foldsShape?: number; fade?: number; drops?: number; seed?: number; From 920b64ac60e37f9717d4c4f8d65c8ada892076b8 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 15 Aug 2026 18:44:14 +0400 Subject: [PATCH 124/186] control rules full removal --- docs/src/helpers/leva-control-rules.ts | 42 -------------------------- 1 file changed, 42 deletions(-) delete mode 100644 docs/src/helpers/leva-control-rules.ts diff --git a/docs/src/helpers/leva-control-rules.ts b/docs/src/helpers/leva-control-rules.ts deleted file mode 100644 index a84909165..000000000 --- a/docs/src/helpers/leva-control-rules.ts +++ /dev/null @@ -1,42 +0,0 @@ -export type ControlCondition = - | { moreThan: number } - | { is: string | number | boolean } - | { isNot: string | number | boolean }; - -export type ControlRule = { - showWhen: Record; -}; - -export type ControlRules = Record; - -function evaluateCondition(value: unknown, condition: ControlCondition): boolean { - if ('moreThan' in condition) return typeof value === 'number' && value > condition.moreThan; - if ('is' in condition) return value === condition.is; - return value !== condition.isNot; -} - -/** Adds Leva `render` callbacks from `rules`; controls not in `rules` pass through. */ -export function applyControlRules( - schema: Record, - rules: ControlRules -): Record { - const result: Record = { ...schema }; - - for (const [name, rule] of Object.entries(rules)) { - const control = result[name]; - if (control == null || typeof control !== 'object') continue; - - const passesRule = (get: (key: string) => unknown) => - Object.entries(rule.showWhen).every(([dep, condition]) => evaluateCondition(get(dep), condition)); - - result[name] = { - ...control, - label: `↳ ${name}`, - render: control.render - ? (get: (key: string) => unknown) => control.render(get) && passesRule(get) - : passesRule, - }; - } - - return result; -} \ No newline at end of file From 7b8e7e6fa6bc317dee1efb348dc4b2ace6235afe Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 15 Aug 2026 18:45:01 +0400 Subject: [PATCH 125/186] defs update --- docs/src/shader-defs/paper-texture-def.ts | 80 ++++++++++++++----- packages/shaders/src/shaders/paper-texture.ts | 17 ++-- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 935b495d7..784f7dd0e 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -29,12 +29,27 @@ export const paperTextureDef: ShaderDef = { description: 'Foreground color', }, { - name: 'contrast', + name: 'blending', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.contrast, - description: 'Blending behavior (sharper vs. smoother color transitions)', + defaultValue: defaultParams.blending, + description: + 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the paper ink), needs image', + }, + { + name: 'distortion', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.distortion, + description: 'Amount of image distortion following the paper surface (needs image)', + }, + { + name: 'background', + type: 'boolean', + defaultValue: defaultParams.background, + description: 'Shows or hides the paper texture outside the image frame', }, { name: 'roughness', @@ -44,6 +59,14 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.roughness, description: 'Pixel noise, related to canvas (not scalable)', }, + { + name: 'roughnessSize', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.roughnessSize, + description: 'Scale of the roughness noise (needs roughness > 0)', + }, { name: 'fiber', type: 'number', @@ -55,43 +78,56 @@ export const paperTextureDef: ShaderDef = { { name: 'fiberSize', type: 'number', - min: 0.01, + min: 0, max: 1, defaultValue: defaultParams.fiberSize, description: 'Curly-shaped noise scale (needs fiber > 0)', }, { - name: 'crumples', + name: 'folds', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.crumples, - description: 'Cell-based crumple pattern', + defaultValue: defaultParams.folds, + description: 'Depth of the folds', }, { - name: 'crumpleSize', + name: 'foldType', + type: 'enum', + defaultValue: defaultParams.foldType, + description: 'The type of the fold pattern (needs folds > 0)', + options: ['folds', 'creases'], + }, + { + name: 'foldCount', type: 'number', - min: 0.01, - max: 1, - defaultValue: defaultParams.crumpleSize, - description: 'Cell-based crumple pattern scale (needs crumples > 0)', + min: 1, + max: 20, + step: 1, + defaultValue: defaultParams.foldCount, + description: 'Number of radial folds (20 max), needs folds > 0 and foldType = folds', }, { - name: 'folds', + name: 'foldSize', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.folds, - description: 'Depth of the folds (needs contrast > 0 to shade the surface)', + defaultValue: defaultParams.foldSize, + description: 'Size of the creases, needs folds > 0 and foldType = creases', }, { - name: 'foldCount', + name: 'foldY', + type: 'boolean', + defaultValue: defaultParams.foldY, + description: 'Adds a horizontal crease, needs folds > 0 and foldType = creases', + }, + { + name: 'foldOffset', type: 'number', - min: 1, - max: 15, - step: 1, - defaultValue: defaultParams.foldCount, - description: 'Number of folds (15 max), needs folds > 0', + min: -0.5, + max: 0.5, + defaultValue: defaultParams.foldOffset, + description: 'Shifts the creases across the surface, needs folds > 0 and foldType = creases', }, { name: 'fade', @@ -115,7 +151,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1000, defaultValue: defaultParams.seed, - description: 'Seed applied to folds, crumples, dots and fade', + description: 'Seed applied to folds, drops and fade', }, ...staticImageCommonParams, ], diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 73f0823d7..6fda6a867 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -16,17 +16,20 @@ export const paperTextureMeta = { * - u_colorFront (vec4): Foreground color in RGBA * - u_colorBack (vec4): Background color in RGBA * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) - * - u_roughnessSize (float): Scale of the roughness noise (0 to 1) + * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) - * - u_fiberSize (float): Curly-shaped noise scale (0 to 1) + * - u_fiberSize (float): Curly-shaped noise scale, needs u_fiber > 0 (0 to 1) * - u_folds (float): Depth of the folds (0 to 1) - * - u_foldType (float): Type of folds pattern (0 = radial folds, 1 = creases) - * - u_foldCount (float): Size/frequency of folds or creases (0 to 1) + * - u_foldType (float): Type of folds pattern, needs u_folds > 0 (0 = radial folds, 1 = creases) + * - u_foldCount (float): Number of radial folds, needs u_folds > 0 and u_foldType = 0 (1 to 20) + * - u_foldSize (float): Size of the creases, needs u_folds > 0 and u_foldType = 1 (0 to 1) + * - u_foldY (bool): Adds a horizontal crease, needs u_folds > 0 and u_foldType = 1 + * - u_foldOffset (float): Shifts the creases across the surface, needs u_folds > 0 and u_foldType = 1 (-0.5 to 0.5) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) - * - u_seed (float): Seed applied to folds and dots (0 to 1000) + * - u_seed (float): Seed applied to folds, drops and fade (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) - * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through (0 to 1) - * - u_distortion (float): Amount of distortion of the image by the paper normals (0 to 1) + * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through, needs image (0 to 1) + * - u_distortion (float): Amount of distortion of the image by the paper normals, needs image (-1 to 1) * - u_background (bool): Shows or hides the paper texture outside the image frame * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * From 66b9b87834278459d5863a9057fb28c01f574c23 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 15 Aug 2026 19:39:02 +0400 Subject: [PATCH 126/186] remove conditional relations between props --- docs/src/app/(shaders)/paper-texture/page.tsx | 16 +-- docs/src/shader-defs/paper-texture-def.ts | 41 +++--- .../src/shaders/paper-texture.tsx | 54 ++++---- packages/shaders/src/index.ts | 9 +- packages/shaders/src/shaders/paper-texture.ts | 122 +++++++++--------- 5 files changed, 119 insertions(+), 123 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 756658a2c..987b15065 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -5,7 +5,7 @@ import { useControls, button, folder } from 'leva'; import { setParamsSafe, useResetLevaParams } from '@/helpers/use-reset-leva-params'; import { usePresetHighlight } from '@/helpers/use-preset-highlight'; import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; -import { ShaderFit, PaperTextureFoldTypes, paperTextureMeta, type PaperTextureFoldType } from '@paper-design/shaders'; +import { ShaderFit, paperTextureMeta } from '@paper-design/shaders'; import { levaImageButton, levaDeleteImageButton } from '@/helpers/leva-image-button'; import { useState, useEffect, useCallback } from 'react'; import { toHsla } from '@/helpers/color-utils'; @@ -77,15 +77,11 @@ const PaperTextureWithControls = () => { fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, - foldType: { - value: defaults.foldType, - options: Object.keys(PaperTextureFoldTypes) as PaperTextureFoldType[], - order: 231, - }, - foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 232 }, - foldSize: { value: defaults.foldSize, min: 0, max: 1, order: 232.5 }, - foldY: { value: defaults.foldY, order: 232.6 }, - foldOffset: { value: defaults.foldOffset, min: -0.5, max: 0.5, order: 233 }, + foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 231 }, + creases: { value: defaults.creases, min: 0, max: 1, order: 235 }, + creaseSize: { value: defaults.creaseSize, min: 0, max: 1, order: 236 }, + creaseCross: { value: defaults.creaseCross, min: 0, max: 1, order: 237 }, + creaseOffset: { value: defaults.creaseOffset, min: -0.5, max: 0.5, order: 238 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 784f7dd0e..e21e3f365 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -89,14 +89,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.folds, - description: 'Depth of the folds', - }, - { - name: 'foldType', - type: 'enum', - defaultValue: defaultParams.foldType, - description: 'The type of the fold pattern (needs folds > 0)', - options: ['folds', 'creases'], + description: 'Depth of the radial folds', }, { name: 'foldCount', @@ -105,29 +98,39 @@ export const paperTextureDef: ShaderDef = { max: 20, step: 1, defaultValue: defaultParams.foldCount, - description: 'Number of radial folds (20 max), needs folds > 0 and foldType = folds', + description: 'Number of radial folds (20 max), needs folds > 0', }, { - name: 'foldSize', + name: 'creases', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.foldSize, - description: 'Size of the creases, needs folds > 0 and foldType = creases', + defaultValue: defaultParams.creases, + description: 'Depth of the creases, an independent layer over the radial folds', }, { - name: 'foldY', - type: 'boolean', - defaultValue: defaultParams.foldY, - description: 'Adds a horizontal crease, needs folds > 0 and foldType = creases', + name: 'creaseSize', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.creaseSize, + description: 'Size of the creases (needs creases > 0)', + }, + { + name: 'creaseCross', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.creaseCross, + description: 'Depth of a single horizontal crease running across the others (needs creases > 0)', }, { - name: 'foldOffset', + name: 'creaseOffset', type: 'number', min: -0.5, max: 0.5, - defaultValue: defaultParams.foldOffset, - description: 'Shifts the creases across the surface, needs folds > 0 and foldType = creases', + defaultValue: defaultParams.creaseOffset, + description: 'Shifts the creases across the surface (needs creases > 0)', }, { name: 'fade', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 4bbce5882..78c82dc93 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -6,7 +6,6 @@ import { getShaderColorFromString, getShaderNoiseTexture, paperTextureFragmentShader, - PaperTextureFoldTypes, ShaderFitOptions, type ImageShaderPreset, type PaperTextureParams, @@ -41,11 +40,11 @@ export const defaultPreset: PaperTexturePreset = { fiber: 0.5, fiberSize: 0.5, folds: 0.7, - foldType: 'folds', foldCount: 15, - foldSize: 0.79, - foldY: false, - foldOffset: 0, + creases: 0, + creaseSize: 0.79, + creaseCross: 0, + creaseOffset: 0, fade: 0, drops: 0, seed: 354, @@ -69,12 +68,12 @@ export const cardboardPreset: PaperTexturePreset = { roughnessSize: 0.65, fiber: 0.35, fiberSize: 0.14, - folds: 0.25, - foldType: 'creases', + folds: 0, foldCount: 18, - foldSize: 0.45, - foldY: false, - foldOffset: 0, + creases: 0.25, + creaseSize: 0.45, + creaseCross: 0, + creaseOffset: 0, fade: 0, drops: 0.2, seed: 1.6, @@ -95,12 +94,12 @@ export const spreadPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, - folds: 0.7, - foldType: 'creases', + folds: 0, foldCount: 1, - foldSize: 1, - foldY: true, - foldOffset: 0, + creases: 0.7, + creaseSize: 1, + creaseCross: 1, + creaseOffset: 0, drops: 0, fade: 0.68, scale: 0.95, @@ -111,7 +110,6 @@ export const spreadPreset: PaperTexturePreset = { }, }; - export const coloredPreset: PaperTexturePreset = { name: 'Colored', params: { @@ -129,11 +127,11 @@ export const coloredPreset: PaperTexturePreset = { fiber: 0.5, fiberSize: 0.5, folds: 1, - foldType: 'folds', foldCount: 10, - foldSize: 0.79, - foldY: false, - foldOffset: 0, + creases: 0, + creaseSize: 0.79, + creaseCross: 0, + creaseOffset: 0, fade: 1, drops: 1, seed: 784, @@ -159,10 +157,10 @@ export const PaperTexture: React.FC = memo(function PaperText roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, folds = defaultPreset.params.folds, - foldType = defaultPreset.params.foldType, - foldSize = defaultPreset.params.foldSize, - foldY = defaultPreset.params.foldY, - foldOffset = defaultPreset.params.foldOffset, + creases = defaultPreset.params.creases, + creaseSize = defaultPreset.params.creaseSize, + creaseCross = defaultPreset.params.creaseCross, + creaseOffset = defaultPreset.params.creaseOffset, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, @@ -201,11 +199,11 @@ export const PaperTexture: React.FC = memo(function PaperText u_fiber: fiber, u_fiberSize: fiberSize, u_foldCount: foldCount, - u_foldSize: foldSize, - u_foldY: foldY, - u_foldOffset: foldOffset, + u_creaseSize: creaseSize, + u_creaseCross: creaseCross, + u_creaseOffset: creaseOffset, u_folds: folds, - u_foldType: PaperTextureFoldTypes[foldType], + u_creases: creases, u_fade: fade, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index b01a4ae2a..904e32d6e 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -157,8 +157,6 @@ export { export { paperTextureFragmentShader, paperTextureMeta, - PaperTextureFoldTypes, - type PaperTextureFoldType, type PaperTextureParams, type PaperTextureUniforms, } from './shaders/paper-texture.js'; @@ -185,7 +183,12 @@ export { } from './shaders/image-dithering.js'; /** Lens Distortion image filter that separates an image into shifting color layers, recreating the chromatic aberration of a lens, and warps the image geometry with barrel or pincushion curvature */ -export { lensDistortionFragmentShader, lensDistortionMeta, type LensDistortionParams, type LensDistortionUniforms } from './shaders/lens-distortion.js'; +export { + lensDistortionFragmentShader, + lensDistortionMeta, + type LensDistortionParams, + type LensDistortionUniforms, +} from './shaders/lens-distortion.js'; /** A glowing gradient of colors flowing through an input image. The effect creates a smoothly animated wave of intensity across the image */ export { diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6fda6a867..271a7664f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -19,12 +19,12 @@ export const paperTextureMeta = { * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) * - u_fiber (float): Curly-shaped noise intensity (0 to 1) * - u_fiberSize (float): Curly-shaped noise scale, needs u_fiber > 0 (0 to 1) - * - u_folds (float): Depth of the folds (0 to 1) - * - u_foldType (float): Type of folds pattern, needs u_folds > 0 (0 = radial folds, 1 = creases) - * - u_foldCount (float): Number of radial folds, needs u_folds > 0 and u_foldType = 0 (1 to 20) - * - u_foldSize (float): Size of the creases, needs u_folds > 0 and u_foldType = 1 (0 to 1) - * - u_foldY (bool): Adds a horizontal crease, needs u_folds > 0 and u_foldType = 1 - * - u_foldOffset (float): Shifts the creases across the surface, needs u_folds > 0 and u_foldType = 1 (-0.5 to 0.5) + * - u_folds (float): Depth of the radial folds (0 to 1) + * - u_foldCount (float): Number of radial folds, needs u_folds > 0 (1 to 20) + * - u_creases (float): Depth of the creases, an independent layer over the radial folds (0 to 1) + * - u_creaseSize (float): Size of the creases, needs u_creases > 0 (0 to 1) + * - u_creaseCross (float): Depth of a single horizontal crease running across the others, needs u_creases > 0 (0 to 1) + * - u_creaseOffset (float): Shifts the creases across the surface, needs u_creases > 0 (-0.5 to 0.5) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds, drops and fade (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) @@ -65,11 +65,11 @@ uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; uniform float u_folds; -uniform float u_foldType; uniform float u_foldCount; -uniform float u_foldSize; -uniform bool u_foldY; -uniform float u_foldOffset; +uniform float u_creases; +uniform float u_creaseSize; +uniform float u_creaseCross; +uniform float u_creaseOffset; uniform float u_drops; uniform float u_seed; uniform float u_fade; @@ -310,45 +310,48 @@ void main() { drops = mix(drops, 0., fade); } + // Radial folds and creases are independent layers: both may be visible at once. + // Folds go first, so the crease shift below also carries the roughness and fiber layers. if (u_folds > 0.) { - if (u_foldType < .5) { - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .03 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .015 + u_seed)).rg - .5); - vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); - vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); - radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); - float foldsPattern = radialFolds.x + radialFolds.y; - - pattern += u_folds * foldsPattern; - - vec2 fromCenter = imageUV - .5; - scaleDistortion = .22 * radialFolds.z * u_folds; - } else { - vec2 uv = imageUV + .5; - float count = mix(25., 1., pow(u_foldSize, .4)); - float offset = 1. - u_foldOffset; - - // Normal creases: run vertically (vary along x), displacing the image in y. - vec4 h = getCrease(uv.x, offset, count); - float angleX = h.x * 1.1345; - float crLightX = max(-.5 * sin(angleX) + .5 * cos(angleX), 0.) * mix(.9, 1., h.y); - drops *= mix(1., h.y, u_folds); - pattern += u_folds * crLightX; - float distortBaseX = mix(pow(h.y, .2), abs(h.z), .5); - yShift += .022 * u_folds * (1. - distortBaseX); - patternUV.y += yShift; - - if (u_foldY) { - vec4 v = getCrease(uv.y, 1., 1.); - float angleY = v.x * 1.1345; - float crLightY = max(-.5 * sin(angleY) + .5 * cos(angleY), 0.) * mix(.9, 1., v.y); - drops *= mix(1., v.y, u_folds); - pattern += u_folds * crLightY; - float distortBaseY = mix(pow(v.y, .2), abs(v.z), .5); - float xFan = 2. * (imageUV.x - .5); - xDistortion -= .02 * u_folds * (1. - distortBaseY) * xFan; - patternUV.x -= .022 * u_folds * (1. - distortBaseY) * xFan; - } + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = foldsUV1 + .03 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .015 + u_seed)).rg - .5); + vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); + vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); + radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); + float foldsPattern = radialFolds.x + radialFolds.y; + + pattern += u_folds * foldsPattern; + + scaleDistortion = .22 * radialFolds.z * u_folds; + } + + if (u_creases > 0.) { + // Aspect-corrected, so crease spacing is even in screen units rather than stretched with the image. + vec2 uv = 1. + (imageUV - .5) * vec2(u_imageAspectRatio, 1.); + float count = mix(25., 1., pow(u_creaseSize, .4)); + float offset = 1. - u_creaseOffset; + + // Normal creases: run vertically (vary along x), displacing the image in y. + vec4 h = getCrease(uv.x, offset, count); + float angleX = h.x * 1.1345; + float crLightX = max(-.5 * sin(angleX) + .5 * cos(angleX), 0.) * mix(.9, 1., h.y); + drops *= mix(1., h.y, u_creases); + pattern += u_creases * crLightX; + float distortBaseX = mix(pow(h.y, .2), abs(h.z), .5); + yShift += .022 * u_creases * (1. - distortBaseX); + patternUV.y += yShift; + + if (u_creaseCross > 0.) { + float crossDepth = u_creases * u_creaseCross; + vec4 v = getCrease(uv.y, 1., 1.); + float angleY = v.x * 1.1345; + float crLightY = max(-.5 * sin(angleY) + .5 * cos(angleY), 0.) * mix(.9, 1., v.y); + drops *= mix(1., v.y, crossDepth); + pattern += crossDepth * crLightY; + float distortBaseY = mix(pow(v.y, .2), abs(v.z), .5); + float xFan = 2. * (imageUV.x - .5); + xDistortion -= .02 * crossDepth * (1. - distortBaseY) * xFan; + patternUV.x -= .022 * crossDepth * (1. - distortBaseY) * xFan; } } @@ -440,11 +443,11 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_fiber: number; u_fiberSize: number; u_folds: number; - u_foldType: number; u_foldCount: number; - u_foldSize: number; - u_foldY: boolean; - u_foldOffset: number; + u_creases: number; + u_creaseSize: number; + u_creaseCross: number; + u_creaseOffset: number; u_fade: number; u_drops: number; u_seed: number; @@ -462,11 +465,11 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara fiber?: number; fiberSize?: number; folds?: number; - foldType?: PaperTextureFoldType; foldCount?: number; - foldSize?: number; - foldY?: boolean; - foldOffset?: number; + creases?: number; + creaseSize?: number; + creaseCross?: number; + creaseOffset?: number; fade?: number; drops?: number; seed?: number; @@ -474,10 +477,3 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara distortion?: number; background?: boolean; } - -export type PaperTextureFoldType = 'folds' | 'creases'; - -export const PaperTextureFoldTypes: Record = { - folds: 0, - creases: 1, -}; From f94c12827f2eac34849033f0d74dfd57a62ec5ec Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 17 Aug 2026 19:24:01 +0400 Subject: [PATCH 127/186] big update --- docs/src/app/(shaders)/paper-texture/page.tsx | 8 +- docs/src/shader-defs/paper-texture-def.ts | 36 +++- .../src/shaders/paper-texture.tsx | 49 +++-- packages/shaders/src/shaders/paper-texture.ts | 172 +++++++++++------- 4 files changed, 171 insertions(+), 94 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 987b15065..3708e7b0d 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -79,9 +79,11 @@ const PaperTextureWithControls = () => { folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 231 }, creases: { value: defaults.creases, min: 0, max: 1, order: 235 }, - creaseSize: { value: defaults.creaseSize, min: 0, max: 1, order: 236 }, - creaseCross: { value: defaults.creaseCross, min: 0, max: 1, order: 237 }, - creaseOffset: { value: defaults.creaseOffset, min: -0.5, max: 0.5, order: 238 }, + creaseSizeX: { value: defaults.creaseSizeX, min: 0, max: 1, order: 236 }, + creaseSizeY: { value: defaults.creaseSizeY, min: 0, max: 1, order: 237 }, + creaseOffsetX: { value: defaults.creaseOffsetX, min: -0.5, max: 0.5, order: 238 }, + creaseOffsetY: { value: defaults.creaseOffsetY, min: -0.5, max: 0.5, order: 239 }, + lightAngle: { value: defaults.lightAngle, min: 0, max: 360, order: 240 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index e21e3f365..8f4cbde4c 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -49,7 +49,7 @@ export const paperTextureDef: ShaderDef = { name: 'background', type: 'boolean', defaultValue: defaultParams.background, - description: 'Shows or hides the paper texture outside the image frame', + description: 'Shows or hides the paper texture outside the image frame (needs image)', }, { name: 'roughness', @@ -109,28 +109,44 @@ export const paperTextureDef: ShaderDef = { description: 'Depth of the creases, an independent layer over the radial folds', }, { - name: 'creaseSize', + name: 'creaseSizeX', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creaseSize, - description: 'Size of the creases (needs creases > 0)', + defaultValue: defaultParams.creaseSizeX, + description: 'Size of the vertical creases (needs creases > 0)', }, { - name: 'creaseCross', + name: 'creaseSizeY', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creaseCross, - description: 'Depth of a single horizontal crease running across the others (needs creases > 0)', + defaultValue: defaultParams.creaseSizeY, + description: 'Size of the horizontal creases running across them (needs creases > 0)', }, { - name: 'creaseOffset', + name: 'creaseOffsetX', type: 'number', min: -0.5, max: 0.5, - defaultValue: defaultParams.creaseOffset, - description: 'Shifts the creases across the surface (needs creases > 0)', + defaultValue: defaultParams.creaseOffsetX, + description: 'Shifts the vertical creases across the surface (needs creases > 0)', + }, + { + name: 'creaseOffsetY', + type: 'number', + min: -0.5, + max: 0.5, + defaultValue: defaultParams.creaseOffsetY, + description: 'Shifts the horizontal creases across the surface (needs creases > 0)', + }, + { + name: 'lightAngle', + type: 'number', + min: 0, + max: 360, + defaultValue: defaultParams.lightAngle, + description: 'Direction the folded surface is lit from, clockwise from the top of the canvas, in degrees', }, { name: 'fade', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 78c82dc93..cc900b361 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -42,9 +42,11 @@ export const defaultPreset: PaperTexturePreset = { folds: 0.7, foldCount: 15, creases: 0, - creaseSize: 0.79, - creaseCross: 0, - creaseOffset: 0, + creaseSizeX: 0.32, + creaseSizeY: 0.89, + creaseOffsetX: 0, + creaseOffsetY: 0, + lightAngle: 90, fade: 0, drops: 0, seed: 354, @@ -71,9 +73,11 @@ export const cardboardPreset: PaperTexturePreset = { folds: 0, foldCount: 18, creases: 0.25, - creaseSize: 0.45, - creaseCross: 0, - creaseOffset: 0, + creaseSizeX: 0, + creaseSizeY: 0.89, + creaseOffsetX: 0, + creaseOffsetY: 0, + lightAngle: 90, fade: 0, drops: 0.2, seed: 1.6, @@ -97,9 +101,11 @@ export const spreadPreset: PaperTexturePreset = { folds: 0, foldCount: 1, creases: 0.7, - creaseSize: 1, - creaseCross: 1, - creaseOffset: 0, + creaseSizeX: 0.67, + creaseSizeY: 0.89, + creaseOffsetX: 0, + creaseOffsetY: 0, + lightAngle: 90, drops: 0, fade: 0.68, scale: 0.95, @@ -129,9 +135,11 @@ export const coloredPreset: PaperTexturePreset = { folds: 1, foldCount: 10, creases: 0, - creaseSize: 0.79, - creaseCross: 0, - creaseOffset: 0, + creaseSizeX: 0.32, + creaseSizeY: 0.89, + creaseOffsetX: 0, + creaseOffsetY: 0, + lightAngle: 90, fade: 1, drops: 1, seed: 784, @@ -158,9 +166,11 @@ export const PaperTexture: React.FC = memo(function PaperText fiber = defaultPreset.params.fiber, folds = defaultPreset.params.folds, creases = defaultPreset.params.creases, - creaseSize = defaultPreset.params.creaseSize, - creaseCross = defaultPreset.params.creaseCross, - creaseOffset = defaultPreset.params.creaseOffset, + creaseSizeX = defaultPreset.params.creaseSizeX, + creaseSizeY = defaultPreset.params.creaseSizeY, + creaseOffsetX = defaultPreset.params.creaseOffsetX, + creaseOffsetY = defaultPreset.params.creaseOffsetY, + lightAngle = defaultPreset.params.lightAngle, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, @@ -192,6 +202,7 @@ export const PaperTexture: React.FC = memo(function PaperText const uniforms = { // Own uniforms u_image: image, + u_isImage: Boolean(image), u_colorFront: getShaderColorFromString(colorFront), u_colorBack: getShaderColorFromString(colorBack), u_roughness: roughness, @@ -199,9 +210,11 @@ export const PaperTexture: React.FC = memo(function PaperText u_fiber: fiber, u_fiberSize: fiberSize, u_foldCount: foldCount, - u_creaseSize: creaseSize, - u_creaseCross: creaseCross, - u_creaseOffset: creaseOffset, + u_creaseSizeX: creaseSizeX, + u_creaseSizeY: creaseSizeY, + u_creaseOffsetX: creaseOffsetX, + u_creaseOffsetY: creaseOffsetY, + u_lightAngle: lightAngle, u_folds: folds, u_creases: creases, u_fade: fade, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 271a7664f..20b481144 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -12,6 +12,7 @@ export const paperTextureMeta = { * * Fragment shader uniforms: * - u_image (sampler2D): Optional source image texture + * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image * - u_colorFront (vec4): Foreground color in RGBA * - u_colorBack (vec4): Background color in RGBA @@ -22,15 +23,17 @@ export const paperTextureMeta = { * - u_folds (float): Depth of the radial folds (0 to 1) * - u_foldCount (float): Number of radial folds, needs u_folds > 0 (1 to 20) * - u_creases (float): Depth of the creases, an independent layer over the radial folds (0 to 1) - * - u_creaseSize (float): Size of the creases, needs u_creases > 0 (0 to 1) - * - u_creaseCross (float): Depth of a single horizontal crease running across the others, needs u_creases > 0 (0 to 1) - * - u_creaseOffset (float): Shifts the creases across the surface, needs u_creases > 0 (-0.5 to 0.5) + * - u_creaseSizeX (float): Size of the vertical creases, needs u_creases > 0 (0 to 1) + * - u_creaseSizeY (float): Size of the horizontal creases running across them, needs u_creases > 0 (0 to 1) + * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (-0.5 to 0.5) + * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (-0.5 to 0.5) + * - u_lightAngle (float): Direction the folded surface is lit from, clockwise from the top of the canvas, in degrees (0 to 360) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds, drops and fade (0 to 1000) * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through, needs image (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals, needs image (-1 to 1) - * - u_background (bool): Shows or hides the paper texture outside the image frame + * - u_background (bool): Shows or hides the paper texture outside the image frame, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -54,11 +57,11 @@ export const paperTextureMeta = { export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; -uniform vec2 u_resulution; uniform vec4 u_colorFront; uniform vec4 u_colorBack; uniform sampler2D u_image; +uniform bool u_isImage; uniform float u_imageAspectRatio; uniform float u_roughness; @@ -67,9 +70,11 @@ uniform float u_fiberSize; uniform float u_folds; uniform float u_foldCount; uniform float u_creases; -uniform float u_creaseSize; -uniform float u_creaseCross; -uniform float u_creaseOffset; +uniform float u_creaseSizeX; +uniform float u_creaseSizeY; +uniform float u_creaseOffsetX; +uniform float u_creaseOffsetY; +uniform float u_lightAngle; uniform float u_drops; uniform float u_seed; uniform float u_fade; @@ -82,6 +87,16 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; +#define FACET_CURVE 2. +#define FOLDS_TO_CREASES .02 +#define CELL_EDGE .4 +#define GRAIN_BY_LIGHT .5 +#define CREASE_WEAR .6 +#define LIGHT_ELEVATION 60. +#define LIFT_SHARPNESS 4. +#define GRAIN_DRAG .2 +#define SHADING_SCALE 1. + float getUvFrame(vec2 uv, float blur) { float left = smoothstep(0., blur, uv.x); float right = 1. - smoothstep(1. - blur, 1., uv.x); @@ -238,10 +253,16 @@ float getDrops(vec2 uv) { return 1. - lst(.05, .09, sqrt(dropsMinDist)); } +vec2 getCellTilt(float idx) { + vec2 rand = randomGB(vec2(idx + 31., idx * u_seed + 17.)); + float an = rand.x * TWO_PI; + return vec2(cos(an), sin(an)) * rand.y; +} + vec4 getFolds(vec2 uv1, vec2 uv2) { - vec3 pp1 = vec3(0.), pp2 = vec3(0.); - float l1 = 9., l2 = 9.; - float cellRand = 0.; + float l1 = 9., l2 = 9., l2b = 9.; + float idx1 = 0., idx2 = 0.; + vec2 p1 = vec2(0.), p2 = vec2(0.); for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); @@ -250,35 +271,39 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { float dist1 = distance(uv1, p); if (dist1 < l1) { l1 = dist1; - pp1 = vec3(uv1.x - p.x, dist1, rand.y); + idx1 = float(i); + p1 = p; } float dist2 = distance(uv2, p); if (dist2 < l2) { + l2b = l2; l2 = dist2; - pp2 = vec3(uv2.x - p.x, dist2, rand.y); - cellRand = .5 * (rand.x + rand.y); + idx2 = float(i); + p2 = p; + } else if (dist2 < l2b) { + l2b = dist2; } } - return vec4( - mix(pp1.x, .17 * pp1.z, pow(pp1.y, .02)), - mix(pp2.x, .18 * pp2.z, pow(pp2.y, .02)), - .2 * pp2.y, - cellRand - ); + + vec2 tilt1 = getCellTilt(idx1) + FACET_CURVE * (uv1 - p1); + vec2 tilt2 = getCellTilt(idx2) + FACET_CURVE * (uv2 - p2); + + return vec4(.5 * (tilt1 + tilt2), .2 * l2, smoothstep(0., CELL_EDGE, l2b - l2)); } -vec4 getCrease(float coord, float offset, float count) { +vec3 getCrease(float coord, float offset, float count) { float g = (coord - 1.) * count + .5 * offset; float crX = fract(g); float creaseIdx = floor(g); float depthMod = .7 + .3 * fract(sin(creaseIdx * 127.1 + u_seed * 3.7) * 43758.5453); float dx = crX - .5; float foldWidth = .5; - float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * depthMod; + float parity = depthMod * (1. - 2. * mod(creaseIdx, 2.)); + float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * parity; float slope = sign(dx) * foldAmount; float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); - // .z is the signed in-cell position: 0 at the crease, negative on one side and positive on the other. - return vec4(slope, creaseDark, dx, foldAmount); + float lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), LIFT_SHARPNESS) * parity; + return vec3(slope, creaseDark, lift); } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -310,55 +335,71 @@ void main() { drops = mix(drops, 0., fade); } - // Radial folds and creases are independent layers: both may be visible at once. - // Folds go first, so the crease shift below also carries the roughness and fiber layers. + float lightAzimuth = radians(u_lightAngle); + vec2 lightDir = vec2(sin(lightAzimuth), -cos(lightAzimuth)); + float grazing = radians(90. - LIGHT_ELEVATION); + + vec2 relief = vec2(0.); + float reliefAmount = 0.; + float creaseInk = 1.; + float creaseWear = 0.; + vec2 foldFlow = vec2(0.); + if (u_folds > 0.) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); vec2 foldsUV2 = foldsUV1 + .03 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .015 + u_seed)).rg - .5); - vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); - vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); - radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); - float foldsPattern = radialFolds.x + radialFolds.y; + vec4 folds = getFolds(foldsUV1, foldsUV2); - pattern += u_folds * foldsPattern; + vec2 foldTilt = u_folds * mix(folds.xy, vec2(0.), .4 * fade); + relief += foldTilt; + reliefAmount += u_folds; + foldFlow = folds.w * foldTilt; - scaleDistortion = .22 * radialFolds.z * u_folds; + scaleDistortion = .22 * mix(clamp(5. * folds.z, 0., 1.), .5, .4 * fade) * u_folds; } if (u_creases > 0.) { - // Aspect-corrected, so crease spacing is even in screen units rather than stretched with the image. - vec2 uv = 1. + (imageUV - .5) * vec2(u_imageAspectRatio, 1.); - float count = mix(25., 1., pow(u_creaseSize, .4)); - float offset = 1. - u_creaseOffset; + vec2 uv = 1. + (imageUV - .5) * vec2(u_imageAspectRatio, 1.) + FOLDS_TO_CREASES * foldFlow; // Normal creases: run vertically (vary along x), displacing the image in y. - vec4 h = getCrease(uv.x, offset, count); - float angleX = h.x * 1.1345; - float crLightX = max(-.5 * sin(angleX) + .5 * cos(angleX), 0.) * mix(.9, 1., h.y); + vec3 h = getCrease(uv.x, 1. - u_creaseOffsetX, mix(3., .5, u_creaseSizeX)); + relief.x += u_creases * h.x; + reliefAmount += u_creases; + creaseInk *= mix(.9, 1., h.y); + creaseWear = max(creaseWear, (1. - h.y) * u_creases); drops *= mix(1., h.y, u_creases); - pattern += u_creases * crLightX; - float distortBaseX = mix(pow(h.y, .2), abs(h.z), .5); - yShift += .022 * u_creases * (1. - distortBaseX); + yShift += .022 * u_creases * h.z * abs(lightDir.x); patternUV.y += yShift; - if (u_creaseCross > 0.) { - float crossDepth = u_creases * u_creaseCross; - vec4 v = getCrease(uv.y, 1., 1.); - float angleY = v.x * 1.1345; - float crLightY = max(-.5 * sin(angleY) + .5 * cos(angleY), 0.) * mix(.9, 1., v.y); - drops *= mix(1., v.y, crossDepth); - pattern += crossDepth * crLightY; - float distortBaseY = mix(pow(v.y, .2), abs(v.z), .5); - float xFan = 2. * (imageUV.x - .5); - xDistortion -= .02 * crossDepth * (1. - distortBaseY) * xFan; - patternUV.x -= .022 * crossDepth * (1. - distortBaseY) * xFan; - } + vec3 v = getCrease(uv.y, 1. - u_creaseOffsetY, mix(3., .5, u_creaseSizeY)); + relief.y += u_creases * v.x; + reliefAmount += u_creases; + creaseInk *= mix(.9, 1., v.y); + creaseWear = max(creaseWear, (1. - v.y) * u_creases); + drops *= mix(1., v.y, u_creases); + float xFan = 2. * (imageUV.x - .5) * abs(lightDir.y); + xDistortion -= .02 * u_creases * v.z * xFan; + patternUV.x -= .022 * u_creases * v.z * xFan; } + float unlit = SHADING_SCALE * cos(grazing); + float lit = unlit; + + if (reliefAmount > 0.) { + float slope = clamp(dot(relief, lightDir), -1.2, 1.2); + lit = SHADING_SCALE * max(cos(slope + grazing), 0.); + + pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * creaseInk; + } + + patternUV += GRAIN_DRAG * foldFlow; + + float detailGain = max(0., 1. + GRAIN_BY_LIGHT * (lit - unlit) / SHADING_SCALE) * (1. + CREASE_WEAR * creaseWear); + if (u_roughness > 0.) { float roughness = getRoughness(200. * patternUV); roughness *= mix(1., .3, fade); - roughness *= u_roughness; + roughness *= u_roughness * detailGain; pattern += roughness; radialDistortion += .02 * roughness; } @@ -366,7 +407,7 @@ void main() { if (u_fiber > 0.) { float fiber = getFiber(10. * patternUV); fiber *= mix(1., .3, fade); - fiber *= u_fiber; + fiber *= u_fiber * detailGain; pattern += fiber; radialDistortion += .02 * fiber; } @@ -424,7 +465,7 @@ void main() { vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); float opacity = overlayAlpha + bgOpacity * (1. - overlayAlpha); - if (!u_background) { + if (!u_background && u_isImage) { color *= imageFootprint; opacity *= imageFootprint; } @@ -435,6 +476,7 @@ void main() { export interface PaperTextureUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; + u_isImage: boolean; u_noiseTexture?: HTMLImageElement; u_colorFront: [number, number, number, number]; u_colorBack: [number, number, number, number]; @@ -445,9 +487,11 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_folds: number; u_foldCount: number; u_creases: number; - u_creaseSize: number; - u_creaseCross: number; - u_creaseOffset: number; + u_creaseSizeX: number; + u_creaseSizeY: number; + u_creaseOffsetX: number; + u_creaseOffsetY: number; + u_lightAngle: number; u_fade: number; u_drops: number; u_seed: number; @@ -467,9 +511,11 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara folds?: number; foldCount?: number; creases?: number; - creaseSize?: number; - creaseCross?: number; - creaseOffset?: number; + creaseSizeX?: number; + creaseSizeY?: number; + creaseOffsetX?: number; + creaseOffsetY?: number; + lightAngle?: number; fade?: number; drops?: number; seed?: number; From 7a41e14996f420895e7c735f2cde7908e5b6fa57 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 17 Aug 2026 19:34:28 +0400 Subject: [PATCH 128/186] no fade --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 - docs/src/shader-defs/paper-texture-def.ts | 10 +--- .../src/shaders/paper-texture.tsx | 9 ---- packages/shaders/src/shaders/paper-texture.ts | 51 ++----------------- 4 files changed, 6 insertions(+), 65 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 3708e7b0d..5b42580ae 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -86,7 +86,6 @@ const PaperTextureWithControls = () => { lightAngle: { value: defaults.lightAngle, min: 0, max: 360, order: 240 }, drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, - fade: { value: defaults.fade, min: 0, max: 1, order: 260 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, Image: folder( diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 8f4cbde4c..ae248cbe8 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -148,14 +148,6 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.lightAngle, description: 'Direction the folded surface is lit from, clockwise from the top of the canvas, in degrees', }, - { - name: 'fade', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.fade, - description: 'Big-scale noise mask applied to the pattern', - }, { name: 'drops', type: 'number', @@ -170,7 +162,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1000, defaultValue: defaultParams.seed, - description: 'Seed applied to folds, drops and fade', + description: 'Seed applied to folds and drops', }, ...staticImageCommonParams, ], diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index cc900b361..1c7ccb71b 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -17,8 +17,6 @@ export interface PaperTextureProps extends ShaderComponentProps, PaperTexturePar fiberScale?: number; /** @deprecated use `foldCount` instead */ foldsNumber?: number; - /** @deprecated use `fade` instead */ - blur?: number; } type PaperTexturePreset = ImageShaderPreset; @@ -47,7 +45,6 @@ export const defaultPreset: PaperTexturePreset = { creaseOffsetX: 0, creaseOffsetY: 0, lightAngle: 90, - fade: 0, drops: 0, seed: 354, background: true, @@ -78,7 +75,6 @@ export const cardboardPreset: PaperTexturePreset = { creaseOffsetX: 0, creaseOffsetY: 0, lightAngle: 90, - fade: 0, drops: 0.2, seed: 1.6, background: true, @@ -107,7 +103,6 @@ export const spreadPreset: PaperTexturePreset = { creaseOffsetY: 0, lightAngle: 90, drops: 0, - fade: 0.68, scale: 0.95, fit: 'contain', speed: 1, @@ -140,7 +135,6 @@ export const coloredPreset: PaperTexturePreset = { creaseOffsetX: 0, creaseOffsetY: 0, lightAngle: 90, - fade: 1, drops: 1, seed: 784, background: true, @@ -180,8 +174,6 @@ export const PaperTexture: React.FC = memo(function PaperText // Reworked props fiberScale, fiberSize = fiberScale === undefined ? defaultPreset.params.fiberSize : 0.2 / fiberScale, - blur, - fade = blur === undefined ? defaultPreset.params.fade : blur, foldsNumber, foldCount = foldsNumber === undefined ? defaultPreset.params.foldCount : foldsNumber, @@ -217,7 +209,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_lightAngle: lightAngle, u_folds: folds, u_creases: creases, - u_fade: fade, u_drops: drops, u_seed: seed, u_blending: blending, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 20b481144..0d2a536c2 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -29,8 +29,7 @@ export const paperTextureMeta = { * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (-0.5 to 0.5) * - u_lightAngle (float): Direction the folded surface is lit from, clockwise from the top of the canvas, in degrees (0 to 360) * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) - * - u_seed (float): Seed applied to folds, drops and fade (0 to 1000) - * - u_fade (float): Large-scale noise mask applied to the pattern (0 to 1) + * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through, needs image (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals, needs image (-1 to 1) * - u_background (bool): Shows or hides the paper texture outside the image frame, needs image @@ -77,7 +76,6 @@ uniform float u_creaseOffsetY; uniform float u_lightAngle; uniform float u_drops; uniform float u_seed; -uniform float u_fade; uniform float u_blending; uniform float u_distortion; uniform float u_roughnessSize; @@ -92,7 +90,7 @@ out vec4 fragColor; #define CELL_EDGE .4 #define GRAIN_BY_LIGHT .5 #define CREASE_WEAR .6 -#define LIGHT_ELEVATION 60. +#define LIGHT_ELEVATION 45. #define LIFT_SHARPNESS 4. #define GRAIN_DRAG .2 #define SHADING_SCALE 1. @@ -108,37 +106,9 @@ float getUvFrame(vec2 uv, float blur) { float lst(float edge0, float edge1, float x) { return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } -float sst(float edge0, float edge1, float x) { - return smoothstep(edge0, edge1, x); -} ${declarePI} ${rotation2} -float randomR(vec2 p) { - vec2 uv = floor(p) / 100. + .5; - return texture(u_noiseTexture, fract(uv)).r; -} -float valueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = randomR(i); - float b = randomR(i + vec2(1.0, 0.0)); - float c = randomR(i + vec2(0.0, 1.0)); - float d = randomR(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} -float getFadeMask(vec2 n) { - float total = 0.0, amplitude = .4; - for (int i = 0; i < 2; i++) { - total += valueNoise(n) * amplitude; - n *= 1.99; - amplitude *= 0.65; - } - return total; -} float getRoughness(vec2 p) { vec2 u = p / vec2(2, 4); @@ -326,13 +296,9 @@ void main() { float yShift = 0.; float scaleDistortion = 0.; - float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); - fade = clamp(8. * fade * fade * fade, 0., 1.); - float drops = 0.; if (u_drops > 0.) { drops = getDrops(patternUV * 2.); - drops = mix(drops, 0., fade); } float lightAzimuth = radians(u_lightAngle); @@ -350,12 +316,12 @@ void main() { vec2 foldsUV2 = foldsUV1 + .03 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .015 + u_seed)).rg - .5); vec4 folds = getFolds(foldsUV1, foldsUV2); - vec2 foldTilt = u_folds * mix(folds.xy, vec2(0.), .4 * fade); + vec2 foldTilt = u_folds * folds.xy; relief += foldTilt; reliefAmount += u_folds; foldFlow = folds.w * foldTilt; - scaleDistortion = .22 * mix(clamp(5. * folds.z, 0., 1.), .5, .4 * fade) * u_folds; + scaleDistortion = .22 * clamp(5. * folds.z, 0., 1.) * u_folds; } if (u_creases > 0.) { @@ -398,7 +364,6 @@ void main() { if (u_roughness > 0.) { float roughness = getRoughness(200. * patternUV); - roughness *= mix(1., .3, fade); roughness *= u_roughness * detailGain; pattern += roughness; radialDistortion += .02 * roughness; @@ -406,7 +371,6 @@ void main() { if (u_fiber > 0.) { float fiber = getFiber(10. * patternUV); - fiber *= mix(1., .3, fade); fiber *= u_fiber * detailGain; pattern += fiber; radialDistortion += .02 * fiber; @@ -431,12 +395,9 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness); - float frame = getUvFrame(imageUV, frameSoftness); + float frame = getUvFrame(imageUV, .005); vec4 image = texture(u_image, imageUV); - pattern = clamp(.75 * pattern, 0., 1.); - float patternAlpha = fgOpacity * pattern; float maxC = max(max(image.r, image.g), image.b); @@ -492,7 +453,6 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_creaseOffsetX: number; u_creaseOffsetY: number; u_lightAngle: number; - u_fade: number; u_drops: number; u_seed: number; u_blending: number; @@ -516,7 +476,6 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara creaseOffsetX?: number; creaseOffsetY?: number; lightAngle?: number; - fade?: number; drops?: number; seed?: number; blending?: number; From 7fce1fa80f643811da4fdf232bc3c822377f6c7f Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 17 Aug 2026 20:56:00 +0400 Subject: [PATCH 129/186] - --- .../src/shaders/paper-texture.tsx | 6 +- packages/shaders/src/shaders/paper-texture.ts | 70 ++++++++++++------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 1c7ccb71b..88636ad08 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -32,7 +32,7 @@ export const defaultPreset: PaperTexturePreset = { speed: 0, frame: 0, colorFront: '#b3b3b3', - colorBack: '#ffffff', + colorBack: '#ebebeb', roughness: 0, roughnessSize: 0.25, fiber: 0.5, @@ -44,9 +44,9 @@ export const defaultPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 90, + lightAngle: 26, drops: 0, - seed: 354, + seed: 0, background: true, }, }; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0d2a536c2..779336c34 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { rotation2, declarePI } from '../shader-utils.js'; +import { rotation2, declarePI, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { maxFoldCount: 20, @@ -87,6 +87,11 @@ out vec4 fragColor; #define FACET_CURVE 2. #define FOLDS_TO_CREASES .02 +#define FOLD_LINE_GAP .02 +#define FOLD_CROWDING 7. +#define FOLD_AXES 6. +#define RADIAL_FALLOFF .25 +#define TILT_FLOOR .5 #define CELL_EDGE .4 #define GRAIN_BY_LIGHT .5 #define CREASE_WEAR .6 @@ -108,6 +113,7 @@ float lst(float edge0, float edge1, float x) { } ${declarePI} +${proceduralHash22} ${rotation2} float getRoughness(vec2 p) { @@ -128,13 +134,13 @@ float getRoughness(vec2 p) { float norm = 0., amp = .5; for (int i = 0; i < 4; i++) { float absIdx = baseLevel + float(i); - float freq = pow(2.1, absIdx); + float freq = exp2(absIdx * logLac); vec2 qx = size * px * freq; float qy = size * py * freq; float wi = 1.; if (i == 0) wi = 1. - fade; - if (i == 4) wi = fade; + if (i == 3) wi = fade; vec2 fx = fract(qx); float fy = fract(qy); @@ -170,7 +176,7 @@ float getFiber(vec2 p) { float amp = 1.; for (int i = 0; i < 5; i++) { float absIdx = baseLevel + float(i); - float freq = pow(1.7, absIdx); + float freq = exp2(absIdx * 0.76553); vec2 q = size * p * freq; float an = absIdx * .8; @@ -201,6 +207,15 @@ float getFiber(vec2 p) { return .5 * length(grad) - .5; } +vec2 smoothNoise(vec2 p) { + vec2 sz = vec2(textureSize(u_noiseTexture, 0)); + vec2 t = p * sz - .5; + vec2 i = floor(t); + vec2 f = fract(t); + f = f * f * (3. - 2. * f); + return texture(u_noiseTexture, fract((i + f + .5) / sz)).rg; +} + vec2 randomGB(vec2 p) { vec2 uv = floor(p) / 50. + .5; return texture(u_noiseTexture, fract(uv)).gb; @@ -223,42 +238,47 @@ float getDrops(vec2 uv) { return 1. - lst(.05, .09, sqrt(dropsMinDist)); } -vec2 getCellTilt(float idx) { - vec2 rand = randomGB(vec2(idx + 31., idx * u_seed + 17.)); - float an = rand.x * TWO_PI; - return vec2(cos(an), sin(an)) * rand.y; +vec2 getCellTilt(float idx, float radius) { + vec2 rand = hash22(vec2(idx + 31., idx * u_seed + 17.)); + float an = floor(rand.x * FOLD_AXES) / FOLD_AXES * TWO_PI; + return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); } vec4 getFolds(vec2 uv1, vec2 uv2) { - float l1 = 9., l2 = 9., l2b = 9.; + float near1 = 9., near2 = 9., near2b = 9.; float idx1 = 0., idx2 = 0.; vec2 p1 = vec2(0.), p2 = vec2(0.); for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { if (float(i) >= floor(u_foldCount + .5)) break; - vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); + vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; - vec2 p = vec2(cos(an), sin(an)) * rand.y; - float dist1 = distance(uv1, p); - if (dist1 < l1) { - l1 = dist1; + vec2 p = vec2(cos(an), sin(an)) * pow(rand.y, FOLD_CROWDING); + + vec2 d1 = uv1 - p; + float dsq1 = dot(d1, d1); + if (dsq1 < near1) { + near1 = dsq1; idx1 = float(i); p1 = p; } - float dist2 = distance(uv2, p); - if (dist2 < l2) { - l2b = l2; - l2 = dist2; + + vec2 d2 = uv2 - p; + float dsq2 = dot(d2, d2); + if (dsq2 < near2) { + near2b = near2; + near2 = dsq2; idx2 = float(i); p2 = p; - } else if (dist2 < l2b) { - l2b = dist2; + } else if (dsq2 < near2b) { + near2b = dsq2; } } + float l = sqrt(near2), lb = sqrt(near2b); - vec2 tilt1 = getCellTilt(idx1) + FACET_CURVE * (uv1 - p1); - vec2 tilt2 = getCellTilt(idx2) + FACET_CURVE * (uv2 - p2); + vec2 tilt1 = getCellTilt(idx1, length(p1)) + FACET_CURVE * (uv1 - p1); + vec2 tilt2 = getCellTilt(idx2, length(p2)) + FACET_CURVE * (uv2 - p2); - return vec4(.5 * (tilt1 + tilt2), .2 * l2, smoothstep(0., CELL_EDGE, l2b - l2)); + return vec4(.5 * (tilt1 + tilt2), .2 * l, smoothstep(0., CELL_EDGE, lb - l)); } vec3 getCrease(float coord, float offset, float count) { @@ -313,7 +333,7 @@ void main() { if (u_folds > 0.) { vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .03 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .015 + u_seed)).rg - .5); + vec2 foldsUV2 = foldsUV1 + FOLD_LINE_GAP * sin(2. * u_seed) * (smoothNoise(patternUV * .015 + u_seed) - .5); vec4 folds = getFolds(foldsUV1, foldsUV2); vec2 foldTilt = u_folds * folds.xy; @@ -398,7 +418,7 @@ void main() { float frame = getUvFrame(imageUV, .005); vec4 image = texture(u_image, imageUV); - float patternAlpha = fgOpacity * pattern; + float patternAlpha = clamp(fgOpacity * pattern, 0., 1.); float maxC = max(max(image.r, image.g), image.b); float minC = min(min(image.r, image.g), image.b); From 50b05f6c1558a6bdfa9848744590b229b8953300 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 17 Aug 2026 21:09:43 +0400 Subject: [PATCH 130/186] - --- .../src/shaders/paper-texture.tsx | 7 +- packages/shaders/src/shaders/paper-texture.ts | 67 ++++++++++--------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 88636ad08..c003fd011 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -32,7 +32,7 @@ export const defaultPreset: PaperTexturePreset = { speed: 0, frame: 0, colorFront: '#b3b3b3', - colorBack: '#ebebeb', + colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, fiber: 0.5, @@ -191,6 +191,8 @@ export const PaperTexture: React.FC = memo(function PaperText }: PaperTextureProps) { const noiseTexture = typeof window !== 'undefined' && { u_noiseTexture: getShaderNoiseTexture() }; + const lightRadians = (lightAngle * Math.PI) / 180; + const uniforms = { // Own uniforms u_image: image, @@ -206,7 +208,8 @@ export const PaperTexture: React.FC = memo(function PaperText u_creaseSizeY: creaseSizeY, u_creaseOffsetX: creaseOffsetX, u_creaseOffsetY: creaseOffsetY, - u_lightAngle: lightAngle, + u_lightDir: [Math.sin(lightRadians), -Math.cos(lightRadians)], + u_foldTrig: [Math.cos(4 * seed), Math.sin(4 * seed), Math.sin(2 * seed)], u_folds: folds, u_creases: creases, u_drops: drops, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 779336c34..a5589cd8e 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { rotation2, declarePI, proceduralHash22 } from '../shader-utils.js'; +import { declarePI, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { maxFoldCount: 20, @@ -27,7 +27,8 @@ export const paperTextureMeta = { * - u_creaseSizeY (float): Size of the horizontal creases running across them, needs u_creases > 0 (0 to 1) * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (-0.5 to 0.5) * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (-0.5 to 0.5) - * - u_lightAngle (float): Direction the folded surface is lit from, clockwise from the top of the canvas, in degrees (0 to 360) + * - u_lightDir (vec2): Direction the folded surface is lit from, as a unit vector (derived from the lightAngle prop) + * - u_foldTrig (vec3): Seed-derived constants for the crumple: cos and sin of its frame rotation, and the amplitude of the second fold line * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through, needs image (0 to 1) @@ -73,7 +74,8 @@ uniform float u_creaseSizeX; uniform float u_creaseSizeY; uniform float u_creaseOffsetX; uniform float u_creaseOffsetY; -uniform float u_lightAngle; +uniform vec2 u_lightDir; +uniform vec3 u_foldTrig; uniform float u_drops; uniform float u_seed; uniform float u_blending; @@ -114,7 +116,6 @@ float lst(float edge0, float edge1, float x) { ${declarePI} ${proceduralHash22} -${rotation2} float getRoughness(vec2 p) { vec2 u = p / vec2(2, 4); @@ -321,8 +322,6 @@ void main() { drops = getDrops(patternUV * 2.); } - float lightAzimuth = radians(u_lightAngle); - vec2 lightDir = vec2(sin(lightAzimuth), -cos(lightAzimuth)); float grazing = radians(90. - LIGHT_ELEVATION); vec2 relief = vec2(0.); @@ -332,8 +331,9 @@ void main() { vec2 foldFlow = vec2(0.); if (u_folds > 0.) { - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + FOLD_LINE_GAP * sin(2. * u_seed) * (smoothNoise(patternUV * .015 + u_seed) - .5); + vec2 foldQ = patternUV * .18; + vec2 foldsUV1 = vec2(u_foldTrig.x * foldQ.x - u_foldTrig.y * foldQ.y, u_foldTrig.y * foldQ.x + u_foldTrig.x * foldQ.y); + vec2 foldsUV2 = foldsUV1 + FOLD_LINE_GAP * u_foldTrig.z * (smoothNoise(patternUV * .015 + u_seed) - .5); vec4 folds = getFolds(foldsUV1, foldsUV2); vec2 foldTilt = u_folds * folds.xy; @@ -345,7 +345,7 @@ void main() { } if (u_creases > 0.) { - vec2 uv = 1. + (imageUV - .5) * vec2(u_imageAspectRatio, 1.) + FOLDS_TO_CREASES * foldFlow; + vec2 uv = 1. + patternUV * .2 + FOLDS_TO_CREASES * foldFlow; // Normal creases: run vertically (vary along x), displacing the image in y. vec3 h = getCrease(uv.x, 1. - u_creaseOffsetX, mix(3., .5, u_creaseSizeX)); @@ -354,7 +354,7 @@ void main() { creaseInk *= mix(.9, 1., h.y); creaseWear = max(creaseWear, (1. - h.y) * u_creases); drops *= mix(1., h.y, u_creases); - yShift += .022 * u_creases * h.z * abs(lightDir.x); + yShift += .022 * u_creases * h.z * abs(u_lightDir.x); patternUV.y += yShift; vec3 v = getCrease(uv.y, 1. - u_creaseOffsetY, mix(3., .5, u_creaseSizeY)); @@ -363,7 +363,7 @@ void main() { creaseInk *= mix(.9, 1., v.y); creaseWear = max(creaseWear, (1. - v.y) * u_creases); drops *= mix(1., v.y, u_creases); - float xFan = 2. * (imageUV.x - .5) * abs(lightDir.y); + float xFan = 2. * (imageUV.x - .5) * abs(u_lightDir.y); xDistortion -= .02 * u_creases * v.z * xFan; patternUV.x -= .022 * u_creases * v.z * xFan; } @@ -372,7 +372,7 @@ void main() { float lit = unlit; if (reliefAmount > 0.) { - float slope = clamp(dot(relief, lightDir), -1.2, 1.2); + float slope = clamp(dot(relief, u_lightDir), -1.2, 1.2); lit = SHADING_SCALE * max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * creaseInk; @@ -415,29 +415,35 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - float frame = getUvFrame(imageUV, .005); - vec4 image = texture(u_image, imageUV); - float patternAlpha = clamp(fgOpacity * pattern, 0., 1.); - float maxC = max(max(image.r, image.g), image.b); - float minC = min(min(image.r, image.g), image.b); - float sat = maxC > 0. ? (maxC - minC) / maxC : 0.; - float midC = image.r + image.g + image.b - maxC - minC; - float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; - float satDampen = sat * (1. - .5 * secondaryness); + vec3 pic = vec3(0.); + float imageFootprint = 0.; + + if (u_isImage) { + float frame = getUvFrame(imageUV, .005); + vec4 image = texture(u_image, imageUV); + + float maxC = max(max(image.r, image.g), image.b); + float minC = min(min(image.r, image.g), image.b); + float sat = maxC > 0. ? (maxC - minC) / maxC : 0.; + float midC = image.r + image.g + image.b - maxC - minC; + float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; + float satDampen = sat * (1. - .5 * secondaryness); - float lum = dot(vec3(.2126, .7152, .0722), image.rgb); + float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - vec3 ink = mix(vec3(1.), fgColor, patternAlpha); - vec3 pic = blendMultiply(image.rgb, ink, u_blending); + vec3 ink = mix(vec3(1.), fgColor, patternAlpha); + pic = blendMultiply(image.rgb, ink, u_blending); - float darkDampen = 1. - lum; - float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); - pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); - pic = clamp(pic, 0., 1.); + float darkDampen = 1. - lum; + float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); + pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); + pic = clamp(pic, 0., 1.); + + imageFootprint = frame * image.a; + } - float imageFootprint = frame * image.a; float imageAlpha = imageFootprint; vec3 overlay = pic * imageAlpha + fgColor * patternAlpha * (1. - imageAlpha); @@ -472,7 +478,8 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_creaseSizeY: number; u_creaseOffsetX: number; u_creaseOffsetY: number; - u_lightAngle: number; + u_lightDir: [number, number]; + u_foldTrig: [number, number, number]; u_drops: number; u_seed: number; u_blending: number; From bfb664051290675dad6eb5770c3021eb50845b91 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 17 Aug 2026 22:32:58 +0400 Subject: [PATCH 131/186] - --- docs/src/app/(shaders)/paper-texture/page.tsx | 39 +++--- docs/src/shader-defs/paper-texture-def.ts | 8 +- .../src/shaders/paper-texture.tsx | 114 +++++++++--------- packages/shaders/src/shaders/paper-texture.ts | 107 +++++++--------- 4 files changed, 127 insertions(+), 141 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 5b42580ae..29a366196 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -69,25 +69,26 @@ const PaperTextureWithControls = () => { return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorFront: { value: toHsla(defaults.colorFront), order: 101 }, - blending: { value: defaults.blending, min: 0, max: 1, order: 150 }, - distortion: { value: defaults.distortion, min: -1, max: 1, order: 151 }, - background: { value: defaults.background, order: 152 }, - roughness: { value: defaults.roughness, min: 0, max: 1, order: 201 }, - roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 202 }, - fiber: { value: defaults.fiber, min: 0, max: 1, order: 210 }, - fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 211 }, - folds: { value: defaults.folds, min: 0, max: 1, order: 230 }, - foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 231 }, - creases: { value: defaults.creases, min: 0, max: 1, order: 235 }, - creaseSizeX: { value: defaults.creaseSizeX, min: 0, max: 1, order: 236 }, - creaseSizeY: { value: defaults.creaseSizeY, min: 0, max: 1, order: 237 }, - creaseOffsetX: { value: defaults.creaseOffsetX, min: -0.5, max: 0.5, order: 238 }, - creaseOffsetY: { value: defaults.creaseOffsetY, min: -0.5, max: 0.5, order: 239 }, - lightAngle: { value: defaults.lightAngle, min: 0, max: 360, order: 240 }, - drops: { value: defaults.drops, min: 0, max: 1, order: 250 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, - scale: { value: defaults.scale, min: 0.5, max: 10, order: 300 }, - fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, + seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, + lightAngle: { value: defaults.lightAngle, min: 0, max: 360, order: 201 }, + roughness: { value: defaults.roughness, min: 0, max: 1, order: 300 }, + roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 301 }, + fiber: { value: defaults.fiber, min: 0, max: 1, order: 310 }, + fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 311 }, + folds: { value: defaults.folds, min: 0, max: 1, order: 320 }, + foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 321 }, + creases: { value: defaults.creases, min: 0, max: 1, order: 330 }, + creaseSizeX: { value: defaults.creaseSizeX, min: 0, max: 1, order: 331 }, + creaseOffsetX: { value: defaults.creaseOffsetX, min: 0, max: 1, order: 332 }, + creaseSizeY: { value: defaults.creaseSizeY, min: 0, max: 1, order: 333 }, + creaseOffsetY: { value: defaults.creaseOffsetY, min: 0, max: 1, order: 334 }, + drops: { value: defaults.drops, min: 0, max: 1, order: 340 }, + blending: { value: defaults.blending, min: 0, max: 1, order: 350 }, + distortion: { value: defaults.distortion, min: -1, max: 1, order: 351 }, + background: { value: defaults.background, order: 352 }, + + scale: { value: defaults.scale, min: 0.5, max: 10, order: 400 }, + fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 401 }, Image: folder( { 'Upload image': levaImageButton(setImageWithoutStatus), diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index ae248cbe8..b1ad5e518 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -127,16 +127,16 @@ export const paperTextureDef: ShaderDef = { { name: 'creaseOffsetX', type: 'number', - min: -0.5, - max: 0.5, + min: 0, + max: 1, defaultValue: defaultParams.creaseOffsetX, description: 'Shifts the vertical creases across the surface (needs creases > 0)', }, { name: 'creaseOffsetY', type: 'number', - min: -0.5, - max: 0.5, + min: 0, + max: 1, defaultValue: defaultParams.creaseOffsetY, description: 'Shifts the horizontal creases across the surface (needs creases > 0)', }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index c003fd011..d820bf52d 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -28,25 +28,25 @@ export const defaultPreset: PaperTexturePreset = { blending: 1, distortion: 0.5, fit: 'contain', - scale: 0.8, + scale: 0.9, speed: 0, frame: 0, - colorFront: '#b3b3b3', + colorFront: '#a3a6a8', colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, fiber: 0.5, fiberSize: 0.5, - folds: 0.7, - foldCount: 15, + folds: 0.5, + foldCount: 12, creases: 0, - creaseSizeX: 0.32, + creaseSizeX: 0.6, creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 26, - drops: 0, - seed: 0, + lightAngle: 300, + drops: 0.25, + seed: 613, background: true, }, }; @@ -55,28 +55,28 @@ export const cardboardPreset: PaperTexturePreset = { name: 'Cardboard', params: { ...defaultObjectSizing, - blending: 0.7, + blending: 1, distortion: 0.5, fit: 'contain', - speed: 1, + scale: 0.9, + speed: 0, frame: 0, - scale: 0.8, - colorBack: '#c3ac79', - colorFront: '#111111', - roughness: 0.1, - roughnessSize: 0.65, - fiber: 0.35, - fiberSize: 0.14, + colorFront: '#878f9b', + colorBack: '#ffffff', + roughness: 0.16, + roughnessSize: 0.25, + fiber: 0.32, + fiberSize: 0.5, folds: 0, - foldCount: 18, - creases: 0.25, - creaseSizeX: 0, + foldCount: 12, + creases: 0.55, + creaseSizeX: 0.6, creaseSizeY: 0.89, - creaseOffsetX: 0, - creaseOffsetY: 0, - lightAngle: 90, - drops: 0.2, - seed: 1.6, + creaseOffsetX: 0.59, + creaseOffsetY: 1, + lightAngle: 140, + drops: 0, + seed: 613, background: true, }, }; @@ -85,28 +85,28 @@ export const spreadPreset: PaperTexturePreset = { name: 'Spread', params: { ...defaultObjectSizing, + blending: 1, + distortion: 0.38, + fit: 'contain', + scale: 0.9, + speed: 0, + frame: 0, + colorFront: '#c2c2c2', colorBack: '#ffffff', - colorFront: '#9d9e9f', - blending: 0.6, - distortion: -0.8, - seed: 448, - roughness: 0.3, + roughness: 0.16, roughnessSize: 0.25, - fiber: 0, - fiberSize: 0.5, + fiber: 0.62, + fiberSize: 0.58, folds: 0, - foldCount: 1, - creases: 0.7, - creaseSizeX: 0.67, + foldCount: 12, + creases: 0.55, + creaseSizeX: 0, creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 90, + lightAngle: 88, drops: 0, - scale: 0.95, - fit: 'contain', - speed: 1, - frame: 0, + seed: 613, background: false, }, }; @@ -115,28 +115,28 @@ export const coloredPreset: PaperTexturePreset = { name: 'Colored', params: { ...defaultObjectSizing, - blending: 0.8, + blending: 1, distortion: 0.5, fit: 'contain', - scale: 0.8, + scale: 0.9, speed: 0, frame: 0, - colorFront: '#7155b9', - colorBack: '#ffe0e0', - roughness: 0, - roughnessSize: 0.25, - fiber: 0.5, - fiberSize: 0.5, - folds: 1, - foldCount: 10, - creases: 0, - creaseSizeX: 0.32, + colorFront: '#ffcccc', + colorBack: '#3b2942', + roughness: 0.25, + roughnessSize: 0.08, + fiber: 0.52, + fiberSize: 1, + folds: 0, + foldCount: 12, + creases: 0.43, + creaseSizeX: 1, creaseSizeY: 0.89, - creaseOffsetX: 0, - creaseOffsetY: 0, - lightAngle: 90, + creaseOffsetX: 0.62, + creaseOffsetY: 0.2, + lightAngle: 316, drops: 1, - seed: 784, + seed: 476, background: true, }, }; @@ -209,7 +209,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_creaseOffsetX: creaseOffsetX, u_creaseOffsetY: creaseOffsetY, u_lightDir: [Math.sin(lightRadians), -Math.cos(lightRadians)], - u_foldTrig: [Math.cos(4 * seed), Math.sin(4 * seed), Math.sin(2 * seed)], + u_foldTrig: [Math.cos(4 * seed), Math.sin(4 * seed)], u_folds: folds, u_creases: creases, u_drops: drops, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index a5589cd8e..b48c077e4 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -25,10 +25,10 @@ export const paperTextureMeta = { * - u_creases (float): Depth of the creases, an independent layer over the radial folds (0 to 1) * - u_creaseSizeX (float): Size of the vertical creases, needs u_creases > 0 (0 to 1) * - u_creaseSizeY (float): Size of the horizontal creases running across them, needs u_creases > 0 (0 to 1) - * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (-0.5 to 0.5) - * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (-0.5 to 0.5) + * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (0 to 1) + * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (0 to 1) * - u_lightDir (vec2): Direction the folded surface is lit from, as a unit vector (derived from the lightAngle prop) - * - u_foldTrig (vec3): Seed-derived constants for the crumple: cos and sin of its frame rotation, and the amplitude of the second fold line + * - u_foldTrig (vec2): Cos and sin of the crumple frame rotation, derived from the seed * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through, needs image (0 to 1) @@ -75,7 +75,7 @@ uniform float u_creaseSizeY; uniform float u_creaseOffsetX; uniform float u_creaseOffsetY; uniform vec2 u_lightDir; -uniform vec3 u_foldTrig; +uniform vec2 u_foldTrig; uniform float u_drops; uniform float u_seed; uniform float u_blending; @@ -87,14 +87,7 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; -#define FACET_CURVE 2. #define FOLDS_TO_CREASES .02 -#define FOLD_LINE_GAP .02 -#define FOLD_CROWDING 7. -#define FOLD_AXES 6. -#define RADIAL_FALLOFF .25 -#define TILT_FLOOR .5 -#define CELL_EDGE .4 #define GRAIN_BY_LIGHT .5 #define CREASE_WEAR .6 #define LIGHT_ELEVATION 45. @@ -232,17 +225,16 @@ float getDrops(vec2 uv) { vec2 offset = randomGB(iDropsUV + neighbor); offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); vec2 pos = neighbor + offset - fDropsUV; - float dist = length(pos); - dropsMinDist = min(dropsMinDist, dropsMinDist*dist); + dropsMinDist *= min(1., dot(pos, pos)); } } - return 1. - lst(.05, .09, sqrt(dropsMinDist)); + return 1. - lst(.05, .09, sqrt(sqrt(dropsMinDist))); } vec2 getCellTilt(float idx, float radius) { vec2 rand = hash22(vec2(idx + 31., idx * u_seed + 17.)); - float an = floor(rand.x * FOLD_AXES) / FOLD_AXES * TWO_PI; - return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); + float an = floor(rand.x * 24.) / 24. * TWO_PI; + return vec2(cos(an), sin(an)) * mix(.5, 1., rand.y) * mix(.45, 1., radius); } vec4 getFolds(vec2 uv1, vec2 uv2) { @@ -253,18 +245,17 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; - vec2 p = vec2(cos(an), sin(an)) * pow(rand.y, FOLD_CROWDING); + vec2 p = vec2(cos(an), sin(an)) * rand.y * rand.y; - vec2 d1 = uv1 - p; - float dsq1 = dot(d1, d1); + vec4 d = vec4(uv1, uv2) - p.xyxy; + float dsq1 = dot(d.xy, d.xy); if (dsq1 < near1) { near1 = dsq1; idx1 = float(i); p1 = p; } - vec2 d2 = uv2 - p; - float dsq2 = dot(d2, d2); + float dsq2 = dot(d.zw, d.zw); if (dsq2 < near2) { near2b = near2; near2 = dsq2; @@ -276,25 +267,21 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { } float l = sqrt(near2), lb = sqrt(near2b); - vec2 tilt1 = getCellTilt(idx1, length(p1)) + FACET_CURVE * (uv1 - p1); - vec2 tilt2 = getCellTilt(idx2, length(p2)) + FACET_CURVE * (uv2 - p2); + vec2 tilt1 = getCellTilt(idx1, length(p1)) + 1.1 * (uv1 - p1); + vec2 tilt2 = getCellTilt(idx2, length(p2)) + 1.1 * (uv2 - p2); - return vec4(.5 * (tilt1 + tilt2), .2 * l, smoothstep(0., CELL_EDGE, lb - l)); + return vec4(.5 * (tilt1 + tilt2), .2 * l, lst(0., .5, lb - l)); } -vec3 getCrease(float coord, float offset, float count) { - float g = (coord - 1.) * count + .5 * offset; - float crX = fract(g); - float creaseIdx = floor(g); - float depthMod = .7 + .3 * fract(sin(creaseIdx * 127.1 + u_seed * 3.7) * 43758.5453); - float dx = crX - .5; +void getCreases(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { + vec2 g = (coord - 1.) * count + .5 * offset; + vec2 creaseIdx = floor(g); + vec2 dx = fract(g) - .5; float foldWidth = .5; - float parity = depthMod * (1. - 2. * mod(creaseIdx, 2.)); - float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * parity; - float slope = sign(dx) * foldAmount; - float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); - float lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), LIFT_SHARPNESS) * parity; - return vec3(slope, creaseDark, lift); + vec2 parity = (1. - 2. * mod(creaseIdx, 2.)); + slope = sign(dx) * (1. - smoothstep(0., foldWidth, abs(dx))) * parity; + dark = smoothstep(0., foldWidth * .5, abs(dx)); + lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(LIFT_SHARPNESS)) * parity; } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -322,8 +309,7 @@ void main() { drops = getDrops(patternUV * 2.); } - float grazing = radians(90. - LIGHT_ELEVATION); - + float grazing = 0.7; vec2 relief = vec2(0.); float reliefAmount = 0.; float creaseInk = 1.; @@ -333,7 +319,7 @@ void main() { if (u_folds > 0.) { vec2 foldQ = patternUV * .18; vec2 foldsUV1 = vec2(u_foldTrig.x * foldQ.x - u_foldTrig.y * foldQ.y, u_foldTrig.y * foldQ.x + u_foldTrig.x * foldQ.y); - vec2 foldsUV2 = foldsUV1 + FOLD_LINE_GAP * u_foldTrig.z * (smoothNoise(patternUV * .015 + u_seed) - .5); + vec2 foldsUV2 = foldsUV1 + .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); vec4 folds = getFolds(foldsUV1, foldsUV2); vec2 foldTilt = u_folds * folds.xy; @@ -345,27 +331,27 @@ void main() { } if (u_creases > 0.) { - vec2 uv = 1. + patternUV * .2 + FOLDS_TO_CREASES * foldFlow; - - // Normal creases: run vertically (vary along x), displacing the image in y. - vec3 h = getCrease(uv.x, 1. - u_creaseOffsetX, mix(3., .5, u_creaseSizeX)); - relief.x += u_creases * h.x; - reliefAmount += u_creases; - creaseInk *= mix(.9, 1., h.y); - creaseWear = max(creaseWear, (1. - h.y) * u_creases); - drops *= mix(1., h.y, u_creases); - yShift += .022 * u_creases * h.z * abs(u_lightDir.x); + vec2 uv = 1. + patternUV * .2 + .03 * foldFlow; + + // x drives the creases running vertically, y the ones running across them. + vec2 slope, dark, lift; + getCreases(uv, vec2(1. - 2. * u_creaseOffsetX, 1. - 2. * u_creaseOffsetY), + vec2(mix(3., .5, u_creaseSizeX), mix(3., .5, u_creaseSizeY)), slope, dark, lift); + + relief += u_creases * slope; + reliefAmount += 1. * u_creases; + vec2 ink = mix(vec2(.9), vec2(1.), dark); + creaseInk *= ink.x * ink.y; + creaseWear = max(creaseWear, max(1. - dark.x, 1. - dark.y) * u_creases); + vec2 dropMask = mix(vec2(1.), dark, u_creases); + drops *= dropMask.x * dropMask.y; + + yShift += .022 * u_creases * lift.x * abs(u_lightDir.x); patternUV.y += yShift; - vec3 v = getCrease(uv.y, 1. - u_creaseOffsetY, mix(3., .5, u_creaseSizeY)); - relief.y += u_creases * v.x; - reliefAmount += u_creases; - creaseInk *= mix(.9, 1., v.y); - creaseWear = max(creaseWear, (1. - v.y) * u_creases); - drops *= mix(1., v.y, u_creases); float xFan = 2. * (imageUV.x - .5) * abs(u_lightDir.y); - xDistortion -= .02 * u_creases * v.z * xFan; - patternUV.x -= .022 * u_creases * v.z * xFan; + xDistortion -= .02 * u_creases * lift.y * xFan; + patternUV.x -= .022 * u_creases * lift.y * xFan; } float unlit = SHADING_SCALE * cos(grazing); @@ -380,7 +366,7 @@ void main() { patternUV += GRAIN_DRAG * foldFlow; - float detailGain = max(0., 1. + GRAIN_BY_LIGHT * (lit - unlit) / SHADING_SCALE) * (1. + CREASE_WEAR * creaseWear); + float detailGain = (1. + CREASE_WEAR * creaseWear); if (u_roughness > 0.) { float roughness = getRoughness(200. * patternUV); @@ -409,8 +395,7 @@ void main() { float fgOpacity = u_colorFront.a; imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); - imageUV.x += u_distortion * xDistortion; - imageUV.y -= u_distortion * yShift; + imageUV += u_distortion * vec2(xDistortion, -yShift); vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); @@ -479,7 +464,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_creaseOffsetX: number; u_creaseOffsetY: number; u_lightDir: [number, number]; - u_foldTrig: [number, number, number]; + u_foldTrig: [number, number]; u_drops: number; u_seed: number; u_blending: number; From 46600dc498183487f91efa0b46e461aeccdd000d Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 17 Aug 2026 22:33:35 +0400 Subject: [PATCH 132/186] presets --- packages/shaders-react/src/shaders/paper-texture.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index d820bf52d..2aac9c25f 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -51,8 +51,8 @@ export const defaultPreset: PaperTexturePreset = { }, }; -export const cardboardPreset: PaperTexturePreset = { - name: 'Cardboard', +export const gridPreset: PaperTexturePreset = { + name: 'Grid', params: { ...defaultObjectSizing, blending: 1, @@ -144,7 +144,7 @@ export const coloredPreset: PaperTexturePreset = { export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, spreadPreset, - cardboardPreset, + gridPreset, coloredPreset, ] as const; From 8f1e8aec0e6d927d21faad8c76561a920d2a1454 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 18 Aug 2026 20:30:15 +0400 Subject: [PATCH 133/186] tweaks and presets --- .../src/shaders/paper-texture.tsx | 16 +++++++------- packages/shaders/src/shaders/paper-texture.ts | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 2aac9c25f..2c704422e 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -31,11 +31,11 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorFront: '#a3a6a8', + colorFront: '#b3b3b3', colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, - fiber: 0.5, + fiber: 0.65, fiberSize: 0.5, folds: 0.5, foldCount: 12, @@ -44,7 +44,7 @@ export const defaultPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 300, + lightAngle: 45, drops: 0.25, seed: 613, background: true, @@ -61,7 +61,7 @@ export const gridPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorFront: '#878f9b', + colorFront: '#605b52', colorBack: '#ffffff', roughness: 0.16, roughnessSize: 0.25, @@ -69,7 +69,7 @@ export const gridPreset: PaperTexturePreset = { fiberSize: 0.5, folds: 0, foldCount: 12, - creases: 0.55, + creases: 0.25, creaseSizeX: 0.6, creaseSizeY: 0.89, creaseOffsetX: 0.59, @@ -97,7 +97,7 @@ export const spreadPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.62, fiberSize: 0.58, - folds: 0, + folds: 0.25, foldCount: 12, creases: 0.55, creaseSizeX: 0, @@ -106,7 +106,7 @@ export const spreadPreset: PaperTexturePreset = { creaseOffsetY: 0, lightAngle: 88, drops: 0, - seed: 613, + seed: 563, background: false, }, }; @@ -133,7 +133,7 @@ export const coloredPreset: PaperTexturePreset = { creaseSizeX: 1, creaseSizeY: 0.89, creaseOffsetX: 0.62, - creaseOffsetY: 0.2, + creaseOffsetY: 0, lightAngle: 316, drops: 1, seed: 476, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b48c077e4..11180e9fd 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -87,13 +87,17 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; -#define FOLDS_TO_CREASES .02 -#define GRAIN_BY_LIGHT .5 #define CREASE_WEAR .6 -#define LIGHT_ELEVATION 45. #define LIFT_SHARPNESS 4. #define GRAIN_DRAG .2 -#define SHADING_SCALE 1. + + #define TILT_FLOOR .9 +//#define TILT_FLOOR .35 + #define RADIAL_FALLOFF .7 +//#define RADIAL_FALLOFF .25 + #define FACET_CURVE 1.5 +//#define FACET_CURVE .3 + #define SHADING_SCALE 1. float getUvFrame(vec2 uv, float blur) { float left = smoothstep(0., blur, uv.x); @@ -234,7 +238,7 @@ float getDrops(vec2 uv) { vec2 getCellTilt(float idx, float radius) { vec2 rand = hash22(vec2(idx + 31., idx * u_seed + 17.)); float an = floor(rand.x * 24.) / 24. * TWO_PI; - return vec2(cos(an), sin(an)) * mix(.5, 1., rand.y) * mix(.45, 1., radius); + return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); } vec4 getFolds(vec2 uv1, vec2 uv2) { @@ -245,7 +249,8 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; - vec2 p = vec2(cos(an), sin(an)) * rand.y * rand.y; + // vec2 p = vec2(cos(an), sin(an)) * rand.y * rand.y; + vec2 p = vec2(cos(an), sin(an)) * rand.y; vec4 d = vec4(uv1, uv2) - p.xyxy; float dsq1 = dot(d.xy, d.xy); @@ -267,8 +272,8 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { } float l = sqrt(near2), lb = sqrt(near2b); - vec2 tilt1 = getCellTilt(idx1, length(p1)) + 1.1 * (uv1 - p1); - vec2 tilt2 = getCellTilt(idx2, length(p2)) + 1.1 * (uv2 - p2); + vec2 tilt1 = getCellTilt(idx1, length(p1)) + FACET_CURVE * (uv1 - p1); + vec2 tilt2 = getCellTilt(idx2, length(p2)) + FACET_CURVE * (uv2 - p2); return vec4(.5 * (tilt1 + tilt2), .2 * l, lst(0., .5, lb - l)); } From 72bcbd29f3c75c2cf7c0f4f541ac65acb4c92e37 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 18 Aug 2026 20:47:11 +0400 Subject: [PATCH 134/186] hash22 for drops --- packages/shaders/src/shaders/paper-texture.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 11180e9fd..0fa38748c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -214,11 +214,6 @@ vec2 smoothNoise(vec2 p) { return texture(u_noiseTexture, fract((i + f + .5) / sz)).rg; } -vec2 randomGB(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).gb; -} - float getDrops(vec2 uv) { vec2 iDropsUV = floor(uv); vec2 fDropsUV = fract(uv); @@ -226,7 +221,7 @@ float getDrops(vec2 uv) { for (int y = -1; y < 2; y += 1) { for (int x = -1; x < 2; x += 1) { vec2 neighbor = vec2(float(y), float(x)); - vec2 offset = randomGB(iDropsUV + neighbor); + vec2 offset = hash22(iDropsUV + neighbor); offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); vec2 pos = neighbor + offset - fDropsUV; dropsMinDist *= min(1., dot(pos, pos)); From 0720189405f70399bd2ef98f9c2e5d21f9f6d0b2 Mon Sep 17 00:00:00 2001 From: ksenia Date: Wed, 19 Aug 2026 12:12:45 +0400 Subject: [PATCH 135/186] colorShadow --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 6 +++--- .../src/shaders/paper-texture.tsx | 12 +++++------ packages/shaders/src/shaders/paper-texture.ts | 20 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 29a366196..7d8f78e19 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -68,7 +68,7 @@ const PaperTextureWithControls = () => { ); return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + colorShadow: { value: toHsla(defaults.colorShadow), order: 101 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, lightAngle: { value: defaults.lightAngle, min: 0, max: 360, order: 201 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 300 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index b1ad5e518..854d131cd 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -22,11 +22,11 @@ export const paperTextureDef: ShaderDef = { description: 'Background color', }, { - name: 'colorFront', + name: 'colorShadow', type: 'string', - defaultValue: defaultParams.colorFront, + defaultValue: defaultParams.colorShadow, isColor: true, - description: 'Foreground color', + description: 'Colour laid into the shaded side of the surface', }, { name: 'blending', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 2c704422e..99d609fda 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -31,7 +31,7 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorFront: '#b3b3b3', + colorShadow: '#b3b3b3', colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, @@ -61,7 +61,7 @@ export const gridPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorFront: '#605b52', + colorShadow: '#605b52', colorBack: '#ffffff', roughness: 0.16, roughnessSize: 0.25, @@ -91,7 +91,7 @@ export const spreadPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorFront: '#c2c2c2', + colorShadow: '#c2c2c2', colorBack: '#ffffff', roughness: 0.16, roughnessSize: 0.25, @@ -121,7 +121,7 @@ export const coloredPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorFront: '#ffcccc', + colorShadow: '#ffcccc', colorBack: '#3b2942', roughness: 0.25, roughnessSize: 0.08, @@ -152,7 +152,7 @@ export const PaperTexture: React.FC = memo(function PaperText // Own props speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, - colorFront = defaultPreset.params.colorFront, + colorShadow = defaultPreset.params.colorShadow, colorBack = defaultPreset.params.colorBack, image = '', roughness = defaultPreset.params.roughness, @@ -197,7 +197,7 @@ export const PaperTexture: React.FC = memo(function PaperText // Own uniforms u_image: image, u_isImage: Boolean(image), - u_colorFront: getShaderColorFromString(colorFront), + u_colorShadow: getShaderColorFromString(colorShadow), u_colorBack: getShaderColorFromString(colorBack), u_roughness: roughness, u_roughnessSize: roughnessSize, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0fa38748c..b68cb6cb5 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -14,7 +14,7 @@ export const paperTextureMeta = { * - u_image (sampler2D): Optional source image texture * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image - * - u_colorFront (vec4): Foreground color in RGBA + * - u_colorShadow (vec4): Colour laid into the shaded side of the surface, in RGBA * - u_colorBack (vec4): Background color in RGBA * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) @@ -31,7 +31,7 @@ export const paperTextureMeta = { * - u_foldTrig (vec2): Cos and sin of the crumple frame rotation, derived from the seed * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) * - u_seed (float): Seed applied to folds and drops (0 to 1000) - * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorFront) and thinned so the background reads through, needs image (0 to 1) + * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) * - u_distortion (float): Amount of distortion of the image by the paper normals, needs image (-1 to 1) * - u_background (bool): Shows or hides the paper texture outside the image frame, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture @@ -57,7 +57,7 @@ export const paperTextureMeta = { export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; -uniform vec4 u_colorFront; +uniform vec4 u_colorShadow; uniform vec4 u_colorBack; uniform sampler2D u_image; @@ -391,8 +391,8 @@ void main() { vec3 bgColor = u_colorBack.rgb; float bgOpacity = u_colorBack.a; - vec3 fgColor = u_colorFront.rgb; - float fgOpacity = u_colorFront.a; + vec3 shadowColor = u_colorShadow.rgb; + float shadowOpacity = u_colorShadow.a; imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); imageUV += u_distortion * vec2(xDistortion, -yShift); @@ -400,7 +400,7 @@ void main() { float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - float patternAlpha = clamp(fgOpacity * pattern, 0., 1.); + float patternAlpha = clamp(shadowOpacity * pattern, 0., 1.); vec3 pic = vec3(0.); float imageFootprint = 0.; @@ -418,7 +418,7 @@ void main() { float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - vec3 ink = mix(vec3(1.), fgColor, patternAlpha); + vec3 ink = mix(vec3(1.), shadowColor, patternAlpha); pic = blendMultiply(image.rgb, ink, u_blending); float darkDampen = 1. - lum; @@ -431,7 +431,7 @@ void main() { float imageAlpha = imageFootprint; - vec3 overlay = pic * imageAlpha + fgColor * patternAlpha * (1. - imageAlpha); + vec3 overlay = pic * imageAlpha + shadowColor * patternAlpha * (1. - imageAlpha); float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); @@ -450,7 +450,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_isImage: boolean; u_noiseTexture?: HTMLImageElement; - u_colorFront: [number, number, number, number]; + u_colorShadow: [number, number, number, number]; u_colorBack: [number, number, number, number]; u_roughness: number; u_roughnessSize: number; @@ -474,7 +474,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string; - colorFront?: string; + colorShadow?: string; colorBack?: string; roughness?: number; roughnessSize?: number; From 25e270f40d48b5fe54f2bcd2110a711364fc9887 Mon Sep 17 00:00:00 2001 From: ksenia Date: Wed, 19 Aug 2026 19:51:44 +0400 Subject: [PATCH 136/186] merging issue on Water --- docs/src/app/(shaders)/water/page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/app/(shaders)/water/page.tsx b/docs/src/app/(shaders)/water/page.tsx index 878abe508..c005c24c5 100644 --- a/docs/src/app/(shaders)/water/page.tsx +++ b/docs/src/app/(shaders)/water/page.tsx @@ -68,6 +68,7 @@ const WaterWithControls = () => { ); return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorHighlight: { value: toHsla(defaults.colorHighlight), order: 101 }, highlights: { value: defaults.highlights, min: 0, max: 1, order: 200 }, layering: { value: defaults.layering, min: 0, max: 1, order: 201 }, edges: { value: defaults.edges, min: 0, max: 1, order: 202 }, From f3e43c3607b07dff68226949b216b7c532c34d16 Mon Sep 17 00:00:00 2001 From: ksenia Date: Wed, 19 Aug 2026 20:18:51 +0400 Subject: [PATCH 137/186] defs apss 1 --- docs/src/shader-defs/paper-texture-def.ts | 32 ++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 854d131cd..b3b93d4b3 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -7,7 +7,7 @@ const defaultParams = paperTexturePresets[0].params; export const paperTextureDef: ShaderDef = { name: 'Paper Texture', description: - 'A static texture built from multiple noise layers, usable for a realistic paper and cardboard surfaces. Can be used as a image filter or as a texture.', + 'Static paper-like texture built from a combination of grain, noise and multiple folds pattarns. Works as an image filter or as a standalone texture.', params: [ { name: 'image', @@ -19,14 +19,14 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBack, isColor: true, - description: 'Background color', + description: 'The color behind the image, usually light', }, { name: 'colorShadow', type: 'string', defaultValue: defaultParams.colorShadow, isColor: true, - description: 'Colour laid into the shaded side of the surface', + description: 'The color used for folds, creases, grain and speckles, blends into image', }, { name: 'blending', @@ -35,7 +35,7 @@ export const paperTextureDef: ShaderDef = { max: 1, defaultValue: defaultParams.blending, description: - 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the paper ink), needs image', + 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the pattern), needs image', }, { name: 'distortion', @@ -43,13 +43,14 @@ export const paperTextureDef: ShaderDef = { min: -1, max: 1, defaultValue: defaultParams.distortion, - description: 'Amount of image distortion following the paper surface (needs image)', + description: + 'How much the image bends with the paper surface; negative values bend it the opposite direction (needs image)', }, { name: 'background', type: 'boolean', defaultValue: defaultParams.background, - description: 'Shows or hides the paper texture outside the image frame (needs image)', + description: 'Shows or hides the paper texture outside the distorted image frame (needs image)', }, { name: 'roughness', @@ -57,7 +58,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.roughness, - description: 'Pixel noise, related to canvas (not scalable)', + description: 'Grain noise with size independednt from scaling and with level of delails depending on it', }, { name: 'roughnessSize', @@ -73,7 +74,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.fiber, - description: 'Curly-shaped noise', + description: 'Curly fiber noise, simulating the real paper', }, { name: 'fiberSize', @@ -81,7 +82,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.fiberSize, - description: 'Curly-shaped noise scale (needs fiber > 0)', + description: 'Scale of the fiber noise (needs fiber > 0)', }, { name: 'folds', @@ -89,7 +90,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.folds, - description: 'Depth of the radial folds', + description: 'Depth of the centered, irregular field of facets across the sheet', }, { name: 'foldCount', @@ -98,7 +99,7 @@ export const paperTextureDef: ShaderDef = { max: 20, step: 1, defaultValue: defaultParams.foldCount, - description: 'Number of radial folds (20 max), needs folds > 0', + description: 'Number of folds (20 max), needs folds > 0', }, { name: 'creases', @@ -106,7 +107,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.creases, - description: 'Depth of the creases, an independent layer over the radial folds', + description: 'Depth of the straight creases, alternating between ridges and valleys', }, { name: 'creaseSizeX', @@ -122,7 +123,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.creaseSizeY, - description: 'Size of the horizontal creases running across them (needs creases > 0)', + description: 'Size of the horizontal creases (needs creases > 0)', }, { name: 'creaseOffsetX', @@ -146,7 +147,8 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 360, defaultValue: defaultParams.lightAngle, - description: 'Direction the folded surface is lit from, clockwise from the top of the canvas, in degrees', + description: + 'Direction the surface is lit from, clockwise from the top of the canvas, needs folds or creases > 0', }, { name: 'drops', @@ -154,7 +156,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.drops, - description: 'The visibility of speckle pattern', + description: 'Visibility of the speckle pattern', }, { name: 'seed', From 5cac77d5c477b6a30856bd50d31643c0b6d25316 Mon Sep 17 00:00:00 2001 From: ksenia Date: Wed, 19 Aug 2026 20:24:11 +0400 Subject: [PATCH 138/186] defs pass 1 --- docs/src/shader-defs/paper-texture-def.ts | 6 ++-- packages/shaders/src/shaders/paper-texture.ts | 30 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index b3b93d4b3..a63254da2 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -7,7 +7,7 @@ const defaultParams = paperTexturePresets[0].params; export const paperTextureDef: ShaderDef = { name: 'Paper Texture', description: - 'Static paper-like texture built from a combination of grain, noise and multiple folds pattarns. Works as an image filter or as a standalone texture.', + 'Static paper-like texture built from a combination of grain, noise and multiple fold patterns. Works as an image filter or as a standalone texture.', params: [ { name: 'image', @@ -26,7 +26,7 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorShadow, isColor: true, - description: 'The color used for folds, creases, grain and speckles, blends into image', + description: 'The color used for folds, creases, grain and speckles, blends into the image', }, { name: 'blending', @@ -58,7 +58,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.roughness, - description: 'Grain noise with size independednt from scaling and with level of delails depending on it', + description: 'Grain noise, sized independently of scaling, with its level of detail depending on the scale', }, { name: 'roughnessSize', diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b68cb6cb5..92417881b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -7,33 +7,33 @@ export const paperTextureMeta = { } as const; /** - * A static texture built from multiple noise layers, usable for realistic paper and cardboard surfaces. - * Can be used as an image filter or as a standalone texture. + * Static paper-like texture built from a combination of grain, noise and multiple fold patterns. + * Works as an image filter or as a standalone texture. * * Fragment shader uniforms: * - u_image (sampler2D): Optional source image texture * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image - * - u_colorShadow (vec4): Colour laid into the shaded side of the surface, in RGBA - * - u_colorBack (vec4): Background color in RGBA - * - u_roughness (float): Hi-freq grain-like distortion intensity (0 to 1) + * - u_colorShadow (vec4): Color used for folds, creases, grain and speckles, blends into the image, in RGBA + * - u_colorBack (vec4): Color behind the image, usually light, in RGBA + * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) - * - u_fiber (float): Curly-shaped noise intensity (0 to 1) - * - u_fiberSize (float): Curly-shaped noise scale, needs u_fiber > 0 (0 to 1) - * - u_folds (float): Depth of the radial folds (0 to 1) - * - u_foldCount (float): Number of radial folds, needs u_folds > 0 (1 to 20) - * - u_creases (float): Depth of the creases, an independent layer over the radial folds (0 to 1) + * - u_fiber (float): Curly fiber noise, simulating real paper (0 to 1) + * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) + * - u_folds (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) + * - u_foldCount (float): Number of folds, needs u_folds > 0 (1 to 20) + * - u_creases (float): Depth of the straight creases, alternating between ridges and valleys (0 to 1) * - u_creaseSizeX (float): Size of the vertical creases, needs u_creases > 0 (0 to 1) - * - u_creaseSizeY (float): Size of the horizontal creases running across them, needs u_creases > 0 (0 to 1) + * - u_creaseSizeY (float): Size of the horizontal creases, needs u_creases > 0 (0 to 1) * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (0 to 1) * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (0 to 1) - * - u_lightDir (vec2): Direction the folded surface is lit from, as a unit vector (derived from the lightAngle prop) + * - u_lightDir (vec2): Direction the surface is lit from as a unit vector, derived from the lightAngle prop, needs u_folds or u_creases > 0 * - u_foldTrig (vec2): Cos and sin of the crumple frame rotation, derived from the seed - * - u_drops (float): Visibility of speckle / drop pattern (0 to 1) + * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) - * - u_distortion (float): Amount of distortion of the image by the paper normals, needs image (-1 to 1) - * - u_background (bool): Shows or hides the paper texture outside the image frame, needs image + * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) + * - u_background (bool): Shows or hides the paper texture outside the distorted image frame, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): From 6713625fce82f88dd602ec328746b75cc168db17 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:06:14 +0400 Subject: [PATCH 139/186] folds upd --- docs/src/shader-defs/paper-texture-def.ts | 4 +-- .../src/shaders/paper-texture.tsx | 4 +-- packages/shaders/src/shaders/paper-texture.ts | 30 ++++++++++--------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index a63254da2..a5ef49d7b 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -96,10 +96,10 @@ export const paperTextureDef: ShaderDef = { name: 'foldCount', type: 'number', min: 1, - max: 20, + max: 15, step: 1, defaultValue: defaultParams.foldCount, - description: 'Number of folds (20 max), needs folds > 0', + description: 'Number of folds (15 max), needs folds > 0', }, { name: 'creases', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 99d609fda..f0569424e 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -45,7 +45,7 @@ export const defaultPreset: PaperTexturePreset = { creaseOffsetX: 0, creaseOffsetY: 0, lightAngle: 45, - drops: 0.25, + drops: 0.7, seed: 613, background: true, }, @@ -98,7 +98,7 @@ export const spreadPreset: PaperTexturePreset = { fiber: 0.62, fiberSize: 0.58, folds: 0.25, - foldCount: 12, + foldCount: 9, creases: 0.55, creaseSizeX: 0, creaseSizeY: 0.89, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 92417881b..eea0484c5 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -3,7 +3,7 @@ import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-si import { declarePI, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { - maxFoldCount: 20, + maxFoldCount: 15, } as const; /** @@ -21,7 +21,7 @@ export const paperTextureMeta = { * - u_fiber (float): Curly fiber noise, simulating real paper (0 to 1) * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_folds (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) - * - u_foldCount (float): Number of folds, needs u_folds > 0 (1 to 20) + * - u_foldCount (float): Number of folds, needs u_folds > 0 (1 to 15) * - u_creases (float): Depth of the straight creases, alternating between ridges and valleys (0 to 1) * - u_creaseSizeX (float): Size of the vertical creases, needs u_creases > 0 (0 to 1) * - u_creaseSizeY (float): Size of the horizontal creases, needs u_creases > 0 (0 to 1) @@ -92,11 +92,10 @@ out vec4 fragColor; #define GRAIN_DRAG .2 #define TILT_FLOOR .9 -//#define TILT_FLOOR .35 #define RADIAL_FALLOFF .7 -//#define RADIAL_FALLOFF .25 #define FACET_CURVE 1.5 -//#define FACET_CURVE .3 + #define FOLD_LIT_GAIN .35 + #define LIT_FLOOR 0. #define SHADING_SCALE 1. float getUvFrame(vec2 uv, float blur) { @@ -239,12 +238,11 @@ vec2 getCellTilt(float idx, float radius) { vec4 getFolds(vec2 uv1, vec2 uv2) { float near1 = 9., near2 = 9., near2b = 9.; float idx1 = 0., idx2 = 0.; - vec2 p1 = vec2(0.), p2 = vec2(0.); + vec2 p1 = vec2(0.), p2 = vec2(0.), p2b = vec2(0.); for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; - // vec2 p = vec2(cos(an), sin(an)) * rand.y * rand.y; vec2 p = vec2(cos(an), sin(an)) * rand.y; vec4 d = vec4(uv1, uv2) - p.xyxy; @@ -258,19 +256,22 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { float dsq2 = dot(d.zw, d.zw); if (dsq2 < near2) { near2b = near2; + p2b = p2; near2 = dsq2; idx2 = float(i); p2 = p; } else if (dsq2 < near2b) { near2b = dsq2; + p2b = p; } } float l = sqrt(near2), lb = sqrt(near2b); - vec2 tilt1 = getCellTilt(idx1, length(p1)) + FACET_CURVE * (uv1 - p1); - vec2 tilt2 = getCellTilt(idx2, length(p2)) + FACET_CURVE * (uv2 - p2); + float edge = lst(0., .5, lb - l); + vec2 edgeGrad = (uv2 - p2b) / max(lb, 1e-4) - (uv2 - p2) / max(l, 1e-4); + vec2 tilt = .5 * (getCellTilt(idx1, length(p1)) + getCellTilt(idx2, length(p2))); - return vec4(.5 * (tilt1 + tilt2), .2 * l, lst(0., .5, lb - l)); + return vec4(tilt + FACET_CURVE * (1. - edge) * edgeGrad, .2 * l, edge); } void getCreases(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { @@ -322,12 +323,13 @@ void main() { vec2 foldsUV2 = foldsUV1 + .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); vec4 folds = getFolds(foldsUV1, foldsUV2); - vec2 foldTilt = u_folds * folds.xy; + vec2 foldTilt = .5 * u_folds * folds.xy; + foldTilt -= (1. - FOLD_LIT_GAIN) * max(dot(foldTilt, u_lightDir), 0.) * u_lightDir; relief += foldTilt; - reliefAmount += u_folds; + reliefAmount += .5 * u_folds; foldFlow = folds.w * foldTilt; - scaleDistortion = .22 * clamp(5. * folds.z, 0., 1.) * u_folds; + scaleDistortion = .15 * clamp(5. * folds.z, 0., 1.) * u_folds; } if (u_creases > 0.) { @@ -359,7 +361,7 @@ void main() { if (reliefAmount > 0.) { float slope = clamp(dot(relief, u_lightDir), -1.2, 1.2); - lit = SHADING_SCALE * max(cos(slope + grazing), 0.); + lit = SHADING_SCALE * max(cos(slope + grazing), LIT_FLOOR); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * creaseInk; } From 2e865f8055c02f813abcc44066041978b45800fa Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:14:14 +0400 Subject: [PATCH 140/186] preset adjusted --- packages/shaders-react/src/shaders/paper-texture.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index f0569424e..60663e654 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,7 +38,7 @@ export const defaultPreset: PaperTexturePreset = { fiber: 0.65, fiberSize: 0.5, folds: 0.5, - foldCount: 12, + foldCount: 6, creases: 0, creaseSizeX: 0.6, creaseSizeY: 0.89, @@ -46,7 +46,7 @@ export const defaultPreset: PaperTexturePreset = { creaseOffsetY: 0, lightAngle: 45, drops: 0.7, - seed: 613, + seed: 315, background: true, }, }; From f5cf08b7dc60a3cceabbe15986f7c08bbbd7b257 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:18:22 +0400 Subject: [PATCH 141/186] preset adjusted --- packages/shaders-react/src/shaders/paper-texture.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 60663e654..a8d737d7e 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -44,9 +44,9 @@ export const defaultPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 45, + lightAngle: 300, drops: 0.7, - seed: 315, + seed: 358, background: true, }, }; From 8f82ed77ecbf20ecb7254da722a1a7d6adfbe4e8 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:19:45 +0400 Subject: [PATCH 142/186] small opt --- packages/shaders/src/shaders/paper-texture.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index eea0484c5..f9262a684 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -237,7 +237,7 @@ vec2 getCellTilt(float idx, float radius) { vec4 getFolds(vec2 uv1, vec2 uv2) { float near1 = 9., near2 = 9., near2b = 9.; - float idx1 = 0., idx2 = 0.; + float idx1 = 0., idx2 = 0., rad1 = 0., rad2 = 0.; vec2 p1 = vec2(0.), p2 = vec2(0.), p2b = vec2(0.); for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { if (float(i) >= floor(u_foldCount + .5)) break; @@ -250,6 +250,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { if (dsq1 < near1) { near1 = dsq1; idx1 = float(i); + rad1 = rand.y; p1 = p; } @@ -259,6 +260,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { p2b = p2; near2 = dsq2; idx2 = float(i); + rad2 = rand.y; p2 = p; } else if (dsq2 < near2b) { near2b = dsq2; @@ -269,7 +271,7 @@ vec4 getFolds(vec2 uv1, vec2 uv2) { float edge = lst(0., .5, lb - l); vec2 edgeGrad = (uv2 - p2b) / max(lb, 1e-4) - (uv2 - p2) / max(l, 1e-4); - vec2 tilt = .5 * (getCellTilt(idx1, length(p1)) + getCellTilt(idx2, length(p2))); + vec2 tilt = .5 * (getCellTilt(idx1, rad1) + getCellTilt(idx2, rad2)); return vec4(tilt + FACET_CURVE * (1. - edge) * edgeGrad, .2 * l, edge); } From 389adacb16781c2b363a72a7c87191505612fa91 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:41:14 +0400 Subject: [PATCH 143/186] folds 2nd UV removed --- packages/shaders/src/shaders/paper-texture.ts | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index f9262a684..274771244 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -235,45 +235,36 @@ vec2 getCellTilt(float idx, float radius) { return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); } -vec4 getFolds(vec2 uv1, vec2 uv2) { - float near1 = 9., near2 = 9., near2b = 9.; - float idx1 = 0., idx2 = 0., rad1 = 0., rad2 = 0.; - vec2 p1 = vec2(0.), p2 = vec2(0.), p2b = vec2(0.); +vec4 getFolds(vec2 uv) { + float near = 9., nearB = 9.; + float idx = 0., rad = 0.; + vec2 nearP = vec2(0.), nearPb = vec2(0.); for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { if (float(i) >= floor(u_foldCount + .5)) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; - vec4 d = vec4(uv1, uv2) - p.xyxy; - float dsq1 = dot(d.xy, d.xy); - if (dsq1 < near1) { - near1 = dsq1; - idx1 = float(i); - rad1 = rand.y; - p1 = p; - } - - float dsq2 = dot(d.zw, d.zw); - if (dsq2 < near2) { - near2b = near2; - p2b = p2; - near2 = dsq2; - idx2 = float(i); - rad2 = rand.y; - p2 = p; - } else if (dsq2 < near2b) { - near2b = dsq2; - p2b = p; + vec2 d = uv - p; + float dsq = dot(d, d); + if (dsq < near) { + nearB = near; + nearPb = nearP; + near = dsq; + idx = float(i); + rad = rand.y; + nearP = p; + } else if (dsq < nearB) { + nearB = dsq; + nearPb = p; } } - float l = sqrt(near2), lb = sqrt(near2b); + float l = sqrt(near), lb = sqrt(nearB); float edge = lst(0., .5, lb - l); - vec2 edgeGrad = (uv2 - p2b) / max(lb, 1e-4) - (uv2 - p2) / max(l, 1e-4); - vec2 tilt = .5 * (getCellTilt(idx1, rad1) + getCellTilt(idx2, rad2)); + vec2 edgeGrad = (uv - nearPb) / max(lb, 1e-4) - (uv - nearP) / max(l, 1e-4); - return vec4(tilt + FACET_CURVE * (1. - edge) * edgeGrad, .2 * l, edge); + return vec4(getCellTilt(idx, rad) + FACET_CURVE * (1. - edge) * edgeGrad, .2 * l, edge); } void getCreases(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { @@ -321,9 +312,9 @@ void main() { if (u_folds > 0.) { vec2 foldQ = patternUV * .18; - vec2 foldsUV1 = vec2(u_foldTrig.x * foldQ.x - u_foldTrig.y * foldQ.y, u_foldTrig.y * foldQ.x + u_foldTrig.x * foldQ.y); - vec2 foldsUV2 = foldsUV1 + .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); - vec4 folds = getFolds(foldsUV1, foldsUV2); + vec2 foldsUV = vec2(u_foldTrig.x * foldQ.x - u_foldTrig.y * foldQ.y, u_foldTrig.y * foldQ.x + u_foldTrig.x * foldQ.y) + + .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); + vec4 folds = getFolds(foldsUV); vec2 foldTilt = .5 * u_folds * folds.xy; foldTilt -= (1. - FOLD_LIT_GAIN) * max(dot(foldTilt, u_lightDir), 0.) * u_lightDir; From 9f6b9ee8970d7654254c5b57a3ab700990607170 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:47:38 +0400 Subject: [PATCH 144/186] lightAngle as a single uniform --- .../src/shaders/paper-texture.tsx | 5 +--- packages/shaders/src/shaders/paper-texture.ts | 24 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index a8d737d7e..17ca74d68 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -191,8 +191,6 @@ export const PaperTexture: React.FC = memo(function PaperText }: PaperTextureProps) { const noiseTexture = typeof window !== 'undefined' && { u_noiseTexture: getShaderNoiseTexture() }; - const lightRadians = (lightAngle * Math.PI) / 180; - const uniforms = { // Own uniforms u_image: image, @@ -208,8 +206,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_creaseSizeY: creaseSizeY, u_creaseOffsetX: creaseOffsetX, u_creaseOffsetY: creaseOffsetY, - u_lightDir: [Math.sin(lightRadians), -Math.cos(lightRadians)], - u_foldTrig: [Math.cos(4 * seed), Math.sin(4 * seed)], + u_lightAngle: lightAngle, u_folds: folds, u_creases: creases, u_drops: drops, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 274771244..a61a59e23 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -27,8 +27,7 @@ export const paperTextureMeta = { * - u_creaseSizeY (float): Size of the horizontal creases, needs u_creases > 0 (0 to 1) * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (0 to 1) * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (0 to 1) - * - u_lightDir (vec2): Direction the surface is lit from as a unit vector, derived from the lightAngle prop, needs u_folds or u_creases > 0 - * - u_foldTrig (vec2): Cos and sin of the crumple frame rotation, derived from the seed + * - u_lightAngle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_folds or u_creases > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) @@ -74,8 +73,7 @@ uniform float u_creaseSizeX; uniform float u_creaseSizeY; uniform float u_creaseOffsetX; uniform float u_creaseOffsetY; -uniform vec2 u_lightDir; -uniform vec2 u_foldTrig; +uniform float u_lightAngle; uniform float u_drops; uniform float u_seed; uniform float u_blending; @@ -304,6 +302,8 @@ void main() { } float grazing = 0.7; + float lightRad = radians(u_lightAngle); + vec2 lightDir = vec2(sin(lightRad), -cos(lightRad)); vec2 relief = vec2(0.); float reliefAmount = 0.; float creaseInk = 1.; @@ -311,13 +311,14 @@ void main() { vec2 foldFlow = vec2(0.); if (u_folds > 0.) { - vec2 foldQ = patternUV * .18; - vec2 foldsUV = vec2(u_foldTrig.x * foldQ.x - u_foldTrig.y * foldQ.y, u_foldTrig.y * foldQ.x + u_foldTrig.x * foldQ.y) + + float foldRad = 4. * u_seed; + float foldCos = cos(foldRad), foldSin = sin(foldRad); + vec2 foldsUV = mat2(foldCos, foldSin, -foldSin, foldCos) * (patternUV * .18) + .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); vec4 folds = getFolds(foldsUV); vec2 foldTilt = .5 * u_folds * folds.xy; - foldTilt -= (1. - FOLD_LIT_GAIN) * max(dot(foldTilt, u_lightDir), 0.) * u_lightDir; + foldTilt -= (1. - FOLD_LIT_GAIN) * max(dot(foldTilt, lightDir), 0.) * lightDir; relief += foldTilt; reliefAmount += .5 * u_folds; foldFlow = folds.w * foldTilt; @@ -341,10 +342,10 @@ void main() { vec2 dropMask = mix(vec2(1.), dark, u_creases); drops *= dropMask.x * dropMask.y; - yShift += .022 * u_creases * lift.x * abs(u_lightDir.x); + yShift += .022 * u_creases * lift.x * abs(lightDir.x); patternUV.y += yShift; - float xFan = 2. * (imageUV.x - .5) * abs(u_lightDir.y); + float xFan = 2. * (imageUV.x - .5) * abs(lightDir.y); xDistortion -= .02 * u_creases * lift.y * xFan; patternUV.x -= .022 * u_creases * lift.y * xFan; } @@ -353,7 +354,7 @@ void main() { float lit = unlit; if (reliefAmount > 0.) { - float slope = clamp(dot(relief, u_lightDir), -1.2, 1.2); + float slope = clamp(dot(relief, lightDir), -1.2, 1.2); lit = SHADING_SCALE * max(cos(slope + grazing), LIT_FLOOR); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * creaseInk; @@ -458,8 +459,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_creaseSizeY: number; u_creaseOffsetX: number; u_creaseOffsetY: number; - u_lightDir: [number, number]; - u_foldTrig: [number, number]; + u_lightAngle: number; u_drops: number; u_seed: number; u_blending: number; From 87bcef4d9d86bad47dcd358d6a6e631ffc793281 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 21:57:26 +0400 Subject: [PATCH 145/186] preset --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 17ca74d68..d48ef39a0 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -46,7 +46,7 @@ export const defaultPreset: PaperTexturePreset = { creaseOffsetY: 0, lightAngle: 300, drops: 0.7, - seed: 358, + seed: 234, background: true, }, }; From 7fc2fb35407f7b0d53a6da1c6f33032300f6ccca Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 23 Aug 2026 23:33:06 +0400 Subject: [PATCH 146/186] preset --- packages/shaders-react/src/shaders/paper-texture.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index d48ef39a0..b99326fe2 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -121,9 +121,9 @@ export const coloredPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorShadow: '#ffcccc', - colorBack: '#3b2942', - roughness: 0.25, + colorShadow: '#7654c9', + colorBack: '#f5b689', + roughness: 0, roughnessSize: 0.08, fiber: 0.52, fiberSize: 1, From 0ca6ce7522423f06e5c09f51afaf15aa3978a835 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 11:01:32 +0200 Subject: [PATCH 147/186] lightAngle => angle --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 4 +-- .../src/shaders/paper-texture.tsx | 12 ++++---- packages/shaders/src/shaders/paper-texture.ts | 28 +++++++++---------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 7d8f78e19..92c3e3be0 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -70,7 +70,7 @@ const PaperTextureWithControls = () => { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, colorShadow: { value: toHsla(defaults.colorShadow), order: 101 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, - lightAngle: { value: defaults.lightAngle, min: 0, max: 360, order: 201 }, + angle: { value: defaults.angle, min: 0, max: 360, order: 201 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 300 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 301 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 310 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index a5ef49d7b..03a5d31bd 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -142,11 +142,11 @@ export const paperTextureDef: ShaderDef = { description: 'Shifts the horizontal creases across the surface (needs creases > 0)', }, { - name: 'lightAngle', + name: 'angle', type: 'number', min: 0, max: 360, - defaultValue: defaultParams.lightAngle, + defaultValue: defaultParams.angle, description: 'Direction the surface is lit from, clockwise from the top of the canvas, needs folds or creases > 0', }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index b99326fe2..4d8fba701 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -44,7 +44,7 @@ export const defaultPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 300, + angle: 300, drops: 0.7, seed: 234, background: true, @@ -74,7 +74,7 @@ export const gridPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0.59, creaseOffsetY: 1, - lightAngle: 140, + angle: 140, drops: 0, seed: 613, background: true, @@ -104,7 +104,7 @@ export const spreadPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0, creaseOffsetY: 0, - lightAngle: 88, + angle: 88, drops: 0, seed: 563, background: false, @@ -134,7 +134,7 @@ export const coloredPreset: PaperTexturePreset = { creaseSizeY: 0.89, creaseOffsetX: 0.62, creaseOffsetY: 0, - lightAngle: 316, + angle: 316, drops: 1, seed: 476, background: true, @@ -164,7 +164,7 @@ export const PaperTexture: React.FC = memo(function PaperText creaseSizeY = defaultPreset.params.creaseSizeY, creaseOffsetX = defaultPreset.params.creaseOffsetX, creaseOffsetY = defaultPreset.params.creaseOffsetY, - lightAngle = defaultPreset.params.lightAngle, + angle = defaultPreset.params.angle, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, @@ -206,7 +206,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_creaseSizeY: creaseSizeY, u_creaseOffsetX: creaseOffsetX, u_creaseOffsetY: creaseOffsetY, - u_lightAngle: lightAngle, + u_angle: angle, u_folds: folds, u_creases: creases, u_drops: drops, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index a61a59e23..22dcff96f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -27,7 +27,7 @@ export const paperTextureMeta = { * - u_creaseSizeY (float): Size of the horizontal creases, needs u_creases > 0 (0 to 1) * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (0 to 1) * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (0 to 1) - * - u_lightAngle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_folds or u_creases > 0 (0 to 360) + * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_folds or u_creases > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) @@ -73,7 +73,7 @@ uniform float u_creaseSizeX; uniform float u_creaseSizeY; uniform float u_creaseOffsetX; uniform float u_creaseOffsetY; -uniform float u_lightAngle; +uniform float u_angle; uniform float u_drops; uniform float u_seed; uniform float u_blending; @@ -89,12 +89,11 @@ out vec4 fragColor; #define LIFT_SHARPNESS 4. #define GRAIN_DRAG .2 - #define TILT_FLOOR .9 - #define RADIAL_FALLOFF .7 - #define FACET_CURVE 1.5 - #define FOLD_LIT_GAIN .35 - #define LIT_FLOOR 0. - #define SHADING_SCALE 1. +#define TILT_FLOOR .9 +#define RADIAL_FALLOFF .7 +#define FACET_CURVE 1.5 +#define FOLD_LIT_GAIN .35 +#define SHADING_SCALE 1. float getUvFrame(vec2 uv, float blur) { float left = smoothstep(0., blur, uv.x); @@ -203,12 +202,11 @@ float getFiber(vec2 p) { } vec2 smoothNoise(vec2 p) { - vec2 sz = vec2(textureSize(u_noiseTexture, 0)); - vec2 t = p * sz - .5; + vec2 t = p * 50. - .5; vec2 i = floor(t); vec2 f = fract(t); f = f * f * (3. - 2. * f); - return texture(u_noiseTexture, fract((i + f + .5) / sz)).rg; + return texture(u_noiseTexture, fract((i + f + .5) / vec2(50.))).rg; } float getDrops(vec2 uv) { @@ -302,7 +300,7 @@ void main() { } float grazing = 0.7; - float lightRad = radians(u_lightAngle); + float lightRad = radians(u_angle); vec2 lightDir = vec2(sin(lightRad), -cos(lightRad)); vec2 relief = vec2(0.); float reliefAmount = 0.; @@ -355,7 +353,7 @@ void main() { if (reliefAmount > 0.) { float slope = clamp(dot(relief, lightDir), -1.2, 1.2); - lit = SHADING_SCALE * max(cos(slope + grazing), LIT_FLOOR); + lit = SHADING_SCALE * max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * creaseInk; } @@ -459,7 +457,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_creaseSizeY: number; u_creaseOffsetX: number; u_creaseOffsetY: number; - u_lightAngle: number; + u_angle: number; u_drops: number; u_seed: number; u_blending: number; @@ -482,7 +480,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara creaseSizeY?: number; creaseOffsetX?: number; creaseOffsetY?: number; - lightAngle?: number; + angle?: number; drops?: number; seed?: number; blending?: number; From d1e2b10ef74bfd15c50a39f6a8a3c96ca76d9eb1 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 11:05:52 +0200 Subject: [PATCH 148/186] background => clip --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 6 +++--- packages/shaders-react/src/shaders/paper-texture.tsx | 12 ++++++------ packages/shaders/src/shaders/paper-texture.ts | 10 +++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 92c3e3be0..984cc0a2f 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -85,7 +85,7 @@ const PaperTextureWithControls = () => { drops: { value: defaults.drops, min: 0, max: 1, order: 340 }, blending: { value: defaults.blending, min: 0, max: 1, order: 350 }, distortion: { value: defaults.distortion, min: -1, max: 1, order: 351 }, - background: { value: defaults.background, order: 352 }, + clip: { value: defaults.clip, order: 352 }, scale: { value: defaults.scale, min: 0.5, max: 10, order: 400 }, fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 401 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 03a5d31bd..6f5c5f38d 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -47,10 +47,10 @@ export const paperTextureDef: ShaderDef = { 'How much the image bends with the paper surface; negative values bend it the opposite direction (needs image)', }, { - name: 'background', + name: 'clip', type: 'boolean', - defaultValue: defaultParams.background, - description: 'Shows or hides the paper texture outside the distorted image frame (needs image)', + defaultValue: defaultParams.clip, + description: 'Hides the paper texture outside the distorted image frame (needs image)', }, { name: 'roughness', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 4d8fba701..dac822711 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -47,7 +47,7 @@ export const defaultPreset: PaperTexturePreset = { angle: 300, drops: 0.7, seed: 234, - background: true, + clip: false, }, }; @@ -77,7 +77,7 @@ export const gridPreset: PaperTexturePreset = { angle: 140, drops: 0, seed: 613, - background: true, + clip: false, }, }; @@ -107,7 +107,7 @@ export const spreadPreset: PaperTexturePreset = { angle: 88, drops: 0, seed: 563, - background: false, + clip: true, }, }; @@ -137,7 +137,7 @@ export const coloredPreset: PaperTexturePreset = { angle: 316, drops: 1, seed: 476, - background: true, + clip: false, }, }; @@ -169,7 +169,7 @@ export const PaperTexture: React.FC = memo(function PaperText seed = defaultPreset.params.seed, blending = defaultPreset.params.blending, distortion = defaultPreset.params.distortion, - background = defaultPreset.params.background, + clip = defaultPreset.params.clip, // Reworked props fiberScale, @@ -213,7 +213,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_seed: seed, u_blending: blending, u_distortion: distortion, - u_background: background, + u_clip: clip, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 22dcff96f..05b8985f4 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -32,7 +32,7 @@ export const paperTextureMeta = { * - u_seed (float): Seed applied to folds and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) - * - u_background (bool): Shows or hides the paper texture outside the distorted image frame, needs image + * - u_clip (bool): Hides the paper texture outside the distorted image frame, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -79,7 +79,7 @@ uniform float u_seed; uniform float u_blending; uniform float u_distortion; uniform float u_roughnessSize; -uniform bool u_background; +uniform bool u_clip; uniform sampler2D u_noiseTexture; in vec2 v_imageUV; @@ -431,7 +431,7 @@ void main() { vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); float opacity = overlayAlpha + bgOpacity * (1. - overlayAlpha); - if (!u_background && u_isImage) { + if (u_clip && u_isImage) { color *= imageFootprint; opacity *= imageFootprint; } @@ -462,7 +462,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_seed: number; u_blending: number; u_distortion: number; - u_background: boolean; + u_clip: boolean; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { @@ -485,5 +485,5 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara seed?: number; blending?: number; distortion?: number; - background?: boolean; + clip?: boolean; } From badf489e3a93e27e31b66d29e644da42410fbeaa Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 11:08:41 +0200 Subject: [PATCH 149/186] distortion inverted --- packages/shaders-react/src/shaders/paper-texture.tsx | 8 ++++---- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index dac822711..e84e31696 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -26,7 +26,7 @@ export const defaultPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: 0.5, + distortion: -0.5, fit: 'contain', scale: 0.9, speed: 0, @@ -56,7 +56,7 @@ export const gridPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: 0.5, + distortion: -0.5, fit: 'contain', scale: 0.9, speed: 0, @@ -86,7 +86,7 @@ export const spreadPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: 0.38, + distortion: -0.38, fit: 'contain', scale: 0.9, speed: 0, @@ -116,7 +116,7 @@ export const coloredPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: 0.5, + distortion: -0.5, fit: 'contain', scale: 0.9, speed: 0, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 05b8985f4..0610174e7 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -388,8 +388,8 @@ void main() { vec3 shadowColor = u_colorShadow.rgb; float shadowOpacity = u_colorShadow.a; - imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); - imageUV += u_distortion * vec2(xDistortion, -yShift); + imageUV = .5 + fromCenter * (1. - u_distortion * scaleDistortion); + imageUV -= u_distortion * vec2(xDistortion, -yShift); vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); From 9d3a3bd58fb60caf5ef592f8f0aa241827a4efd9 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 13:24:08 +0200 Subject: [PATCH 150/186] creases -> folds, folds -> crumples --- docs/src/app/(shaders)/paper-texture/page.tsx | 14 +- docs/src/shader-defs/paper-texture-def.ts | 46 +++--- .../src/shaders/paper-texture.tsx | 82 +++++------ packages/shaders/src/shaders/paper-texture.ts | 136 +++++++++--------- 4 files changed, 139 insertions(+), 139 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 984cc0a2f..2f00d51cd 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -75,13 +75,13 @@ const PaperTextureWithControls = () => { roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 301 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 310 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 311 }, - folds: { value: defaults.folds, min: 0, max: 1, order: 320 }, - foldCount: { value: defaults.foldCount, min: 1, max: paperTextureMeta.maxFoldCount, step: 1, order: 321 }, - creases: { value: defaults.creases, min: 0, max: 1, order: 330 }, - creaseSizeX: { value: defaults.creaseSizeX, min: 0, max: 1, order: 331 }, - creaseOffsetX: { value: defaults.creaseOffsetX, min: 0, max: 1, order: 332 }, - creaseSizeY: { value: defaults.creaseSizeY, min: 0, max: 1, order: 333 }, - creaseOffsetY: { value: defaults.creaseOffsetY, min: 0, max: 1, order: 334 }, + crumples: { value: defaults.crumples, min: 0, max: 1, order: 320 }, + crumpleCount: { value: defaults.crumpleCount, min: 1, max: paperTextureMeta.maxCrumpleCount, step: 1, order: 321 }, + folds: { value: defaults.folds, min: 0, max: 1, order: 330 }, + foldSizeX: { value: defaults.foldSizeX, min: 0, max: 1, order: 331 }, + foldOffsetX: { value: defaults.foldOffsetX, min: 0, max: 1, order: 332 }, + foldSizeY: { value: defaults.foldSizeY, min: 0, max: 1, order: 333 }, + foldOffsetY: { value: defaults.foldOffsetY, min: 0, max: 1, order: 334 }, drops: { value: defaults.drops, min: 0, max: 1, order: 340 }, blending: { value: defaults.blending, min: 0, max: 1, order: 350 }, distortion: { value: defaults.distortion, min: -1, max: 1, order: 351 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 6f5c5f38d..ade969b1b 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -26,7 +26,7 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorShadow, isColor: true, - description: 'The color used for folds, creases, grain and speckles, blends into the image', + description: 'The color used for crumples, folds, grain and speckles, blends into the image', }, { name: 'blending', @@ -85,61 +85,61 @@ export const paperTextureDef: ShaderDef = { description: 'Scale of the fiber noise (needs fiber > 0)', }, { - name: 'folds', + name: 'crumples', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.folds, + defaultValue: defaultParams.crumples, description: 'Depth of the centered, irregular field of facets across the sheet', }, { - name: 'foldCount', + name: 'crumpleCount', type: 'number', min: 1, max: 15, step: 1, - defaultValue: defaultParams.foldCount, - description: 'Number of folds (15 max), needs folds > 0', + defaultValue: defaultParams.crumpleCount, + description: 'Number of crumples (15 max), needs crumples > 0', }, { - name: 'creases', + name: 'folds', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creases, - description: 'Depth of the straight creases, alternating between ridges and valleys', + defaultValue: defaultParams.folds, + description: 'Depth of the straight folds, alternating between ridges and valleys', }, { - name: 'creaseSizeX', + name: 'foldSizeX', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creaseSizeX, - description: 'Size of the vertical creases (needs creases > 0)', + defaultValue: defaultParams.foldSizeX, + description: 'Size of the vertical folds (needs folds > 0)', }, { - name: 'creaseSizeY', + name: 'foldSizeY', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creaseSizeY, - description: 'Size of the horizontal creases (needs creases > 0)', + defaultValue: defaultParams.foldSizeY, + description: 'Size of the horizontal folds (needs folds > 0)', }, { - name: 'creaseOffsetX', + name: 'foldOffsetX', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creaseOffsetX, - description: 'Shifts the vertical creases across the surface (needs creases > 0)', + defaultValue: defaultParams.foldOffsetX, + description: 'Shifts the vertical folds across the surface (needs folds > 0)', }, { - name: 'creaseOffsetY', + name: 'foldOffsetY', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.creaseOffsetY, - description: 'Shifts the horizontal creases across the surface (needs creases > 0)', + defaultValue: defaultParams.foldOffsetY, + description: 'Shifts the horizontal folds across the surface (needs folds > 0)', }, { name: 'angle', @@ -148,7 +148,7 @@ export const paperTextureDef: ShaderDef = { max: 360, defaultValue: defaultParams.angle, description: - 'Direction the surface is lit from, clockwise from the top of the canvas, needs folds or creases > 0', + 'Direction the surface is lit from, clockwise from the top of the canvas, needs crumples or folds > 0', }, { name: 'drops', @@ -164,7 +164,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1000, defaultValue: defaultParams.seed, - description: 'Seed applied to folds and drops', + description: 'Seed applied to crumples and drops', }, ...staticImageCommonParams, ], diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e84e31696..e9c6450bc 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -15,7 +15,7 @@ import { export interface PaperTextureProps extends ShaderComponentProps, PaperTextureParams { /** @deprecated use `fiberSize` instead */ fiberScale?: number; - /** @deprecated use `foldCount` instead */ + /** @deprecated use `crumpleCount` instead */ foldsNumber?: number; } @@ -37,13 +37,13 @@ export const defaultPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.65, fiberSize: 0.5, - folds: 0.5, - foldCount: 6, - creases: 0, - creaseSizeX: 0.6, - creaseSizeY: 0.89, - creaseOffsetX: 0, - creaseOffsetY: 0, + crumples: 0.5, + crumpleCount: 6, + folds: 0, + foldSizeX: 0.6, + foldSizeY: 0.89, + foldOffsetX: 0, + foldOffsetY: 0, angle: 300, drops: 0.7, seed: 234, @@ -67,13 +67,13 @@ export const gridPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.32, fiberSize: 0.5, - folds: 0, - foldCount: 12, - creases: 0.25, - creaseSizeX: 0.6, - creaseSizeY: 0.89, - creaseOffsetX: 0.59, - creaseOffsetY: 1, + crumples: 0, + crumpleCount: 12, + folds: 0.25, + foldSizeX: 0.6, + foldSizeY: 0.89, + foldOffsetX: 0.59, + foldOffsetY: 1, angle: 140, drops: 0, seed: 613, @@ -97,13 +97,13 @@ export const spreadPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0.62, fiberSize: 0.58, - folds: 0.25, - foldCount: 9, - creases: 0.55, - creaseSizeX: 0, - creaseSizeY: 0.89, - creaseOffsetX: 0, - creaseOffsetY: 0, + crumples: 0.25, + crumpleCount: 9, + folds: 0.55, + foldSizeX: 0, + foldSizeY: 0.89, + foldOffsetX: 0, + foldOffsetY: 0, angle: 88, drops: 0, seed: 563, @@ -127,13 +127,13 @@ export const coloredPreset: PaperTexturePreset = { roughnessSize: 0.08, fiber: 0.52, fiberSize: 1, - folds: 0, - foldCount: 12, - creases: 0.43, - creaseSizeX: 1, - creaseSizeY: 0.89, - creaseOffsetX: 0.62, - creaseOffsetY: 0, + crumples: 0, + crumpleCount: 12, + folds: 0.43, + foldSizeX: 1, + foldSizeY: 0.89, + foldOffsetX: 0.62, + foldOffsetY: 0, angle: 316, drops: 1, seed: 476, @@ -158,12 +158,12 @@ export const PaperTexture: React.FC = memo(function PaperText roughness = defaultPreset.params.roughness, roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, + crumples = defaultPreset.params.crumples, folds = defaultPreset.params.folds, - creases = defaultPreset.params.creases, - creaseSizeX = defaultPreset.params.creaseSizeX, - creaseSizeY = defaultPreset.params.creaseSizeY, - creaseOffsetX = defaultPreset.params.creaseOffsetX, - creaseOffsetY = defaultPreset.params.creaseOffsetY, + foldSizeX = defaultPreset.params.foldSizeX, + foldSizeY = defaultPreset.params.foldSizeY, + foldOffsetX = defaultPreset.params.foldOffsetX, + foldOffsetY = defaultPreset.params.foldOffsetY, angle = defaultPreset.params.angle, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, @@ -175,7 +175,7 @@ export const PaperTexture: React.FC = memo(function PaperText fiberScale, fiberSize = fiberScale === undefined ? defaultPreset.params.fiberSize : 0.2 / fiberScale, foldsNumber, - foldCount = foldsNumber === undefined ? defaultPreset.params.foldCount : foldsNumber, + crumpleCount = foldsNumber === undefined ? defaultPreset.params.crumpleCount : foldsNumber, // Sizing props fit = defaultPreset.params.fit, @@ -201,14 +201,14 @@ export const PaperTexture: React.FC = memo(function PaperText u_roughnessSize: roughnessSize, u_fiber: fiber, u_fiberSize: fiberSize, - u_foldCount: foldCount, - u_creaseSizeX: creaseSizeX, - u_creaseSizeY: creaseSizeY, - u_creaseOffsetX: creaseOffsetX, - u_creaseOffsetY: creaseOffsetY, + u_crumpleCount: crumpleCount, + u_foldSizeX: foldSizeX, + u_foldSizeY: foldSizeY, + u_foldOffsetX: foldOffsetX, + u_foldOffsetY: foldOffsetY, u_angle: angle, + u_crumples: crumples, u_folds: folds, - u_creases: creases, u_drops: drops, u_seed: seed, u_blending: blending, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0610174e7..560a74f54 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -3,7 +3,7 @@ import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-si import { declarePI, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { - maxFoldCount: 15, + maxCrumpleCount: 15, } as const; /** @@ -14,22 +14,22 @@ export const paperTextureMeta = { * - u_image (sampler2D): Optional source image texture * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image - * - u_colorShadow (vec4): Color used for folds, creases, grain and speckles, blends into the image, in RGBA + * - u_colorShadow (vec4): Color used for crumples, folds, grain and speckles, blends into the image, in RGBA * - u_colorBack (vec4): Color behind the image, usually light, in RGBA * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) * - u_fiber (float): Curly fiber noise, simulating real paper (0 to 1) * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) - * - u_folds (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) - * - u_foldCount (float): Number of folds, needs u_folds > 0 (1 to 15) - * - u_creases (float): Depth of the straight creases, alternating between ridges and valleys (0 to 1) - * - u_creaseSizeX (float): Size of the vertical creases, needs u_creases > 0 (0 to 1) - * - u_creaseSizeY (float): Size of the horizontal creases, needs u_creases > 0 (0 to 1) - * - u_creaseOffsetX (float): Shifts the vertical creases across the surface, needs u_creases > 0 (0 to 1) - * - u_creaseOffsetY (float): Shifts the horizontal creases across the surface, needs u_creases > 0 (0 to 1) - * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_folds or u_creases > 0 (0 to 360) + * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) + * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) + * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) + * - u_foldSizeX (float): Size of the vertical folds, needs u_folds > 0 (0 to 1) + * - u_foldSizeY (float): Size of the horizontal folds, needs u_folds > 0 (0 to 1) + * - u_foldOffsetX (float): Shifts the vertical folds across the surface, needs u_folds > 0 (0 to 1) + * - u_foldOffsetY (float): Shifts the horizontal folds across the surface, needs u_folds > 0 (0 to 1) + * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples or u_folds > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) - * - u_seed (float): Seed applied to folds and drops (0 to 1000) + * - u_seed (float): Seed applied to crumples and drops (0 to 1000) * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Hides the paper texture outside the distorted image frame, needs image @@ -66,13 +66,13 @@ uniform float u_imageAspectRatio; uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; +uniform float u_crumples; +uniform float u_crumpleCount; uniform float u_folds; -uniform float u_foldCount; -uniform float u_creases; -uniform float u_creaseSizeX; -uniform float u_creaseSizeY; -uniform float u_creaseOffsetX; -uniform float u_creaseOffsetY; +uniform float u_foldSizeX; +uniform float u_foldSizeY; +uniform float u_foldOffsetX; +uniform float u_foldOffsetY; uniform float u_angle; uniform float u_drops; uniform float u_seed; @@ -85,14 +85,14 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; -#define CREASE_WEAR .6 +#define FOLD_WEAR .6 #define LIFT_SHARPNESS 4. #define GRAIN_DRAG .2 #define TILT_FLOOR .9 #define RADIAL_FALLOFF .7 #define FACET_CURVE 1.5 -#define FOLD_LIT_GAIN .35 +#define CRUMPLE_LIT_GAIN .35 #define SHADING_SCALE 1. float getUvFrame(vec2 uv, float blur) { @@ -231,12 +231,12 @@ vec2 getCellTilt(float idx, float radius) { return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); } -vec4 getFolds(vec2 uv) { +vec4 getCrumples(vec2 uv) { float near = 9., nearB = 9.; float idx = 0., rad = 0.; vec2 nearP = vec2(0.), nearPb = vec2(0.); - for (int i = 0; i < ${paperTextureMeta.maxFoldCount}; i++) { - if (float(i) >= floor(u_foldCount + .5)) break; + for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { + if (float(i) >= floor(u_crumpleCount + .5)) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; @@ -263,12 +263,12 @@ vec4 getFolds(vec2 uv) { return vec4(getCellTilt(idx, rad) + FACET_CURVE * (1. - edge) * edgeGrad, .2 * l, edge); } -void getCreases(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { +void getFolds(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { vec2 g = (coord - 1.) * count + .5 * offset; - vec2 creaseIdx = floor(g); + vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; float foldWidth = .5; - vec2 parity = (1. - 2. * mod(creaseIdx, 2.)); + vec2 parity = (1. - 2. * mod(foldIdx, 2.)); slope = sign(dx) * (1. - smoothstep(0., foldWidth, abs(dx))) * parity; dark = smoothstep(0., foldWidth * .5, abs(dx)); lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(LIFT_SHARPNESS)) * parity; @@ -304,48 +304,48 @@ void main() { vec2 lightDir = vec2(sin(lightRad), -cos(lightRad)); vec2 relief = vec2(0.); float reliefAmount = 0.; - float creaseInk = 1.; - float creaseWear = 0.; - vec2 foldFlow = vec2(0.); - - if (u_folds > 0.) { - float foldRad = 4. * u_seed; - float foldCos = cos(foldRad), foldSin = sin(foldRad); - vec2 foldsUV = mat2(foldCos, foldSin, -foldSin, foldCos) * (patternUV * .18) + + float foldInk = 1.; + float foldWear = 0.; + vec2 crumpleFlow = vec2(0.); + + if (u_crumples > 0.) { + float crumpleRad = 4. * u_seed; + float crumpleCos = cos(crumpleRad), crumpleSin = sin(crumpleRad); + vec2 crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .18) + .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); - vec4 folds = getFolds(foldsUV); + vec4 crumples = getCrumples(crumplesUV); - vec2 foldTilt = .5 * u_folds * folds.xy; - foldTilt -= (1. - FOLD_LIT_GAIN) * max(dot(foldTilt, lightDir), 0.) * lightDir; - relief += foldTilt; - reliefAmount += .5 * u_folds; - foldFlow = folds.w * foldTilt; + vec2 crumpleTilt = .5 * u_crumples * crumples.xy; + crumpleTilt -= (1. - CRUMPLE_LIT_GAIN) * max(dot(crumpleTilt, lightDir), 0.) * lightDir; + relief += crumpleTilt; + reliefAmount += .5 * u_crumples; + crumpleFlow = crumples.w * crumpleTilt; - scaleDistortion = .15 * clamp(5. * folds.z, 0., 1.) * u_folds; + scaleDistortion = .15 * clamp(5. * crumples.z, 0., 1.) * u_crumples; } - if (u_creases > 0.) { - vec2 uv = 1. + patternUV * .2 + .03 * foldFlow; + if (u_folds > 0.) { + vec2 uv = 1. + patternUV * .2 + .03 * crumpleFlow; - // x drives the creases running vertically, y the ones running across them. + // x drives the folds running vertically, y the ones running across them. vec2 slope, dark, lift; - getCreases(uv, vec2(1. - 2. * u_creaseOffsetX, 1. - 2. * u_creaseOffsetY), - vec2(mix(3., .5, u_creaseSizeX), mix(3., .5, u_creaseSizeY)), slope, dark, lift); + getFolds(uv, vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY), + vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), slope, dark, lift); - relief += u_creases * slope; - reliefAmount += 1. * u_creases; + relief += u_folds * slope; + reliefAmount += 1. * u_folds; vec2 ink = mix(vec2(.9), vec2(1.), dark); - creaseInk *= ink.x * ink.y; - creaseWear = max(creaseWear, max(1. - dark.x, 1. - dark.y) * u_creases); - vec2 dropMask = mix(vec2(1.), dark, u_creases); + foldInk *= ink.x * ink.y; + foldWear = max(foldWear, max(1. - dark.x, 1. - dark.y) * u_folds); + vec2 dropMask = mix(vec2(1.), dark, u_folds); drops *= dropMask.x * dropMask.y; - yShift += .022 * u_creases * lift.x * abs(lightDir.x); + yShift += .022 * u_folds * lift.x * abs(lightDir.x); patternUV.y += yShift; float xFan = 2. * (imageUV.x - .5) * abs(lightDir.y); - xDistortion -= .02 * u_creases * lift.y * xFan; - patternUV.x -= .022 * u_creases * lift.y * xFan; + xDistortion -= .02 * u_folds * lift.y * xFan; + patternUV.x -= .022 * u_folds * lift.y * xFan; } float unlit = SHADING_SCALE * cos(grazing); @@ -355,12 +355,12 @@ void main() { float slope = clamp(dot(relief, lightDir), -1.2, 1.2); lit = SHADING_SCALE * max(cos(slope + grazing), 0.); - pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * creaseInk; + pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk; } - patternUV += GRAIN_DRAG * foldFlow; + patternUV += GRAIN_DRAG * crumpleFlow; - float detailGain = (1. + CREASE_WEAR * creaseWear); + float detailGain = (1. + FOLD_WEAR * foldWear); if (u_roughness > 0.) { float roughness = getRoughness(200. * patternUV); @@ -450,13 +450,13 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_roughnessSize: number; u_fiber: number; u_fiberSize: number; + u_crumples: number; + u_crumpleCount: number; u_folds: number; - u_foldCount: number; - u_creases: number; - u_creaseSizeX: number; - u_creaseSizeY: number; - u_creaseOffsetX: number; - u_creaseOffsetY: number; + u_foldSizeX: number; + u_foldSizeY: number; + u_foldOffsetX: number; + u_foldOffsetY: number; u_angle: number; u_drops: number; u_seed: number; @@ -473,13 +473,13 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara roughnessSize?: number; fiber?: number; fiberSize?: number; + crumples?: number; + crumpleCount?: number; folds?: number; - foldCount?: number; - creases?: number; - creaseSizeX?: number; - creaseSizeY?: number; - creaseOffsetX?: number; - creaseOffsetY?: number; + foldSizeX?: number; + foldSizeY?: number; + foldOffsetX?: number; + foldOffsetY?: number; angle?: number; drops?: number; seed?: number; From f4bc5f95195e6c4ecd069e7168db8a437cc20c91 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 13:32:40 +0200 Subject: [PATCH 151/186] 3-color scheme --- docs/src/app/(shaders)/paper-texture/page.tsx | 3 +- docs/src/shader-defs/paper-texture-def.ts | 11 +++++- .../src/shaders/paper-texture.tsx | 18 ++++++--- packages/shaders/src/shaders/paper-texture.ts | 38 ++++++++++++------- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 2f00d51cd..1762723b2 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -68,7 +68,8 @@ const PaperTextureWithControls = () => { ); return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorShadow: { value: toHsla(defaults.colorShadow), order: 101 }, + colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + colorShadow: { value: toHsla(defaults.colorShadow), order: 102 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, angle: { value: defaults.angle, min: 0, max: 360, order: 201 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 300 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index ade969b1b..9ae65ac34 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -19,7 +19,14 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBack, isColor: true, - description: 'The color behind the image, usually light', + description: 'The color of the bottom layer, behind the sheet; visible where clip cuts the sheet away', + }, + { + name: 'colorFront', + type: 'string', + defaultValue: defaultParams.colorFront, + isColor: true, + description: 'The color of the paper sheet, usually light; printed into the image by blending', }, { name: 'colorShadow', @@ -35,7 +42,7 @@ export const paperTextureDef: ShaderDef = { max: 1, defaultValue: defaultParams.blending, description: - 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the pattern), needs image', + 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the pattern and thinned toward colorFront), needs image', }, { name: 'distortion', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e9c6450bc..53bb1a7a7 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -31,8 +31,9 @@ export const defaultPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, + colorBack: '#00000000', + colorFront: '#ffffff', colorShadow: '#b3b3b3', - colorBack: '#ffffff', roughness: 0, roughnessSize: 0.25, fiber: 0.65, @@ -61,8 +62,9 @@ export const gridPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, + colorBack: '#00000000', + colorFront: '#ffffff', colorShadow: '#605b52', - colorBack: '#ffffff', roughness: 0.16, roughnessSize: 0.25, fiber: 0.32, @@ -91,8 +93,9 @@ export const spreadPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, + colorBack: '#00000000', + colorFront: '#ffffff', colorShadow: '#c2c2c2', - colorBack: '#ffffff', roughness: 0.16, roughnessSize: 0.25, fiber: 0.62, @@ -121,8 +124,9 @@ export const coloredPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, + colorBack: '#00000000', + colorFront: '#f5b689', colorShadow: '#7654c9', - colorBack: '#f5b689', roughness: 0, roughnessSize: 0.08, fiber: 0.52, @@ -152,8 +156,9 @@ export const PaperTexture: React.FC = memo(function PaperText // Own props speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, - colorShadow = defaultPreset.params.colorShadow, colorBack = defaultPreset.params.colorBack, + colorFront = defaultPreset.params.colorFront, + colorShadow = defaultPreset.params.colorShadow, image = '', roughness = defaultPreset.params.roughness, roughnessSize = defaultPreset.params.roughnessSize, @@ -195,8 +200,9 @@ export const PaperTexture: React.FC = memo(function PaperText // Own uniforms u_image: image, u_isImage: Boolean(image), - u_colorShadow: getShaderColorFromString(colorShadow), u_colorBack: getShaderColorFromString(colorBack), + u_colorFront: getShaderColorFromString(colorFront), + u_colorShadow: getShaderColorFromString(colorShadow), u_roughness: roughness, u_roughnessSize: roughnessSize, u_fiber: fiber, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 560a74f54..d9207ae38 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -14,8 +14,9 @@ export const paperTextureMeta = { * - u_image (sampler2D): Optional source image texture * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image + * - u_colorBack (vec4): Color of the bottom layer, behind the sheet; visible where u_clip cuts the sheet away, in RGBA + * - u_colorFront (vec4): Color of the paper sheet, usually light; printed into the image by u_blending, in RGBA * - u_colorShadow (vec4): Color used for crumples, folds, grain and speckles, blends into the image, in RGBA - * - u_colorBack (vec4): Color behind the image, usually light, in RGBA * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) * - u_fiber (float): Curly fiber noise, simulating real paper (0 to 1) @@ -30,7 +31,7 @@ export const paperTextureMeta = { * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples or u_folds > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to crumples and drops (0 to 1000) - * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a grayscale paper-texture ink (toned by colorShadow) and thinned so the background reads through, needs image (0 to 1) + * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a paper-texture ink (colorFront toned by colorShadow) and thinned toward colorFront so the sheet reads through, needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Hides the paper texture outside the distorted image frame, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture @@ -56,8 +57,9 @@ export const paperTextureMeta = { export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; -uniform vec4 u_colorShadow; uniform vec4 u_colorBack; +uniform vec4 u_colorFront; +uniform vec4 u_colorShadow; uniform sampler2D u_image; uniform bool u_isImage; @@ -382,12 +384,17 @@ void main() { xDistortion += .03 * drops; } - vec3 bgColor = u_colorBack.rgb; - float bgOpacity = u_colorBack.a; + vec3 backColor = u_colorBack.rgb; + float backOpacity = u_colorBack.a; + + vec3 frontColor = u_colorFront.rgb; + float frontOpacity = u_colorFront.a; vec3 shadowColor = u_colorShadow.rgb; float shadowOpacity = u_colorShadow.a; + vec3 paper = mix(vec3(1.), frontColor, frontOpacity); + imageUV = .5 + fromCenter * (1. - u_distortion * scaleDistortion); imageUV -= u_distortion * vec2(xDistortion, -yShift); vec2 dc = imageUV - .5; @@ -412,12 +419,12 @@ void main() { float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - vec3 ink = mix(vec3(1.), shadowColor, patternAlpha); + vec3 ink = mix(paper, shadowColor, patternAlpha); pic = blendMultiply(image.rgb, ink, u_blending); float darkDampen = 1. - lum; float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); - pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); + pic = mix(pic, paper, .4 * pow(dampen, 2. + 3. * pattern)); pic = clamp(pic, 0., 1.); imageFootprint = frame * image.a; @@ -428,14 +435,17 @@ void main() { vec3 overlay = pic * imageAlpha + shadowColor * patternAlpha * (1. - imageAlpha); float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); - vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); - float opacity = overlayAlpha + bgOpacity * (1. - overlayAlpha); + vec3 sheet = overlay + frontColor * frontOpacity * (1. - overlayAlpha); + float sheetAlpha = overlayAlpha + frontOpacity * (1. - overlayAlpha); if (u_clip && u_isImage) { - color *= imageFootprint; - opacity *= imageFootprint; + sheet *= imageFootprint; + sheetAlpha *= imageFootprint; } + vec3 color = sheet + backColor * backOpacity * (1. - sheetAlpha); + float opacity = sheetAlpha + backOpacity * (1. - sheetAlpha); + fragColor = vec4(color, opacity); } `; @@ -444,8 +454,9 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_image: HTMLImageElement | string | undefined; u_isImage: boolean; u_noiseTexture?: HTMLImageElement; - u_colorShadow: [number, number, number, number]; u_colorBack: [number, number, number, number]; + u_colorFront: [number, number, number, number]; + u_colorShadow: [number, number, number, number]; u_roughness: number; u_roughnessSize: number; u_fiber: number; @@ -467,8 +478,9 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string; - colorShadow?: string; colorBack?: string; + colorFront?: string; + colorShadow?: string; roughness?: number; roughnessSize?: number; fiber?: number; From 2c3d563b1fa5eb42e282ae0993bcdadc0f54851f Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 13:35:02 +0200 Subject: [PATCH 152/186] colorFront -> colorBase --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 8 ++++---- .../src/shaders/paper-texture.tsx | 12 +++++------ packages/shaders/src/shaders/paper-texture.ts | 20 +++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 1762723b2..ed7527577 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -68,7 +68,7 @@ const PaperTextureWithControls = () => { ); return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorFront: { value: toHsla(defaults.colorFront), order: 101 }, + colorBase: { value: toHsla(defaults.colorBase), order: 101 }, colorShadow: { value: toHsla(defaults.colorShadow), order: 102 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, angle: { value: defaults.angle, min: 0, max: 360, order: 201 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 9ae65ac34..5e8726e45 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -22,11 +22,11 @@ export const paperTextureDef: ShaderDef = { description: 'The color of the bottom layer, behind the sheet; visible where clip cuts the sheet away', }, { - name: 'colorFront', + name: 'colorBase', type: 'string', - defaultValue: defaultParams.colorFront, + defaultValue: defaultParams.colorBase, isColor: true, - description: 'The color of the paper sheet, usually light; printed into the image by blending', + description: 'The color of the paper sheet, printed into the image by blending', }, { name: 'colorShadow', @@ -42,7 +42,7 @@ export const paperTextureDef: ShaderDef = { max: 1, defaultValue: defaultParams.blending, description: - 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the pattern and thinned toward colorFront), needs image', + 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the pattern and thinned toward colorBase), needs image', }, { name: 'distortion', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 53bb1a7a7..66811c01f 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -32,7 +32,7 @@ export const defaultPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#00000000', - colorFront: '#ffffff', + colorBase: '#ffffff', colorShadow: '#b3b3b3', roughness: 0, roughnessSize: 0.25, @@ -63,7 +63,7 @@ export const gridPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#00000000', - colorFront: '#ffffff', + colorBase: '#ffffff', colorShadow: '#605b52', roughness: 0.16, roughnessSize: 0.25, @@ -94,7 +94,7 @@ export const spreadPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#00000000', - colorFront: '#ffffff', + colorBase: '#ffffff', colorShadow: '#c2c2c2', roughness: 0.16, roughnessSize: 0.25, @@ -125,7 +125,7 @@ export const coloredPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#00000000', - colorFront: '#f5b689', + colorBase: '#f5b689', colorShadow: '#7654c9', roughness: 0, roughnessSize: 0.08, @@ -157,7 +157,7 @@ export const PaperTexture: React.FC = memo(function PaperText speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, colorBack = defaultPreset.params.colorBack, - colorFront = defaultPreset.params.colorFront, + colorBase = defaultPreset.params.colorBase, colorShadow = defaultPreset.params.colorShadow, image = '', roughness = defaultPreset.params.roughness, @@ -201,7 +201,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_image: image, u_isImage: Boolean(image), u_colorBack: getShaderColorFromString(colorBack), - u_colorFront: getShaderColorFromString(colorFront), + u_colorBase: getShaderColorFromString(colorBase), u_colorShadow: getShaderColorFromString(colorShadow), u_roughness: roughness, u_roughnessSize: roughnessSize, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index d9207ae38..7b56507ad 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -15,7 +15,7 @@ export const paperTextureMeta = { * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image * - u_colorBack (vec4): Color of the bottom layer, behind the sheet; visible where u_clip cuts the sheet away, in RGBA - * - u_colorFront (vec4): Color of the paper sheet, usually light; printed into the image by u_blending, in RGBA + * - u_colorBase (vec4): Color of the paper sheet, usually light; printed into the image by u_blending, in RGBA * - u_colorShadow (vec4): Color used for crumples, folds, grain and speckles, blends into the image, in RGBA * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) @@ -31,7 +31,7 @@ export const paperTextureMeta = { * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples or u_folds > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to crumples and drops (0 to 1000) - * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a paper-texture ink (colorFront toned by colorShadow) and thinned toward colorFront so the sheet reads through, needs image (0 to 1) + * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a paper-texture ink (colorBase toned by colorShadow) and thinned toward colorBase so the sheet reads through, needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Hides the paper texture outside the distorted image frame, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture @@ -58,7 +58,7 @@ export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; uniform vec4 u_colorBack; -uniform vec4 u_colorFront; +uniform vec4 u_colorBase; uniform vec4 u_colorShadow; uniform sampler2D u_image; @@ -387,13 +387,13 @@ void main() { vec3 backColor = u_colorBack.rgb; float backOpacity = u_colorBack.a; - vec3 frontColor = u_colorFront.rgb; - float frontOpacity = u_colorFront.a; + vec3 baseColor = u_colorBase.rgb; + float baseOpacity = u_colorBase.a; vec3 shadowColor = u_colorShadow.rgb; float shadowOpacity = u_colorShadow.a; - vec3 paper = mix(vec3(1.), frontColor, frontOpacity); + vec3 paper = mix(vec3(1.), baseColor, baseOpacity); imageUV = .5 + fromCenter * (1. - u_distortion * scaleDistortion); imageUV -= u_distortion * vec2(xDistortion, -yShift); @@ -435,8 +435,8 @@ void main() { vec3 overlay = pic * imageAlpha + shadowColor * patternAlpha * (1. - imageAlpha); float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); - vec3 sheet = overlay + frontColor * frontOpacity * (1. - overlayAlpha); - float sheetAlpha = overlayAlpha + frontOpacity * (1. - overlayAlpha); + vec3 sheet = overlay + baseColor * baseOpacity * (1. - overlayAlpha); + float sheetAlpha = overlayAlpha + baseOpacity * (1. - overlayAlpha); if (u_clip && u_isImage) { sheet *= imageFootprint; @@ -455,7 +455,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_isImage: boolean; u_noiseTexture?: HTMLImageElement; u_colorBack: [number, number, number, number]; - u_colorFront: [number, number, number, number]; + u_colorBase: [number, number, number, number]; u_colorShadow: [number, number, number, number]; u_roughness: number; u_roughnessSize: number; @@ -479,7 +479,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string; colorBack?: string; - colorFront?: string; + colorBase?: string; colorShadow?: string; roughness?: number; roughnessSize?: number; From a6dc36652fa3e3fecb359f3d182647ce75f740bb Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 14:23:21 +0200 Subject: [PATCH 153/186] color management --- docs/src/shader-defs/paper-texture-def.ts | 6 +- .../src/shaders/paper-texture.tsx | 12 ++-- packages/shaders/src/shaders/paper-texture.ts | 61 ++++++++----------- 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 5e8726e45..070b03cdd 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -26,7 +26,7 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBase, isColor: true, - description: 'The color of the paper sheet, printed into the image by blending', + description: 'The color of the paper sheet, usually light; multiplied into the image by blending', }, { name: 'colorShadow', @@ -42,7 +42,7 @@ export const paperTextureDef: ShaderDef = { max: 1, defaultValue: defaultParams.blending, description: - 'How much the image is printed into the paper (0 = exact image, 1 = image multiplied with the pattern and thinned toward colorBase), needs image', + 'Amount of image-to-paper blending (0 = original image color, 1 = image multiplied with the paper), needs image', }, { name: 'distortion', @@ -57,7 +57,7 @@ export const paperTextureDef: ShaderDef = { name: 'clip', type: 'boolean', defaultValue: defaultParams.clip, - description: 'Hides the paper texture outside the distorted image frame (needs image)', + description: 'Cuts the paper sheet to the distorted image frame, revealing colorBack outside it (needs image)', }, { name: 'roughness', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 66811c01f..35ab4950b 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -26,13 +26,13 @@ export const defaultPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: -0.5, + distortion: 0.75, fit: 'contain', scale: 0.9, speed: 0, frame: 0, - colorBack: '#00000000', - colorBase: '#ffffff', + colorBack: '#d3d2ab', + colorBase: '#ffffffb5', colorShadow: '#b3b3b3', roughness: 0, roughnessSize: 0.25, @@ -62,7 +62,7 @@ export const gridPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorBack: '#00000000', + colorBack: '#ffffff', colorBase: '#ffffff', colorShadow: '#605b52', roughness: 0.16, @@ -93,7 +93,7 @@ export const spreadPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorBack: '#00000000', + colorBack: '#d4d3ab00', colorBase: '#ffffff', colorShadow: '#c2c2c2', roughness: 0.16, @@ -124,7 +124,7 @@ export const coloredPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorBack: '#00000000', + colorBack: '#d3d2ab', colorBase: '#f5b689', colorShadow: '#7654c9', roughness: 0, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 7b56507ad..93db20c52 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -15,7 +15,7 @@ export const paperTextureMeta = { * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image * - u_colorBack (vec4): Color of the bottom layer, behind the sheet; visible where u_clip cuts the sheet away, in RGBA - * - u_colorBase (vec4): Color of the paper sheet, usually light; printed into the image by u_blending, in RGBA + * - u_colorBase (vec4): Color of the paper sheet, usually light; multiplied into the image by u_blending, in RGBA * - u_colorShadow (vec4): Color used for crumples, folds, grain and speckles, blends into the image, in RGBA * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) @@ -31,9 +31,9 @@ export const paperTextureMeta = { * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples or u_folds > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to crumples and drops (0 to 1000) - * - u_blending (float): How much the image is printed into the paper; 0 = exact image, 1 = image multiplied with a paper-texture ink (colorBase toned by colorShadow) and thinned toward colorBase so the sheet reads through, needs image (0 to 1) + * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorBase toned by colorShadow), needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) - * - u_clip (bool): Hides the paper texture outside the distorted image frame, needs image + * - u_clip (bool): Cuts the paper sheet to the distorted image frame, revealing u_colorBack outside it, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -384,31 +384,33 @@ void main() { xDistortion += .03 * drops; } - vec3 backColor = u_colorBack.rgb; + vec3 backColor = u_colorBack.rgb * u_colorBack.a; float backOpacity = u_colorBack.a; - vec3 baseColor = u_colorBase.rgb; + vec3 baseColor = u_colorBase.rgb * u_colorBase.a; float baseOpacity = u_colorBase.a; - vec3 shadowColor = u_colorShadow.rgb; + vec3 shadowColor = u_colorShadow.rgb * u_colorShadow.a; float shadowOpacity = u_colorShadow.a; - vec3 paper = mix(vec3(1.), baseColor, baseOpacity); - imageUV = .5 + fromCenter * (1. - u_distortion * scaleDistortion); imageUV -= u_distortion * vec2(xDistortion, -yShift); vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - float patternAlpha = clamp(shadowOpacity * pattern, 0., 1.); + pattern = clamp(pattern, 0., 1.); + + vec3 color = shadowColor * pattern; + float opacity = shadowOpacity * pattern; - vec3 pic = vec3(0.); - float imageFootprint = 0.; + color += baseColor * (1. - opacity); + opacity += baseOpacity * (1. - opacity); if (u_isImage) { - float frame = getUvFrame(imageUV, .005); + float frame = getUvFrame(imageUV, .001); vec4 image = texture(u_image, imageUV); + frame *= image.a; float maxC = max(max(image.r, image.g), image.b); float minC = min(min(image.r, image.g), image.b); @@ -416,35 +418,24 @@ void main() { float midC = image.r + image.g + image.b - maxC - minC; float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; float satDampen = sat * (1. - .5 * secondaryness); - - float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - - vec3 ink = mix(paper, shadowColor, patternAlpha); - pic = blendMultiply(image.rgb, ink, u_blending); - - float darkDampen = 1. - lum; + float darkDampen = 1. - dot(vec3(.2126, .7152, .0722), image.rgb); float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); - pic = mix(pic, paper, .4 * pow(dampen, 2. + 3. * pattern)); - pic = clamp(pic, 0., 1.); - imageFootprint = frame * image.a; - } - - float imageAlpha = imageFootprint; + vec3 paper = vec3(1.) - opacity + color; + vec3 pic = blendMultiply(image.rgb, paper, u_blending); + pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); - vec3 overlay = pic * imageAlpha + shadowColor * patternAlpha * (1. - imageAlpha); - float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); + color = mix(color, pic, frame); + opacity = frame + opacity * (1. - frame); - vec3 sheet = overlay + baseColor * baseOpacity * (1. - overlayAlpha); - float sheetAlpha = overlayAlpha + baseOpacity * (1. - overlayAlpha); - - if (u_clip && u_isImage) { - sheet *= imageFootprint; - sheetAlpha *= imageFootprint; + if (u_clip) { + color *= frame; + opacity *= frame; + } } - vec3 color = sheet + backColor * backOpacity * (1. - sheetAlpha); - float opacity = sheetAlpha + backOpacity * (1. - sheetAlpha); + color += backColor * (1. - opacity); + opacity += backOpacity * (1. - opacity); fragColor = vec4(color, opacity); } From ab8acdd9a9d566056ec19e41d14c8ffcf7cb554e Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 16:35:34 +0200 Subject: [PATCH 154/186] manual clean-up pass --- .../src/shaders/paper-texture.tsx | 6 +- .../shaders/src/shaders/paper-texture---.ts | 512 ++++++++++++++++++ packages/shaders/src/shaders/paper-texture.ts | 145 +++-- 3 files changed, 582 insertions(+), 81 deletions(-) create mode 100644 packages/shaders/src/shaders/paper-texture---.ts diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 35ab4950b..87ef47235 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -36,9 +36,9 @@ export const defaultPreset: PaperTexturePreset = { colorShadow: '#b3b3b3', roughness: 0, roughnessSize: 0.25, - fiber: 0.65, + fiber: 1, fiberSize: 0.5, - crumples: 0.5, + crumples: 0, crumpleCount: 6, folds: 0, foldSizeX: 0.6, @@ -46,7 +46,7 @@ export const defaultPreset: PaperTexturePreset = { foldOffsetX: 0, foldOffsetY: 0, angle: 300, - drops: 0.7, + drops: 0, seed: 234, clip: false, }, diff --git a/packages/shaders/src/shaders/paper-texture---.ts b/packages/shaders/src/shaders/paper-texture---.ts new file mode 100644 index 000000000..0cee737dc --- /dev/null +++ b/packages/shaders/src/shaders/paper-texture---.ts @@ -0,0 +1,512 @@ +// language=GLSL +export const declarePI = ` +#define TWO_PI 6.28318530718 +#define PI 3.14159265358979323846 +`; + +// language=GLSL +export const rotation2 = ` +vec2 rotate(vec2 uv, float th) { + return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv; +} +`; + +// language=GLSL +export const proceduralHash11 = ` + float hash11(float p) { + p = fract(p * 0.3183099) + 0.1; + p *= p + 19.19; + return fract(p * p); + } +`; + +// language=GLSL +export const proceduralHash21 = ` + float hash21(vec2 p) { + p = fract(p * vec2(0.3183099, 0.3678794)) + 0.1; + p += dot(p, p + 19.19); + return fract(p.x * p.y); + } +`; + +// language=GLSL +export const proceduralHash22 = ` + vec2 hash22(vec2 p) { + p = fract(p * vec2(0.3183099, 0.3678794)) + 0.1; + p += dot(p, p.yx + 19.19); + return fract(vec2(p.x * p.y, p.x + p.y)); + } +`; + +// language=GLSL +export const textureRandomizerR = ` + float randomR(vec2 p) { + vec2 uv = floor(p) / 100. + .5; + return texture(u_noiseTexture, fract(uv)).r; + } +`; + +// language=GLSL +export const textureRandomizerGB = ` + vec2 randomGB(vec2 p) { + vec2 uv = floor(p) / 100. + .5; + return texture(u_noiseTexture, fract(uv)).gb; + } +`; + +// language=GLSL +export const colorBandingFix = ` + color += 1. / 256. * (fract(sin(dot(.014 * gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453123) - .5); +`; + +// language=GLSL +export const simplexNoise = ` +vec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); } +float snoise(vec2 v) { + const vec4 C = vec4(0.211324865405187, 0.366025403784439, + -0.577350269189626, 0.024390243902439); + vec2 i = floor(v + dot(v, C.yy)); + vec2 x0 = v - i + dot(i, C.xx); + vec2 i1; + i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + i = mod(i, 289.0); + vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + + i.x + vec3(0.0, i1.x, 1.0)); + vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), + dot(x12.zw, x12.zw)), 0.0); + m = m * m; + m = m * m; + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} +`; + +// language=GLSL +export const fiberNoise = ` +float fiberRandom(vec2 p) { + vec2 uv = floor(p) / 100.; + return texture(u_noiseTexture, fract(uv)).b; +} + +float fiberValueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = fiberRandom(i); + float b = fiberRandom(i + vec2(1.0, 0.0)); + float c = fiberRandom(i + vec2(0.0, 1.0)); + float d = fiberRandom(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} + +float fiberNoiseFbm(in vec2 n, vec2 seedOffset) { + float total = 0.0, amplitude = 1.; + for (int i = 0; i < 4; i++) { + n = rotate(n, .7); + total += fiberValueNoise(n + seedOffset) * amplitude; + n *= 2.; + amplitude *= 0.6; + } + return total; +} + +float fiberNoise(vec2 uv, vec2 seedOffset) { + float epsilon = 0.001; + float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset); + float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset); + float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset); + float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset); + return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon); +} +`; + + +// language=GLSL +export const paperTextureeeeFragmentShader: string = `#version 300 es +precision mediump float; + +uniform vec2 u_resulution; +uniform vec4 u_colorFront; +uniform vec4 u_colorBack; + +uniform sampler2D u_image; +uniform float u_imageAspectRatio; + +uniform float u_roughness; +uniform float u_fiber; +uniform float u_fiberSize; +uniform float u_folds; +uniform float u_foldType; +uniform float u_foldCount; +uniform float u_foldSize; +uniform bool u_foldY; +uniform float u_foldsShape; +uniform float u_foldOffset; +uniform float u_drops; +uniform float u_seed; +uniform float u_fade; +uniform float u_blending; +uniform float u_distortion; +uniform float u_roughnessSize; +uniform bool u_background; +uniform sampler2D u_noiseTexture; + +in vec2 v_imageUV; +out vec4 fragColor; + +float getUvFrame(vec2 uv, float blur) { + float left = smoothstep(0., blur, uv.x); + float right = 1. - smoothstep(1. - blur, 1., uv.x); + float bottom = smoothstep(0., blur, uv.y); + float top = 1. - smoothstep(1. - blur, 1., uv.y); + return left * right * bottom * top; +} + +float lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +} +float sst(float edge0, float edge1, float x) { + return smoothstep(edge0, edge1, x); +} + +${ declarePI } +${ rotation2 } +float randomR(vec2 p) { + vec2 uv = floor(p) / 100. + .5; + return texture(u_noiseTexture, fract(uv)).r; +} +float valueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = randomR(i); + float b = randomR(i + vec2(1.0, 0.0)); + float c = randomR(i + vec2(0.0, 1.0)); + float d = randomR(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + float x1 = mix(a, b, u.x); + float x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} +float getFadeMask(vec2 n) { + float total = 0.0, amplitude = .4; + for (int i = 0; i < 2; i++) { + total += valueNoise(n) * amplitude; + n *= 1.99; + amplitude *= 0.65; + } + return total; +} + +float getRoughness(vec2 p) { + vec2 u = p / vec2(2, 4); + float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); + float eps = 4. * w; + + float size = mix(3.2, .6, u_roughnessSize); + float logLac = log2(2.1); + float level = -log2(w + 1e-8) / logLac; + float baseLevel = floor(level) - 2.; + float fade = fract(level); + + vec2 px = (u.x + vec2(eps, -eps)) * .1; + float py = u.y * .1; + + vec2 sum = vec2(0.); + float norm = 0., amp = .5; + for (int i = 0; i < 4; i++) { + float absIdx = baseLevel + float(i); + float freq = pow(2.1, absIdx); + vec2 qx = size * px * freq; + float qy = size * py * freq; + + float wi = 1.; + if (i == 0) wi = 1. - fade; + if (i == 4) wi = fade; + + vec2 fx = fract(qx); + float fy = fract(qy); + float shift = .5 + absIdx * .3; + float uvY = floor(qy) / 50. + shift; + vec2 s0a = texture(u_noiseTexture, fract(vec2(floor(qx.x) / 50. + shift, uvY))).rg; + vec2 s1a = texture(u_noiseTexture, fract(vec2( ceil(qx.x) / 50. + shift, uvY))).rg; + vec2 s0b = texture(u_noiseTexture, fract(vec2(floor(qx.y) / 50. + shift, uvY))).rg; + vec2 s1b = texture(u_noiseTexture, fract(vec2( ceil(qx.y) / 50. + shift, uvY))).rg; + vec2 ny0 = mix(vec2(s0a.r, s0b.r), vec2(s0a.g, s0b.g), fy); + vec2 ny1 = mix(vec2(s1a.r, s1b.r), vec2(s1a.g, s1b.g), fy); + vec2 n = mix(ny0, ny1, fx); + + sum += amp * wi * n; + norm += amp * wi; + amp *= .8; + } + + vec2 r = sum / norm; + float dx = .5 + r.x - r.y; + return 3. * dx * dx; +} + +float getFiber(vec2 p) { + float size = mix(2., .4, u_fiberSize); + float w = max(length(dFdx(p)), length(dFdy(p))); + float level = -log2(w + 1e-8); + float baseLevel = floor(level) - 3.; + float fade = fract(level); + + vec2 grad = vec2(0.); + float scale = 1.; + float amp = 1.; + for (int i = 0; i < 5; i++) { + float absIdx = baseLevel + float(i); + float freq = pow(1.7, absIdx); + vec2 q = size * p * freq; + + float an = absIdx * .8; + float rc = cos(an), rs = sin(an); + q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); + + float wi = 1.; + if (i == 0) wi = 1. - fade; + if (i == 4) wi = fade; + + vec2 iq = floor(q); + vec2 fq = fract(q); + float shift = absIdx * .3; + vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift); + float aF = texture(u_noiseTexture, uv.xy).b; + float bF = texture(u_noiseTexture, uv.zy).b; + float cF = texture(u_noiseTexture, uv.xw).b; + float dF = texture(u_noiseTexture, uv.zw).b; + vec2 u = fq * fq * (3. - 2. * fq); + vec2 du = 8. * fq * (1. - fq); + float dx = du.x * mix(bF - aF, dF - cF, u.y); + float dy = du.y * mix(cF - aF, dF - bF, u.x); + grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); + scale *= 1.7; + amp *= .5; + } + + return min(1., .5 * length(grad)); +} + +vec2 randomGB(vec2 p) { + vec2 uv = floor(p) / 50. + .5; + return texture(u_noiseTexture, fract(uv)).gb; +} + +float getDrops(vec2 uv) { + vec2 iDropsUV = floor(uv); + vec2 fDropsUV = fract(uv); + float dropsMinDist = 1.; + for (int y = -1; y < 2; y += 1) { + for (int x = -1; x < 2; x += 1) { + vec2 neighbor = vec2(float(y), float(x)); + vec2 offset = randomGB(iDropsUV + neighbor); + offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); + vec2 pos = neighbor + offset - fDropsUV; + float dist = length(pos); + dropsMinDist = min(dropsMinDist, dropsMinDist*dist); + } + } + return 1. - lst(.05, .09, sqrt(dropsMinDist)); +} + +vec4 getFolds(vec2 uv1, vec2 uv2) { + vec3 pp1 = vec3(0.), pp2 = vec3(0.); + float l1 = 9., l2 = 9.; + float cellRand = 0.; + for (int i = 0; i < 20; i++) { + if (float(i) >= floor(u_foldCount + .5)) break; + vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); + float an = rand.x * TWO_PI; + vec2 p = vec2(cos(an), sin(an)) * rand.y; + float dist1 = distance(uv1, p); + if (dist1 < l1) { + l1 = dist1; + pp1 = vec3(uv1.x - p.x, dist1, rand.y); + } + float dist2 = distance(uv2, p); + if (dist2 < l2) { + l2 = dist2; + pp2 = vec3(uv2.x - p.x, dist2, rand.y); + cellRand = .5 * (rand.x + rand.y); + } + } + float mult2 = mix(.22, .02, 1.); + return vec4( + mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), + mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)), + .2 * pp2.y, + cellRand + ); +} + +vec4 getCrease(float coord, float offset, float count) { + float g = (coord - 1.) * count + .5 * offset; + float crX = fract(g); + float creaseIdx = floor(g); + float depthMod = .7 + .3 * fract(sin(creaseIdx * 127.1 + u_seed * 3.7) * 43758.5453); + float dx = crX - .5; + float foldWidth = mix(.1, .5, u_foldsShape); + float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * depthMod; + float slope = sign(dx) * foldAmount; + float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); + return vec4(slope, creaseDark, abs(dx), foldAmount); +} + +vec3 blendMultiply(vec3 base, vec3 blend) { + return base * blend; +} +vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { + return blendMultiply(base, blend) * opacity + base * (1. - opacity); +} + +void main() { + + vec2 imageUV = v_imageUV; + vec2 fromCenter = imageUV - .5; + vec2 patternUV = v_imageUV - .5; + patternUV *= 5. * vec2(u_imageAspectRatio, 1.); + + float pattern = 0.; + float radialDistortion = 0.; + float xDistortion = 0.; + float yShift = 0.; + float scaleDistortion = 0.; + + float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); + fade = clamp(8. * fade * fade * fade, 0., 1.); + + float drops = 0.; + if (u_drops > 0.) { + drops = getDrops(patternUV * 2.); + drops = mix(drops, 0., fade); + } + + if (u_folds > 0.) { + if (u_foldType < .5) { + vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); + vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); + vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); + vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); + radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); + float foldsPattern = radialFolds.x + radialFolds.y; + + pattern += u_folds * foldsPattern; + + vec2 fromCenter = imageUV - .5; + scaleDistortion = .22 * radialFolds.z * u_folds; + } else { + vec2 uv = imageUV + .5; + float countX = mix(25., 1., pow(u_foldSize, .4)); + vec4 h = getCrease(uv.x, 1. - u_foldOffset, countX); + + if (u_foldY) { + vec4 v = getCrease(uv.y, 1., 1.); + float ax = h.x * 1.1345; + float ay = v.x * 1.1345; + float gridDark = h.y * v.y; + float crLight = max(-.7 * sin(ax) - .2 * sin(ay) + .5 * cos(ax) * cos(ay), 0.) * mix(.9, 1., gridDark); + drops *= mix(1., gridDark, u_folds); + + pattern += u_folds * crLight; + float distortBaseH = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); + float distortBaseV = mix(pow(v.y, .2), v.z, .5 * u_foldsShape); + xDistortion += .022 * u_folds * (1. - distortBaseV) * 2. * (imageUV.x - .5); + patternUV.x += .022 * u_folds * (1. - distortBaseV); + yShift += .022 * u_folds * (1. - distortBaseH); + } else { + float angle = h.x * 1.1345; + float crLight = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., h.y); + drops *= mix(1., h.y, u_folds); + + pattern += u_folds * crLight; + float distortBase = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); + yShift += .022 * u_folds * (1. - distortBase); + } + patternUV.y += yShift; + } + } + + if (u_roughness > 0.) { + float roughness = getRoughness(200. * patternUV); + roughness *= mix(1., .3, fade); + roughness *= u_roughness; + pattern += roughness; + radialDistortion += .02 * roughness; + } + + if (u_fiber > 0.) { + float fiber = getFiber(10. * patternUV); + fiber *= mix(1., .3, fade); + fiber *= u_fiber; + pattern += fiber; + radialDistortion += .02 * fiber; + } + + if (u_drops > 0.) { + drops *= u_drops; + pattern += drops; + xDistortion += .03 * drops; + } + + vec3 fgColor = u_colorFront.rgb * u_colorFront.a; + float fgOpacity = u_colorFront.a; + vec3 bgColor = u_colorBack.rgb * u_colorBack.a; + float bgOpacity = u_colorBack.a; + + imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); + imageUV.x += u_distortion * xDistortion; + imageUV.y -= u_distortion * yShift; + vec2 dc = imageUV - .5; + float r2 = dot(dc, dc); + imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); + + float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness); + float frame = getUvFrame(imageUV, frameSoftness); + vec4 image = texture(u_image, imageUV); + frame *= image.a; + + vec3 color = fgColor * pattern; + float opacity = fgOpacity * pattern; + + color += bgColor * (1. - opacity); + opacity += bgOpacity * (1. - opacity); + + float maxC = max(max(image.r, image.g), image.b); + float minC = min(min(image.r, image.g), image.b); + float sat = maxC > 0. ? (maxC - minC) / maxC : 0.; + float midC = image.r + image.g + image.b - maxC - minC; + float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; + float satDampen = sat * (1. - .5 * secondaryness); + float darkDampen = 1. - dot(vec3(.2126, .7152, .0722), image.rgb); + float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); + + vec3 pic = blendMultiply(image.rgb, color, u_blending); + pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); + + color = mix(color, pic, frame); + + if (!u_background) { + vec2 shadowUV = imageUV + vec2(-.01, -.015); + float shadowFrame = getUvFrame(shadowUV, .05) * texture(u_image, shadowUV).a; + float shadow = 1.2 * shadowFrame * (1. - frame); + opacity = opacity * frame + shadow; + color *= frame; + } + frame = mix(frame, 0., .2 * fade); + + + fragColor = vec4(color, opacity); +} +`; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 93db20c52..2ff9b2b2c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -87,16 +87,6 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; -#define FOLD_WEAR .6 -#define LIFT_SHARPNESS 4. -#define GRAIN_DRAG .2 - -#define TILT_FLOOR .9 -#define RADIAL_FALLOFF .7 -#define FACET_CURVE 1.5 -#define CRUMPLE_LIT_GAIN .35 -#define SHADING_SCALE 1. - float getUvFrame(vec2 uv, float blur) { float left = smoothstep(0., blur, uv.x); float right = 1. - smoothstep(1. - blur, 1., uv.x); @@ -161,46 +151,46 @@ float getRoughness(vec2 p) { } float getFiber(vec2 p) { - float size = mix(4.5, 1.2, u_fiberSize); - float w = max(length(dFdx(p)), length(dFdy(p))); - float level = -log2(w + 1e-8); - float baseLevel = floor(level) - 3.; - float fade = fract(level); - - vec2 grad = vec2(0.); - float scale = 1.; - float amp = 1.; - for (int i = 0; i < 5; i++) { - float absIdx = baseLevel + float(i); - float freq = exp2(absIdx * 0.76553); - vec2 q = size * p * freq; - - float an = absIdx * .8; - float rc = cos(an), rs = sin(an); - q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); - - float wi = 1.; - if (i == 0) wi = 1. - fade; - if (i == 4) wi = fade; - - vec2 iq = floor(q); - vec2 fq = fract(q); - float shift = absIdx * .3; - vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift); - float aF = texture(u_noiseTexture, uv.xy).b; - float bF = texture(u_noiseTexture, uv.zy).b; - float cF = texture(u_noiseTexture, uv.xw).b; - float dF = texture(u_noiseTexture, uv.zw).b; - vec2 u = fq * fq * (3. - 2. * fq); - vec2 du = 8. * fq * (1. - fq); - float dx = du.x * mix(bF - aF, dF - cF, u.y); - float dy = du.y * mix(cF - aF, dF - bF, u.x); - grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); - scale *= 1.7; - amp *= .5; - } + float size = mix(4., 1., u_fiberSize); + float w = max(length(dFdx(p)), length(dFdy(p))); + float level = -log2(w + 1e-8); + float baseLevel = floor(level) - 3.; + float fade = fract(level); + + vec2 grad = vec2(0.); + float scale = 1.; + float amp = 1.; + for (int i = 0; i < 5; i++) { + float absIdx = baseLevel + float(i); + float freq = pow(1.7, absIdx); + vec2 q = size * p * freq; + + float an = absIdx * .8; + float rc = cos(an), rs = sin(an); + q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); + + float wi = 1.; + if (i == 0) wi = 1. - fade; + if (i == 4) wi = fade; + + vec2 iq = floor(q); + vec2 fq = fract(q); + float shift = absIdx * .3; + vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift); + float aF = texture(u_noiseTexture, uv.xy).b; + float bF = texture(u_noiseTexture, uv.zy).b; + float cF = texture(u_noiseTexture, uv.xw).b; + float dF = texture(u_noiseTexture, uv.zw).b; + vec2 u = fq * fq * (3. - 2. * fq); + vec2 du = 8. * fq * (1. - fq); + float dx = du.x * mix(bF - aF, dF - cF, u.y); + float dy = du.y * mix(cF - aF, dF - bF, u.x); + grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); + scale *= 1.7; + amp *= .5; + } - return .5 * length(grad) - .5; + return clamp(.333 * length(grad), 0., 1.); } vec2 smoothNoise(vec2 p) { @@ -230,7 +220,7 @@ float getDrops(vec2 uv) { vec2 getCellTilt(float idx, float radius) { vec2 rand = hash22(vec2(idx + 31., idx * u_seed + 17.)); float an = floor(rand.x * 24.) / 24. * TWO_PI; - return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); + return vec2(cos(an), sin(an)) * mix(.9, 1., rand.y) * mix(.7, 1., radius); } vec4 getCrumples(vec2 uv) { @@ -262,7 +252,7 @@ vec4 getCrumples(vec2 uv) { float edge = lst(0., .5, lb - l); vec2 edgeGrad = (uv - nearPb) / max(lb, 1e-4) - (uv - nearP) / max(l, 1e-4); - return vec4(getCellTilt(idx, rad) + FACET_CURVE * (1. - edge) * edgeGrad, .2 * l, edge); + return vec4(getCellTilt(idx, rad) + 1.5 * (1. - edge) * edgeGrad, .2 * l, edge); } void getFolds(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { @@ -273,7 +263,7 @@ void getFolds(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark vec2 parity = (1. - 2. * mod(foldIdx, 2.)); slope = sign(dx) * (1. - smoothstep(0., foldWidth, abs(dx))) * parity; dark = smoothstep(0., foldWidth * .5, abs(dx)); - lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(LIFT_SHARPNESS)) * parity; + lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(4.)) * parity; } vec3 blendMultiply(vec3 base, vec3 blend) { @@ -286,19 +276,16 @@ vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { void main() { vec2 imageUV = v_imageUV; - vec2 fromCenter = imageUV - .5; - vec2 patternUV = v_imageUV - .5; - patternUV *= 5. * vec2(u_imageAspectRatio, 1.); + vec2 patternUV = (v_imageUV - .5) * vec2(u_imageAspectRatio, 1.); float pattern = 0.; float radialDistortion = 0.; - float xDistortion = 0.; - float yShift = 0.; + vec2 linearDistortion = vec2(0.); float scaleDistortion = 0.; float drops = 0.; if (u_drops > 0.) { - drops = getDrops(patternUV * 2.); + drops = getDrops(patternUV * 10.); } float grazing = 0.7; @@ -313,12 +300,12 @@ void main() { if (u_crumples > 0.) { float crumpleRad = 4. * u_seed; float crumpleCos = cos(crumpleRad), crumpleSin = sin(crumpleRad); - vec2 crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .18) + - .012 * (smoothNoise(patternUV * .015 + u_seed) - .5); + vec2 crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + + .012 * (smoothNoise(patternUV * .075 + u_seed) - .5); vec4 crumples = getCrumples(crumplesUV); vec2 crumpleTilt = .5 * u_crumples * crumples.xy; - crumpleTilt -= (1. - CRUMPLE_LIT_GAIN) * max(dot(crumpleTilt, lightDir), 0.) * lightDir; + crumpleTilt -= .65 * max(dot(crumpleTilt, lightDir), 0.) * lightDir; relief += crumpleTilt; reliefAmount += .5 * u_crumples; crumpleFlow = crumples.w * crumpleTilt; @@ -327,9 +314,8 @@ void main() { } if (u_folds > 0.) { - vec2 uv = 1. + patternUV * .2 + .03 * crumpleFlow; + vec2 uv = 1. + patternUV + .03 * crumpleFlow; - // x drives the folds running vertically, y the ones running across them. vec2 slope, dark, lift; getFolds(uv, vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY), vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), slope, dark, lift); @@ -342,37 +328,39 @@ void main() { vec2 dropMask = mix(vec2(1.), dark, u_folds); drops *= dropMask.x * dropMask.y; - yShift += .022 * u_folds * lift.x * abs(lightDir.x); - patternUV.y += yShift; + float foldShiftY = u_folds * lift.x * abs(lightDir.x); + patternUV.y += .0044 * foldShiftY; + linearDistortion.y -= .022 * foldShiftY; float xFan = 2. * (imageUV.x - .5) * abs(lightDir.y); - xDistortion -= .02 * u_folds * lift.y * xFan; - patternUV.x -= .022 * u_folds * lift.y * xFan; + float foldShiftX = u_folds * lift.y * xFan; + patternUV.x -= .0044 * foldShiftX; + linearDistortion.x -= .022 * foldShiftX; } - float unlit = SHADING_SCALE * cos(grazing); + float unlit = cos(grazing); float lit = unlit; if (reliefAmount > 0.) { float slope = clamp(dot(relief, lightDir), -1.2, 1.2); - lit = SHADING_SCALE * max(cos(slope + grazing), 0.); + lit = max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk; } - patternUV += GRAIN_DRAG * crumpleFlow; + patternUV += .04 * crumpleFlow; - float detailGain = (1. + FOLD_WEAR * foldWear); + float detailGain = (1. + .6 * foldWear); if (u_roughness > 0.) { - float roughness = getRoughness(200. * patternUV); + float roughness = getRoughness(1000. * patternUV); roughness *= u_roughness * detailGain; pattern += roughness; radialDistortion += .02 * roughness; } if (u_fiber > 0.) { - float fiber = getFiber(10. * patternUV); + float fiber = getFiber(50. * patternUV); fiber *= u_fiber * detailGain; pattern += fiber; radialDistortion += .02 * fiber; @@ -381,8 +369,9 @@ void main() { if (u_drops > 0.) { drops *= u_drops; pattern += drops; - xDistortion += .03 * drops; + linearDistortion.x += .03 * drops; } + pattern = clamp(pattern, 0., 1.); vec3 backColor = u_colorBack.rgb * u_colorBack.a; float backOpacity = u_colorBack.a; @@ -393,14 +382,13 @@ void main() { vec3 shadowColor = u_colorShadow.rgb * u_colorShadow.a; float shadowOpacity = u_colorShadow.a; - imageUV = .5 + fromCenter * (1. - u_distortion * scaleDistortion); - imageUV -= u_distortion * vec2(xDistortion, -yShift); + imageUV = .5 + (imageUV - .5) * (1. - u_distortion * scaleDistortion); + imageUV -= u_distortion * linearDistortion; vec2 dc = imageUV - .5; float r2 = dot(dc, dc); imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - pattern = clamp(pattern, 0., 1.); - + vec3 color = shadowColor * pattern; float opacity = shadowOpacity * pattern; @@ -438,6 +426,7 @@ void main() { opacity += backOpacity * (1. - opacity); fragColor = vec4(color, opacity); +// fragColor = vec4(pattern, pattern, pattern, 1.); } `; From 53498e7248552cff64bf9209ed5fc931bcaad42e Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 17:50:03 +0200 Subject: [PATCH 155/186] - --- packages/shaders/src/shaders/paper-texture.ts | 70 +++++++------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 2ff9b2b2c..9669ca74d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -266,27 +266,21 @@ void getFolds(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(4.)) * parity; } -vec3 blendMultiply(vec3 base, vec3 blend) { - return base * blend; -} vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { - return blendMultiply(base, blend) * opacity + base * (1. - opacity); + return base * blend * opacity + base * (1. - opacity); } void main() { - vec2 imageUV = v_imageUV; vec2 patternUV = (v_imageUV - .5) * vec2(u_imageAspectRatio, 1.); float pattern = 0.; - float radialDistortion = 0.; - vec2 linearDistortion = vec2(0.); - float scaleDistortion = 0.; + float crumpleDepth = 0.; + vec2 foldShift = vec2(0.); + float roughness = 0.; + float fiber = 0.; float drops = 0.; - if (u_drops > 0.) { - drops = getDrops(patternUV * 10.); - } float grazing = 0.7; float lightRad = radians(u_angle); @@ -294,7 +288,6 @@ void main() { vec2 relief = vec2(0.); float reliefAmount = 0.; float foldInk = 1.; - float foldWear = 0.; vec2 crumpleFlow = vec2(0.); if (u_crumples > 0.) { @@ -310,7 +303,7 @@ void main() { reliefAmount += .5 * u_crumples; crumpleFlow = crumples.w * crumpleTilt; - scaleDistortion = .15 * clamp(5. * crumples.z, 0., 1.) * u_crumples; + crumpleDepth = clamp(5. * crumples.z, 0., 1.) * u_crumples; } if (u_folds > 0.) { @@ -324,18 +317,12 @@ void main() { reliefAmount += 1. * u_folds; vec2 ink = mix(vec2(.9), vec2(1.), dark); foldInk *= ink.x * ink.y; - foldWear = max(foldWear, max(1. - dark.x, 1. - dark.y) * u_folds); - vec2 dropMask = mix(vec2(1.), dark, u_folds); - drops *= dropMask.x * dropMask.y; - - float foldShiftY = u_folds * lift.x * abs(lightDir.x); - patternUV.y += .0044 * foldShiftY; - linearDistortion.y -= .022 * foldShiftY; - - float xFan = 2. * (imageUV.x - .5) * abs(lightDir.y); - float foldShiftX = u_folds * lift.y * xFan; - patternUV.x -= .0044 * foldShiftX; - linearDistortion.x -= .022 * foldShiftX; + + float xFan = 2. * (v_imageUV.x - .5) * abs(lightDir.y); + foldShift = u_folds * vec2(lift.y * xFan, lift.x * abs(lightDir.x)); + + patternUV.x -= .0044 * foldShift.x; + patternUV.y += .0044 * foldShift.y; } float unlit = cos(grazing); @@ -350,51 +337,45 @@ void main() { patternUV += .04 * crumpleFlow; - float detailGain = (1. + .6 * foldWear); - if (u_roughness > 0.) { - float roughness = getRoughness(1000. * patternUV); - roughness *= u_roughness * detailGain; + roughness = u_roughness * getRoughness(1000. * patternUV); pattern += roughness; - radialDistortion += .02 * roughness; } if (u_fiber > 0.) { - float fiber = getFiber(50. * patternUV); - fiber *= u_fiber * detailGain; + fiber = u_fiber * getFiber(50. * patternUV); pattern += fiber; - radialDistortion += .02 * fiber; } if (u_drops > 0.) { - drops *= u_drops; + drops = u_drops * getDrops(patternUV * 10.); pattern += drops; - linearDistortion.x += .03 * drops; } + pattern = clamp(pattern, 0., 1.); vec3 backColor = u_colorBack.rgb * u_colorBack.a; float backOpacity = u_colorBack.a; - vec3 baseColor = u_colorBase.rgb * u_colorBase.a; float baseOpacity = u_colorBase.a; - vec3 shadowColor = u_colorShadow.rgb * u_colorShadow.a; float shadowOpacity = u_colorShadow.a; - imageUV = .5 + (imageUV - .5) * (1. - u_distortion * scaleDistortion); - imageUV -= u_distortion * linearDistortion; - vec2 dc = imageUV - .5; - float r2 = dot(dc, dc); - imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - + float notClipped = u_clip ? .2 : 1.; + float scaleDistortion = .15 * crumpleDepth; + vec2 linearDistortion = -.022 * foldShift + notClipped * .05 * lightDir * drops; + float radialDistortion = notClipped * .02 * (roughness + fiber); + vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); + centeredUV -= u_distortion * linearDistortion; + vec2 imageUV = .5 + centeredUV * (1. - abs(u_distortion) * radialDistortion * dot(centeredUV, centeredUV)); + vec3 color = shadowColor * pattern; float opacity = shadowOpacity * pattern; - color += baseColor * (1. - opacity); opacity += baseOpacity * (1. - opacity); + if (u_isImage) { float frame = getUvFrame(imageUV, .001); vec4 image = texture(u_image, imageUV); @@ -426,7 +407,6 @@ void main() { opacity += backOpacity * (1. - opacity); fragColor = vec4(color, opacity); -// fragColor = vec4(pattern, pattern, pattern, 1.); } `; From b1487d927972f83b3ffcda167344260d4540a421 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sat, 29 Aug 2026 18:42:13 +0200 Subject: [PATCH 156/186] SAVING ALT FOLDS --- docs/src/app/(shaders)/paper-texture/page.tsx | 11 ++- docs/src/shader-defs/paper-texture-def.ts | 28 +++++- .../src/shaders/paper-texture.tsx | 22 ++++- packages/shaders/src/shaders/paper-texture.ts | 97 +++++++++++++++++-- 4 files changed, 146 insertions(+), 12 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index ed7527577..f59ee2dc6 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -77,7 +77,16 @@ const PaperTextureWithControls = () => { fiber: { value: defaults.fiber, min: 0, max: 1, order: 310 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 311 }, crumples: { value: defaults.crumples, min: 0, max: 1, order: 320 }, - crumpleCount: { value: defaults.crumpleCount, min: 1, max: paperTextureMeta.maxCrumpleCount, step: 1, order: 321 }, + crumpleCount: { + value: defaults.crumpleCount, + min: 1, + max: paperTextureMeta.maxCrumpleCount, + step: 1, + order: 321, + }, + test: { value: defaults.test, min: 0, max: 1, order: 325 }, + testSize: { value: defaults.testSize, min: 0, max: 1, order: 326 }, + testCurve: { value: defaults.testCurve, min: 0, max: 1, order: 327 }, folds: { value: defaults.folds, min: 0, max: 1, order: 330 }, foldSizeX: { value: defaults.foldSizeX, min: 0, max: 1, order: 331 }, foldOffsetX: { value: defaults.foldOffsetX, min: 0, max: 1, order: 332 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 070b03cdd..be52e1b32 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -108,6 +108,30 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.crumpleCount, description: 'Number of crumples (15 max), needs crumples > 0', }, + { + name: 'test', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.test, + description: 'Depth of the crease pattern: flat facets separated by sharp folds, spread evenly over the sheet', + }, + { + name: 'testSize', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.testSize, + description: 'Size of the crease facets (needs test > 0)', + }, + { + name: 'testCurve', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.testCurve, + description: 'How much the creases bend: 0 = perfectly straight folds, 1 = arcs (needs test > 0)', + }, { name: 'folds', type: 'number', @@ -155,7 +179,7 @@ export const paperTextureDef: ShaderDef = { max: 360, defaultValue: defaultParams.angle, description: - 'Direction the surface is lit from, clockwise from the top of the canvas, needs crumples or folds > 0', + 'Direction the surface is lit from, clockwise from the top of the canvas, needs crumples, folds or test > 0', }, { name: 'drops', @@ -171,7 +195,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1000, defaultValue: defaultParams.seed, - description: 'Seed applied to crumples and drops', + description: 'Seed applied to crumples, creases and drops', }, ...staticImageCommonParams, ], diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 87ef47235..f60927672 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -36,13 +36,16 @@ export const defaultPreset: PaperTexturePreset = { colorShadow: '#b3b3b3', roughness: 0, roughnessSize: 0.25, - fiber: 1, + fiber: 0, fiberSize: 0.5, crumples: 0, crumpleCount: 6, + test: 1, + testSize: 0.65, + testCurve: 0.5, folds: 0, foldSizeX: 0.6, - foldSizeY: 0.89, + foldSizeY: 0.44, foldOffsetX: 0, foldOffsetY: 0, angle: 300, @@ -71,6 +74,9 @@ export const gridPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleCount: 12, + test: 0, + testSize: 0.65, + testCurve: 0.5, folds: 0.25, foldSizeX: 0.6, foldSizeY: 0.89, @@ -102,6 +108,9 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.58, crumples: 0.25, crumpleCount: 9, + test: 0, + testSize: 0.65, + testCurve: 0.5, folds: 0.55, foldSizeX: 0, foldSizeY: 0.89, @@ -133,6 +142,9 @@ export const coloredPreset: PaperTexturePreset = { fiberSize: 1, crumples: 0, crumpleCount: 12, + test: 0, + testSize: 0.65, + testCurve: 0.5, folds: 0.43, foldSizeX: 1, foldSizeY: 0.89, @@ -164,6 +176,9 @@ export const PaperTexture: React.FC = memo(function PaperText roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, + test = defaultPreset.params.test, + testSize = defaultPreset.params.testSize, + testCurve = defaultPreset.params.testCurve, folds = defaultPreset.params.folds, foldSizeX = defaultPreset.params.foldSizeX, foldSizeY = defaultPreset.params.foldSizeY, @@ -214,6 +229,9 @@ export const PaperTexture: React.FC = memo(function PaperText u_foldOffsetY: foldOffsetY, u_angle: angle, u_crumples: crumples, + u_test: test, + u_testSize: testSize, + u_testCurve: testCurve, u_folds: folds, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9669ca74d..a437f719d 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -23,14 +23,17 @@ export const paperTextureMeta = { * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) + * - u_test (float): Depth of the crease pattern: flat facets separated by sharp folds, spread evenly over the sheet (0 to 1) + * - u_testSize (float): Size of the crease facets, needs u_test > 0 (0 to 1) + * - u_testCurve (float): How much the creases bend; 0 = perfectly straight folds, 1 = arcs, needs u_test > 0 (0 to 1) * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) * - u_foldSizeX (float): Size of the vertical folds, needs u_folds > 0 (0 to 1) * - u_foldSizeY (float): Size of the horizontal folds, needs u_folds > 0 (0 to 1) * - u_foldOffsetX (float): Shifts the vertical folds across the surface, needs u_folds > 0 (0 to 1) * - u_foldOffsetY (float): Shifts the horizontal folds across the surface, needs u_folds > 0 (0 to 1) - * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples or u_folds > 0 (0 to 360) + * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples, u_folds or u_test > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) - * - u_seed (float): Seed applied to crumples and drops (0 to 1000) + * - u_seed (float): Seed applied to crumples, creases and drops (0 to 1000) * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorBase toned by colorShadow), needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Cuts the paper sheet to the distorted image frame, revealing u_colorBack outside it, needs image @@ -70,6 +73,9 @@ uniform float u_fiber; uniform float u_fiberSize; uniform float u_crumples; uniform float u_crumpleCount; +uniform float u_test; +uniform float u_testSize; +uniform float u_testCurve; uniform float u_folds; uniform float u_foldSizeX; uniform float u_foldSizeY; @@ -255,13 +261,72 @@ vec4 getCrumples(vec2 uv) { return vec4(getCellTilt(idx, rad) + 1.5 * (1. - edge) * edgeGrad, .2 * l, edge); } +float getCreases(vec2 p, out vec2 grad) { + grad = vec2(0.); + float crease = 0.; + float amp = 1.; + float freq = mix(6., 1.5, u_testSize); + float freq0 = freq; + float pixel = 1.5 * max(length(dFdx(p)), length(dFdy(p))) + 1e-6; + vec2 seedOffset = 16. * hash22(vec2(u_seed, u_seed + 1.3)); + + for (int oct = 0; oct < 4; oct++) { + float o = float(oct); + float ca = cos(2.4 * o), sa = sin(2.4 * o); + mat2 rot = mat2(ca, sa, -sa, ca); + float fillet = pixel * freq; + vec2 v = rot * p * freq; + vec2 base = floor(v); + vec2 octGrad = vec2(0.); + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 cell = base + vec2(float(x), float(y)); + vec2 r1 = hash22(cell + seedOffset + vec2(7.3, -5.1) * o); + vec2 r2 = hash22(cell.yx * vec2(1.7, 1.3) + seedOffset.yx + vec2(11.3 + o, -7.1)); + if (r2.y > .85) continue; + + vec2 q = v - cell - .35 - .3 * r1; + float t = dot(q, q) / 1.5625; + if (t >= 1.) continue; + + float an = r2.x * TWO_PI; + vec2 n = vec2(cos(an), sin(an)); + vec2 m = vec2(-n.y, n.x); + float side = r2.y < .425 ? -1. : 1.; + float depth = amp * (.4 + .6 * r1.x * r1.x); + float falloff = 1. - t; + float w = falloff * falloff; + + float bend = 2. * (fract(7.31 * r1.x + 13.7 * r2.x) - .5); + bend *= .35 * u_testCurve * abs(bend); + float along = dot(q, m); + float norm = inversesqrt(1. + 4. * bend * bend * along * along); + float d = (dot(q, n) + bend * along * along) * norm; + vec2 dir = (n + 2. * bend * along * m) * norm; + float sm = sqrt(d * d + fillet * fillet); + + octGrad += side * depth * (w * (d / sm) * dir - .64 * falloff * sm * q); + if (side < 0.) crease = max(crease, depth * w * fillet * fillet / (d * d + fillet * fillet)); + } + } + + grad += freq * (octGrad * rot); + amp *= .28; + freq *= 2.1; + } + + grad /= freq0; + return crease; +} + void getFolds(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { vec2 g = (coord - 1.) * count + .5 * offset; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; float foldWidth = .5; vec2 parity = (1. - 2. * mod(foldIdx, 2.)); - slope = sign(dx) * (1. - smoothstep(0., foldWidth, abs(dx))) * parity; + slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))) * parity; dark = smoothstep(0., foldWidth * .5, abs(dx)); lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(4.)) * parity; } @@ -289,6 +354,8 @@ void main() { float reliefAmount = 0.; float foldInk = 1.; vec2 crumpleFlow = vec2(0.); + vec2 creaseShift = vec2(0.); + float creaseHighlight = 0.; if (u_crumples > 0.) { float crumpleRad = 4. * u_seed; @@ -306,6 +373,14 @@ void main() { crumpleDepth = clamp(5. * crumples.z, 0., 1.) * u_crumples; } + if (u_test > 0.) { + vec2 creaseGrad; + creaseHighlight = getCreases(patternUV, creaseGrad); + creaseShift = .55 * u_test * creaseGrad; + relief += creaseShift; + reliefAmount += .35 * u_test; + } + if (u_folds > 0.) { vec2 uv = 1. + patternUV + .03 * crumpleFlow; @@ -315,8 +390,9 @@ void main() { relief += u_folds * slope; reliefAmount += 1. * u_folds; - vec2 ink = mix(vec2(.9), vec2(1.), dark); - foldInk *= ink.x * ink.y; + vec2 ink = mix(vec2(.96), vec2(1.), dark); + float flatness = dark.x * dark.y; + foldInk *= ink.x * ink.y * mix(.5, .3, flatness); float xFan = 2. * (v_imageUV.x - .5) * abs(lightDir.y); foldShift = u_folds * vec2(lift.y * xFan, lift.x * abs(lightDir.x)); @@ -333,6 +409,7 @@ void main() { lit = max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk; + pattern -= .3 * u_test * creaseHighlight; } patternUV += .04 * crumpleFlow; @@ -361,9 +438,9 @@ void main() { vec3 shadowColor = u_colorShadow.rgb * u_colorShadow.a; float shadowOpacity = u_colorShadow.a; - float notClipped = u_clip ? .2 : 1.; + float notClipped = u_clip ? .1 : 1.; float scaleDistortion = .15 * crumpleDepth; - vec2 linearDistortion = -.022 * foldShift + notClipped * .05 * lightDir * drops; + vec2 linearDistortion = -.022 * foldShift - .03 * creaseShift + notClipped * .05 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; @@ -423,6 +500,9 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_fiberSize: number; u_crumples: number; u_crumpleCount: number; + u_test: number; + u_testSize: number; + u_testCurve: number; u_folds: number; u_foldSizeX: number; u_foldSizeY: number; @@ -447,6 +527,9 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara fiberSize?: number; crumples?: number; crumpleCount?: number; + test?: number; + testSize?: number; + testCurve?: number; folds?: number; foldSizeX?: number; foldSizeY?: number; From cc9440ccfdeeaddf18047654430416b2888e0f2b Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 30 Aug 2026 12:33:37 +0200 Subject: [PATCH 157/186] crumpleSoftness + noise over fold lines --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 17 +- .../src/shaders/paper-texture.tsx | 16 +- packages/shaders/src/shaders/paper-texture.ts | 184 +++++++++++------- 4 files changed, 135 insertions(+), 84 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index f59ee2dc6..626012512 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -84,9 +84,9 @@ const PaperTextureWithControls = () => { step: 1, order: 321, }, + crumpleSoftness: { value: defaults.crumpleSoftness, min: 0, max: 1, order: 322 }, test: { value: defaults.test, min: 0, max: 1, order: 325 }, testSize: { value: defaults.testSize, min: 0, max: 1, order: 326 }, - testCurve: { value: defaults.testCurve, min: 0, max: 1, order: 327 }, folds: { value: defaults.folds, min: 0, max: 1, order: 330 }, foldSizeX: { value: defaults.foldSizeX, min: 0, max: 1, order: 331 }, foldOffsetX: { value: defaults.foldOffsetX, min: 0, max: 1, order: 332 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index be52e1b32..4c04ee313 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -108,6 +108,15 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.crumpleCount, description: 'Number of crumples (15 max), needs crumples > 0', }, + { + name: 'crumpleSoftness', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.crumpleSoftness, + description: + 'Softens randomly picked crumple cell borders into gradients, leaving the rest as sharp creases (needs crumples > 0)', + }, { name: 'test', type: 'number', @@ -124,14 +133,6 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.testSize, description: 'Size of the crease facets (needs test > 0)', }, - { - name: 'testCurve', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.testCurve, - description: 'How much the creases bend: 0 = perfectly straight folds, 1 = arcs (needs test > 0)', - }, { name: 'folds', type: 'number', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index f60927672..a69a6f0d6 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,11 +38,11 @@ export const defaultPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, - crumples: 0, + crumples: 1, crumpleCount: 6, - test: 1, + crumpleSoftness: 0, + test: 0, testSize: 0.65, - testCurve: 0.5, folds: 0, foldSizeX: 0.6, foldSizeY: 0.44, @@ -74,9 +74,9 @@ export const gridPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleCount: 12, + crumpleSoftness: 0, test: 0, testSize: 0.65, - testCurve: 0.5, folds: 0.25, foldSizeX: 0.6, foldSizeY: 0.89, @@ -108,9 +108,9 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.58, crumples: 0.25, crumpleCount: 9, + crumpleSoftness: 0, test: 0, testSize: 0.65, - testCurve: 0.5, folds: 0.55, foldSizeX: 0, foldSizeY: 0.89, @@ -142,9 +142,9 @@ export const coloredPreset: PaperTexturePreset = { fiberSize: 1, crumples: 0, crumpleCount: 12, + crumpleSoftness: 0, test: 0, testSize: 0.65, - testCurve: 0.5, folds: 0.43, foldSizeX: 1, foldSizeY: 0.89, @@ -176,9 +176,9 @@ export const PaperTexture: React.FC = memo(function PaperText roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, + crumpleSoftness = defaultPreset.params.crumpleSoftness, test = defaultPreset.params.test, testSize = defaultPreset.params.testSize, - testCurve = defaultPreset.params.testCurve, folds = defaultPreset.params.folds, foldSizeX = defaultPreset.params.foldSizeX, foldSizeY = defaultPreset.params.foldSizeY, @@ -223,6 +223,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_fiber: fiber, u_fiberSize: fiberSize, u_crumpleCount: crumpleCount, + u_crumpleSoftness: crumpleSoftness, u_foldSizeX: foldSizeX, u_foldSizeY: foldSizeY, u_foldOffsetX: foldOffsetX, @@ -231,7 +232,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumples: crumples, u_test: test, u_testSize: testSize, - u_testCurve: testCurve, u_folds: folds, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index a437f719d..1b881fcd2 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -23,9 +23,9 @@ export const paperTextureMeta = { * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) - * - u_test (float): Depth of the crease pattern: flat facets separated by sharp folds, spread evenly over the sheet (0 to 1) + * - u_crumpleSoftness (float): Softens randomly picked crumple cell borders into gradients, leaving the rest as sharp creases; higher values soften more borders and soften them further, needs u_crumples > 0 (0 to 1) + * - u_test (float): Depth of the crease pattern: flat facets separated by sharp straight folds, spread evenly over the sheet (0 to 1) * - u_testSize (float): Size of the crease facets, needs u_test > 0 (0 to 1) - * - u_testCurve (float): How much the creases bend; 0 = perfectly straight folds, 1 = arcs, needs u_test > 0 (0 to 1) * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) * - u_foldSizeX (float): Size of the vertical folds, needs u_folds > 0 (0 to 1) * - u_foldSizeY (float): Size of the horizontal folds, needs u_folds > 0 (0 to 1) @@ -73,9 +73,9 @@ uniform float u_fiber; uniform float u_fiberSize; uniform float u_crumples; uniform float u_crumpleCount; +uniform float u_crumpleSoftness; uniform float u_test; uniform float u_testSize; -uniform float u_testCurve; uniform float u_folds; uniform float u_foldSizeX; uniform float u_foldSizeY; @@ -254,74 +254,120 @@ vec4 getCrumples(vec2 uv) { } } float l = sqrt(near), lb = sqrt(nearB); - + vec2 dir = (uv - nearP) / max(l, 1e-4); float edge = lst(0., .5, lb - l); - vec2 edgeGrad = (uv - nearPb) / max(lb, 1e-4) - (uv - nearP) / max(l, 1e-4); - return vec4(getCellTilt(idx, rad) + 1.5 * (1. - edge) * edgeGrad, .2 * l, edge); -} + float pixel = max(length(dFdx(uv)), length(dFdy(uv))); -float getCreases(vec2 p, out vec2 grad) { - grad = vec2(0.); - float crease = 0.; - float amp = 1.; - float freq = mix(6., 1.5, u_testSize); - float freq0 = freq; - float pixel = 1.5 * max(length(dFdx(p)), length(dFdy(p))) + 1e-6; - vec2 seedOffset = 16. * hash22(vec2(u_seed, u_seed + 1.3)); - - for (int oct = 0; oct < 4; oct++) { - float o = float(oct); - float ca = cos(2.4 * o), sa = sin(2.4 * o); - mat2 rot = mat2(ca, sa, -sa, ca); - float fillet = pixel * freq; - vec2 v = rot * p * freq; - vec2 base = floor(v); - vec2 octGrad = vec2(0.); - - for (int y = -1; y <= 1; y++) { - for (int x = -1; x <= 1; x++) { - vec2 cell = base + vec2(float(x), float(y)); - vec2 r1 = hash22(cell + seedOffset + vec2(7.3, -5.1) * o); - vec2 r2 = hash22(cell.yx * vec2(1.7, 1.3) + seedOffset.yx + vec2(11.3 + o, -7.1)); - if (r2.y > .85) continue; - - vec2 q = v - cell - .35 - .3 * r1; - float t = dot(q, q) / 1.5625; - if (t >= 1.) continue; - - float an = r2.x * TWO_PI; - vec2 n = vec2(cos(an), sin(an)); - vec2 m = vec2(-n.y, n.x); - float side = r2.y < .425 ? -1. : 1.; - float depth = amp * (.4 + .6 * r1.x * r1.x); - float falloff = 1. - t; - float w = falloff * falloff; - - float bend = 2. * (fract(7.31 * r1.x + 13.7 * r2.x) - .5); - bend *= .35 * u_testCurve * abs(bend); - float along = dot(q, m); - float norm = inversesqrt(1. + 4. * bend * bend * along * along); - float d = (dot(q, n) + bend * along * along) * norm; - vec2 dir = (n + 2. * bend * along * m) * norm; - float sm = sqrt(d * d + fillet * fillet); - - octGrad += side * depth * (w * (d / sm) * dir - .64 * falloff * sm * q); - if (side < 0.) crease = max(crease, depth * w * fillet * fillet / (d * d + fillet * fillet)); - } + vec2 tilt = getCellTilt(idx, rad); + float tiltSum = 1.; + vec2 wide = vec2(0.); + float wideSum = 0.; + float toEdge = 9., edgeSoft = 0.; + + for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { + if (float(i) >= floor(u_crumpleCount + .5)) break; + if (float(i) == idx) continue; + vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); + float an = rand.x * TWO_PI; + vec2 p = vec2(cos(an), sin(an)) * rand.y; + + vec2 d = uv - p; + float dsq = dot(d, d); + float di = sqrt(dsq); + + float shoulder = 1. - lst(0., .5, di - l); + if (shoulder > 0.) { + wide += shoulder * (d / max(di, 1e-4) - dir); + wideSum += shoulder; + } + + float lo = min(idx, float(i)), hi = max(idx, float(i)); + float pairSoft = .002 + u_crumpleSoftness * smoothstep(.4, .8, rand.y); + + float toBisector = (dsq - near) / (2. * max(length(p - nearP), 1e-4)); + if (toBisector < toEdge) { + toEdge = toBisector; + edgeSoft = pairSoft; } - grad += freq * (octGrad * rot); - amp *= .28; - freq *= 2.1; + float w = 1. - toBisector / pairSoft; + if (w > 0.) { + w *= w; + tilt += w * getCellTilt(float(i), rand.y); + tiltSum += w; + } } - grad /= freq0; - return crease; + float blend = clamp(toEdge / max(1.5 * pixel, .5 * edgeSoft), 0., 1.); + blend = blend * blend * (3. - 2. * blend); + + vec2 sharp = (1. - edge) * ((uv - nearPb) / max(lb, 1e-4) - dir); + vec2 rounding = mix(sharp, wide / max(wideSum, 1.), min(2. * edgeSoft, 1.)); + + float radial = l / max(l + toEdge, 1e-4); + float plateau = lst(.45, 1., radial); + plateau = plateau * plateau * (3. - 2. * plateau); + + return vec4(tilt / tiltSum + 1.5 * rounding * blend * plateau, .2 * l, edge); } -void getFolds(vec2 coord, vec2 offset, vec2 count, out vec2 slope, out vec2 dark, out vec2 lift) { - vec2 g = (coord - 1.) * count + .5 * offset; + +float getCreases(vec2 p, out vec2 grad) { + grad = vec2(0.); + float crease = 0.; + float amp = 1.; + float freq = mix(6., 1.5, u_testSize); + float freq0 = freq; + float pixel = 1.5 * max(length(dFdx(p)), length(dFdy(p))) + 1e-6; + vec2 seedOffset = 16. * hash22(vec2(u_seed, u_seed + 1.3)); + + for (int oct = 0; oct < 5; oct++) { + float o = float(oct); + float ca = cos(2.4 * o), sa = sin(2.4 * o); + mat2 rot = mat2(ca, sa, -sa, ca); + float fillet = pixel * freq; + vec2 v = rot * p * freq; + vec2 base = floor(v); + vec2 octGrad = vec2(0.); + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 cell = base + vec2(float(x), float(y)); + vec2 r1 = hash22(cell + seedOffset + vec2(7.3, -5.1) * o); + vec2 r2 = r1; + if (r2.y > .85) continue; + + vec2 q = v - cell - .35 - .3 * r1; + float t = dot(q, q) / 1.5625; + if (t >= 1.) continue; + + float an = r2.x * TWO_PI; + vec2 n = vec2(cos(an), sin(an)); + float side = r2.y < .425 ? -1. : 1.; + float depth = amp * (.4 + .6 * r1.x * r1.x); + float falloff = 1. - t; + float w = falloff * falloff; + float d = dot(q, n); + float sm = sqrt(d * d + fillet * fillet); + + octGrad += side * depth * (w * (d / sm) * n - .64 * falloff * sm * q); + if (side < 0.) crease = max(crease, depth * w * fillet * fillet / (d * d + fillet * fillet)); + } + } + + grad += freq * (octGrad * rot); + amp *= .28; + freq *= 1.5; + } + + grad /= freq0; + return crease; +} + + +void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift) { + vec2 g = (coord - 1.) * count + .5 * offset + noise; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; float foldWidth = .5; @@ -357,11 +403,15 @@ void main() { vec2 creaseShift = vec2(0.); float creaseHighlight = 0.; + vec2 warpNoise = vec2(0.); + if (u_crumples > 0. || u_folds > 0.) { + warpNoise = smoothNoise(patternUV * .07 + .2 + .6 * fract(.017 * u_seed)) - .5; + } + if (u_crumples > 0.) { float crumpleRad = 4. * u_seed; float crumpleCos = cos(crumpleRad), crumpleSin = sin(crumpleRad); - vec2 crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + - .012 * (smoothNoise(patternUV * .075 + u_seed) - .5); + vec2 crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + .012 * warpNoise; vec4 crumples = getCrumples(crumplesUV); vec2 crumpleTilt = .5 * u_crumples * crumples.xy; @@ -386,7 +436,7 @@ void main() { vec2 slope, dark, lift; getFolds(uv, vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY), - vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), slope, dark, lift); + vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), .007 * warpNoise, slope, dark, lift); relief += u_folds * slope; reliefAmount += 1. * u_folds; @@ -500,9 +550,9 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_fiberSize: number; u_crumples: number; u_crumpleCount: number; + u_crumpleSoftness: number; u_test: number; u_testSize: number; - u_testCurve: number; u_folds: number; u_foldSizeX: number; u_foldSizeY: number; @@ -527,9 +577,9 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara fiberSize?: number; crumples?: number; crumpleCount?: number; + crumpleSoftness?: number; test?: number; testSize?: number; - testCurve?: number; folds?: number; foldSizeX?: number; foldSizeY?: number; From b429e8ce89c5638a66e548d93618127b5b447f37 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 30 Aug 2026 12:48:10 +0200 Subject: [PATCH 158/186] - --- packages/shaders-react/src/shaders/paper-texture.tsx | 2 +- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index a69a6f0d6..7461c14d0 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -40,7 +40,7 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 1, crumpleCount: 6, - crumpleSoftness: 0, + crumpleSoftness: 0.2, test: 0, testSize: 0.65, folds: 0, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 1b881fcd2..095fdfe6c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -370,7 +370,7 @@ void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, o vec2 g = (coord - 1.) * count + .5 * offset + noise; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; - float foldWidth = .5; + float foldWidth = .9; vec2 parity = (1. - 2. * mod(foldIdx, 2.)); slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))) * parity; dark = smoothstep(0., foldWidth * .5, abs(dx)); @@ -436,7 +436,7 @@ void main() { vec2 slope, dark, lift; getFolds(uv, vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY), - vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), .007 * warpNoise, slope, dark, lift); + vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), .002 * warpNoise, slope, dark, lift); relief += u_folds * slope; reliefAmount += 1. * u_folds; From 7f95241a74655eb35df5024d1ae98619c2fe32b4 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 30 Aug 2026 16:33:34 +0200 Subject: [PATCH 159/186] - --- docs/src/app/(shaders)/paper-texture/page.tsx | 3 + docs/src/shader-defs/paper-texture-def.ts | 32 ++- .../src/shaders/paper-texture.tsx | 20 +- packages/shaders/src/shaders/paper-texture.ts | 186 ++++++++++-------- 4 files changed, 159 insertions(+), 82 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 626012512..51be57e43 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -87,6 +87,9 @@ const PaperTextureWithControls = () => { crumpleSoftness: { value: defaults.crumpleSoftness, min: 0, max: 1, order: 322 }, test: { value: defaults.test, min: 0, max: 1, order: 325 }, testSize: { value: defaults.testSize, min: 0, max: 1, order: 326 }, + testLayers: { value: defaults.testLayers, min: 1, max: paperTextureMeta.maxTestLayers, step: 1, order: 327 }, + testDensity: { value: defaults.testDensity, min: 0, max: 1, order: 328 }, + testSoftness: { value: defaults.testSoftness, min: 0, max: 1, order: 329 }, folds: { value: defaults.folds, min: 0, max: 1, order: 330 }, foldSizeX: { value: defaults.foldSizeX, min: 0, max: 1, order: 331 }, foldOffsetX: { value: defaults.foldOffsetX, min: 0, max: 1, order: 332 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 4c04ee313..4f1abc61d 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -123,7 +123,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.test, - description: 'Depth of the crease pattern: flat facets separated by sharp folds, spread evenly over the sheet', + description: 'Depth of a field of fine facets, independent of the crumples', }, { name: 'testSize', @@ -131,7 +131,33 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.testSize, - description: 'Size of the crease facets (needs test > 0)', + description: 'Size of the fine facets (needs test > 0)', + }, + { + name: 'testLayers', + type: 'number', + min: 1, + max: 3, + step: 1, + defaultValue: defaultParams.testLayers, + description: 'Number of facet layers stacked at falling size and depth (needs test > 0)', + }, + { + name: 'testDensity', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.testDensity, + description: 'How many facets survive per layer; low values merge them into fewer, larger ones (needs test > 0)', + }, + { + name: 'testSoftness', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.testSoftness, + description: + 'Softens randomly picked facet borders into gradients, leaving the rest as sharp creases (needs test > 0)', }, { name: 'folds', @@ -196,7 +222,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1000, defaultValue: defaultParams.seed, - description: 'Seed applied to crumples, creases and drops', + description: 'Seed applied to crumples and drops', }, ...staticImageCommonParams, ], diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 7461c14d0..1f029899a 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -40,9 +40,12 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 1, crumpleCount: 6, - crumpleSoftness: 0.2, + crumpleSoftness: 1, test: 0, testSize: 0.65, + testLayers: 2, + testDensity: 0.5, + testSoftness: 0, folds: 0, foldSizeX: 0.6, foldSizeY: 0.44, @@ -77,6 +80,9 @@ export const gridPreset: PaperTexturePreset = { crumpleSoftness: 0, test: 0, testSize: 0.65, + testLayers: 2, + testDensity: 0.5, + testSoftness: 0, folds: 0.25, foldSizeX: 0.6, foldSizeY: 0.89, @@ -111,6 +117,9 @@ export const spreadPreset: PaperTexturePreset = { crumpleSoftness: 0, test: 0, testSize: 0.65, + testLayers: 2, + testDensity: 0.5, + testSoftness: 0, folds: 0.55, foldSizeX: 0, foldSizeY: 0.89, @@ -145,6 +154,9 @@ export const coloredPreset: PaperTexturePreset = { crumpleSoftness: 0, test: 0, testSize: 0.65, + testLayers: 2, + testDensity: 0.5, + testSoftness: 0, folds: 0.43, foldSizeX: 1, foldSizeY: 0.89, @@ -179,6 +191,9 @@ export const PaperTexture: React.FC = memo(function PaperText crumpleSoftness = defaultPreset.params.crumpleSoftness, test = defaultPreset.params.test, testSize = defaultPreset.params.testSize, + testLayers = defaultPreset.params.testLayers, + testDensity = defaultPreset.params.testDensity, + testSoftness = defaultPreset.params.testSoftness, folds = defaultPreset.params.folds, foldSizeX = defaultPreset.params.foldSizeX, foldSizeY = defaultPreset.params.foldSizeY, @@ -232,6 +247,9 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumples: crumples, u_test: test, u_testSize: testSize, + u_testLayers: testLayers, + u_testDensity: testDensity, + u_testSoftness: testSoftness, u_folds: folds, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 095fdfe6c..d03056770 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -4,6 +4,7 @@ import { declarePI, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { maxCrumpleCount: 15, + maxTestLayers: 3, } as const; /** @@ -24,8 +25,11 @@ export const paperTextureMeta = { * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) * - u_crumpleSoftness (float): Softens randomly picked crumple cell borders into gradients, leaving the rest as sharp creases; higher values soften more borders and soften them further, needs u_crumples > 0 (0 to 1) - * - u_test (float): Depth of the crease pattern: flat facets separated by sharp straight folds, spread evenly over the sheet (0 to 1) - * - u_testSize (float): Size of the crease facets, needs u_test > 0 (0 to 1) + * - u_test (float): Depth of a field of fine facets, independent of the crumples (0 to 1) + * - u_testSize (float): Size of the fine facets, needs u_test > 0 (0 to 1) + * - u_testLayers (float): Number of facet layers stacked at falling size and depth, needs u_test > 0 (1 to 3) + * - u_testDensity (float): How many facets survive per layer; low values merge them into fewer, larger ones, needs u_test > 0 (0 to 1) + * - u_testSoftness (float): Softens randomly picked facet borders into gradients, leaving the rest as sharp creases, needs u_test > 0 (0 to 1) * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) * - u_foldSizeX (float): Size of the vertical folds, needs u_folds > 0 (0 to 1) * - u_foldSizeY (float): Size of the horizontal folds, needs u_folds > 0 (0 to 1) @@ -33,7 +37,7 @@ export const paperTextureMeta = { * - u_foldOffsetY (float): Shifts the horizontal folds across the surface, needs u_folds > 0 (0 to 1) * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples, u_folds or u_test > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) - * - u_seed (float): Seed applied to crumples, creases and drops (0 to 1000) + * - u_seed (float): Seed applied to crumples and drops (0 to 1000) * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorBase toned by colorShadow), needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Cuts the paper sheet to the distorted image frame, revealing u_colorBack outside it, needs image @@ -76,6 +80,9 @@ uniform float u_crumpleCount; uniform float u_crumpleSoftness; uniform float u_test; uniform float u_testSize; +uniform float u_testLayers; +uniform float u_testDensity; +uniform float u_testSoftness; uniform float u_folds; uniform float u_foldSizeX; uniform float u_foldSizeY; @@ -225,8 +232,59 @@ float getDrops(vec2 uv) { vec2 getCellTilt(float idx, float radius) { vec2 rand = hash22(vec2(idx + 31., idx * u_seed + 17.)); - float an = floor(rand.x * 24.) / 24. * TWO_PI; - return vec2(cos(an), sin(an)) * mix(.9, 1., rand.y) * mix(.7, 1., radius); + float an = rand.x * TWO_PI; + return vec2(cos(an), sin(an)) * mix(.3, 1.7, rand.y * rand.y) * mix(.7, 1., radius); +} + +vec2 getCrumpleDetail(vec2 uv, float freq, float shift) { + vec2 v = uv * freq; + float pixel = max(length(dFdx(v)), length(dFdy(v))); + vec2 base = floor(v); + float n1 = 9., n2 = 9.; + vec2 s1 = vec2(0.), s2 = vec2(0.), t1 = vec2(0.), t2 = vec2(0.); + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 cell = base + vec2(float(x), float(y)); + vec2 q = hash22(vec2(cell.y * 1.9 + 3.1 + shift, cell.x * 1.3 + .11 * u_seed)); + float keep = mix(.5, .95, u_testDensity); + if (q.y > keep) continue; + + vec2 r = hash22(vec2(cell.x + .37 * u_seed + shift + 11.1, cell.y - .21 * u_seed + 5.7)); + vec2 s = cell + .06 + .88 * r; + float mag = q.y / keep; + float an = q.x * TWO_PI; + vec2 t = vec2(cos(an), sin(an)) * mix(.3, 1.7, mag * mag); + + vec2 d = v - s; + float dsq = dot(d, d); + if (dsq < n1) { + n2 = n1; + s2 = s1; + t2 = t1; + n1 = dsq; + s1 = s; + t1 = t; + } else if (dsq < n2) { + n2 = dsq; + s2 = s; + t2 = t; + } + } + } + + if (n1 > 8.) return vec2(0.); + + vec2 span = s1 - s2; + float toEdge = (n2 - n1) / (2. * max(length(span), 1e-4)); + vec2 pair = hash22(s1 + s2 + 1.7 * abs(s1 - s2)); + float pairSoft = .01 + .4 * u_testSoftness * step(pair.x, .4); + float b = clamp(toEdge / max(1.5 * pixel, .5 * pairSoft), 0., 1.); + float d1 = max(sqrt(n1), 1e-4), d2 = max(sqrt(n2), 1e-4); + vec2 shoulder = max(0., 1. - toEdge / .42) * b * ((v - s2) / d2 - (v - s1) / d1); + + vec2 mid = .5 * (t1 + t2); + return mid + (t1 - mid) * b + .9 * shoulder; } vec4 getCrumples(vec2 uv) { @@ -283,7 +341,7 @@ vec4 getCrumples(vec2 uv) { } float lo = min(idx, float(i)), hi = max(idx, float(i)); - float pairSoft = .002 + u_crumpleSoftness * smoothstep(.4, .8, rand.y); + float pairSoft = .003 + .15 * step(.2, rand.y); float toBisector = (dsq - near) / (2. * max(length(p - nearP), 1e-4)); if (toBisector < toEdge) { @@ -306,63 +364,11 @@ vec4 getCrumples(vec2 uv) { vec2 rounding = mix(sharp, wide / max(wideSum, 1.), min(2. * edgeSoft, 1.)); float radial = l / max(l + toEdge, 1e-4); - float plateau = lst(.45, 1., radial); - plateau = plateau * plateau * (3. - 2. * plateau); - - return vec4(tilt / tiltSum + 1.5 * rounding * blend * plateau, .2 * l, edge); -} - - -float getCreases(vec2 p, out vec2 grad) { - grad = vec2(0.); - float crease = 0.; - float amp = 1.; - float freq = mix(6., 1.5, u_testSize); - float freq0 = freq; - float pixel = 1.5 * max(length(dFdx(p)), length(dFdy(p))) + 1e-6; - vec2 seedOffset = 16. * hash22(vec2(u_seed, u_seed + 1.3)); - - for (int oct = 0; oct < 5; oct++) { - float o = float(oct); - float ca = cos(2.4 * o), sa = sin(2.4 * o); - mat2 rot = mat2(ca, sa, -sa, ca); - float fillet = pixel * freq; - vec2 v = rot * p * freq; - vec2 base = floor(v); - vec2 octGrad = vec2(0.); - - for (int y = -1; y <= 1; y++) { - for (int x = -1; x <= 1; x++) { - vec2 cell = base + vec2(float(x), float(y)); - vec2 r1 = hash22(cell + seedOffset + vec2(7.3, -5.1) * o); - vec2 r2 = r1; - if (r2.y > .85) continue; - - vec2 q = v - cell - .35 - .3 * r1; - float t = dot(q, q) / 1.5625; - if (t >= 1.) continue; - - float an = r2.x * TWO_PI; - vec2 n = vec2(cos(an), sin(an)); - float side = r2.y < .425 ? -1. : 1.; - float depth = amp * (.4 + .6 * r1.x * r1.x); - float falloff = 1. - t; - float w = falloff * falloff; - float d = dot(q, n); - float sm = sqrt(d * d + fillet * fillet); - - octGrad += side * depth * (w * (d / sm) * n - .64 * falloff * sm * q); - if (side < 0.) crease = max(crease, depth * w * fillet * fillet / (d * d + fillet * fillet)); - } - } - - grad += freq * (octGrad * rot); - amp *= .28; - freq *= 1.5; - } + float bendAngle = hash22(vec2(idx + 7.7, idx * u_seed + 2.2)).x * PI; + vec2 axis = vec2(cos(bendAngle), sin(bendAngle)); + vec2 curl = (dot(uv - nearP, axis) / max(l + toEdge, 1e-4)) * axis; - grad /= freq0; - return crease; + return vec4(tilt / tiltSum + curl + rounding * blend * radial, .2 * l, edge); } @@ -400,18 +406,20 @@ void main() { float reliefAmount = 0.; float foldInk = 1.; vec2 crumpleFlow = vec2(0.); - vec2 creaseShift = vec2(0.); - float creaseHighlight = 0.; vec2 warpNoise = vec2(0.); - if (u_crumples > 0. || u_folds > 0.) { + if (u_crumples > 0. || u_folds > 0. || u_test > 0.) { warpNoise = smoothNoise(patternUV * .07 + .2 + .6 * fract(.017 * u_seed)) - .5; } - if (u_crumples > 0.) { + vec2 crumplesUV = vec2(0.); + if (u_crumples > 0. || u_test > 0.) { float crumpleRad = 4. * u_seed; float crumpleCos = cos(crumpleRad), crumpleSin = sin(crumpleRad); - vec2 crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + .012 * warpNoise; + crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + .012 * warpNoise; + } + + if (u_crumples > 0.) { vec4 crumples = getCrumples(crumplesUV); vec2 crumpleTilt = .5 * u_crumples * crumples.xy; @@ -424,27 +432,44 @@ void main() { } if (u_test > 0.) { - vec2 creaseGrad; - creaseHighlight = getCreases(patternUV, creaseGrad); - creaseShift = .55 * u_test * creaseGrad; - relief += creaseShift; - reliefAmount += .35 * u_test; + float detailFreq = mix(6., 1.5, u_testSize); + float detailAmp = .3; + vec2 detailGrad = vec2(0.); + for (int i = 0; i < ${paperTextureMeta.maxTestLayers}; i++) { + if (float(i) >= floor(u_testLayers + .5)) break; + detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i)); + detailAmp *= .5; + detailFreq *= 2.35; + } + + vec2 detailTilt = 1.5 * u_test * detailGrad; + detailTilt -= .65 * max(dot(detailTilt, lightDir), 0.) * lightDir; + relief += detailTilt; + reliefAmount += 1.3 * u_test; } if (u_folds > 0.) { + vec2 foldOffset = vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY); + vec2 foldCount = vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)); + vec2 foldNoise = .002 * warpNoise; vec2 uv = 1. + patternUV + .03 * crumpleFlow; + float xFan = 2. * (v_imageUV.x - .5) * abs(lightDir.y); vec2 slope, dark, lift; - getFolds(uv, vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY), - vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)), .002 * warpNoise, slope, dark, lift); + getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); + + if (u_isImage) { + vec2 shift = u_folds * vec2(lift.y * xFan, lift.x * abs(lightDir.x)); + uv += .022 * u_distortion * shift * vec2(u_imageAspectRatio, 1.); + getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); + } relief += u_folds * slope; reliefAmount += 1. * u_folds; vec2 ink = mix(vec2(.96), vec2(1.), dark); float flatness = dark.x * dark.y; foldInk *= ink.x * ink.y * mix(.5, .3, flatness); - - float xFan = 2. * (v_imageUV.x - .5) * abs(lightDir.y); + foldShift = u_folds * vec2(lift.y * xFan, lift.x * abs(lightDir.x)); patternUV.x -= .0044 * foldShift.x; @@ -459,7 +484,6 @@ void main() { lit = max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk; - pattern -= .3 * u_test * creaseHighlight; } patternUV += .04 * crumpleFlow; @@ -490,7 +514,7 @@ void main() { float notClipped = u_clip ? .1 : 1.; float scaleDistortion = .15 * crumpleDepth; - vec2 linearDistortion = -.022 * foldShift - .03 * creaseShift + notClipped * .05 * lightDir * drops; + vec2 linearDistortion = -.022 * foldShift + notClipped * .05 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; @@ -553,6 +577,9 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_crumpleSoftness: number; u_test: number; u_testSize: number; + u_testLayers: number; + u_testDensity: number; + u_testSoftness: number; u_folds: number; u_foldSizeX: number; u_foldSizeY: number; @@ -580,6 +607,9 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara crumpleSoftness?: number; test?: number; testSize?: number; + testLayers?: number; + testDensity?: number; + testSoftness?: number; folds?: number; foldSizeX?: number; foldSizeY?: number; From dc51c8537c05bba9dc29e54ad8e680ca259ca5f2 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 30 Aug 2026 18:46:32 +0100 Subject: [PATCH 160/186] - --- docs/src/app/(shaders)/paper-texture/page.tsx | 4 - docs/src/shader-defs/paper-texture-def.ts | 35 --------- .../src/shaders/paper-texture.tsx | 28 +------ packages/shaders/src/shaders/paper-texture.ts | 73 ++++++++----------- 4 files changed, 34 insertions(+), 106 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 51be57e43..ab659ea74 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -84,12 +84,8 @@ const PaperTextureWithControls = () => { step: 1, order: 321, }, - crumpleSoftness: { value: defaults.crumpleSoftness, min: 0, max: 1, order: 322 }, test: { value: defaults.test, min: 0, max: 1, order: 325 }, testSize: { value: defaults.testSize, min: 0, max: 1, order: 326 }, - testLayers: { value: defaults.testLayers, min: 1, max: paperTextureMeta.maxTestLayers, step: 1, order: 327 }, - testDensity: { value: defaults.testDensity, min: 0, max: 1, order: 328 }, - testSoftness: { value: defaults.testSoftness, min: 0, max: 1, order: 329 }, folds: { value: defaults.folds, min: 0, max: 1, order: 330 }, foldSizeX: { value: defaults.foldSizeX, min: 0, max: 1, order: 331 }, foldOffsetX: { value: defaults.foldOffsetX, min: 0, max: 1, order: 332 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 4f1abc61d..7b99c111f 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -108,15 +108,6 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.crumpleCount, description: 'Number of crumples (15 max), needs crumples > 0', }, - { - name: 'crumpleSoftness', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.crumpleSoftness, - description: - 'Softens randomly picked crumple cell borders into gradients, leaving the rest as sharp creases (needs crumples > 0)', - }, { name: 'test', type: 'number', @@ -133,32 +124,6 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.testSize, description: 'Size of the fine facets (needs test > 0)', }, - { - name: 'testLayers', - type: 'number', - min: 1, - max: 3, - step: 1, - defaultValue: defaultParams.testLayers, - description: 'Number of facet layers stacked at falling size and depth (needs test > 0)', - }, - { - name: 'testDensity', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.testDensity, - description: 'How many facets survive per layer; low values merge them into fewer, larger ones (needs test > 0)', - }, - { - name: 'testSoftness', - type: 'number', - min: 0, - max: 1, - defaultValue: defaultParams.testSoftness, - description: - 'Softens randomly picked facet borders into gradients, leaving the rest as sharp creases (needs test > 0)', - }, { name: 'folds', type: 'number', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 1f029899a..4deb62044 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -38,14 +38,10 @@ export const defaultPreset: PaperTexturePreset = { roughnessSize: 0.25, fiber: 0, fiberSize: 0.5, - crumples: 1, + crumples: 0, crumpleCount: 6, - crumpleSoftness: 1, - test: 0, + test: 1, testSize: 0.65, - testLayers: 2, - testDensity: 0.5, - testSoftness: 0, folds: 0, foldSizeX: 0.6, foldSizeY: 0.44, @@ -77,12 +73,8 @@ export const gridPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleCount: 12, - crumpleSoftness: 0, test: 0, testSize: 0.65, - testLayers: 2, - testDensity: 0.5, - testSoftness: 0, folds: 0.25, foldSizeX: 0.6, foldSizeY: 0.89, @@ -114,12 +106,8 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.58, crumples: 0.25, crumpleCount: 9, - crumpleSoftness: 0, test: 0, testSize: 0.65, - testLayers: 2, - testDensity: 0.5, - testSoftness: 0, folds: 0.55, foldSizeX: 0, foldSizeY: 0.89, @@ -151,12 +139,8 @@ export const coloredPreset: PaperTexturePreset = { fiberSize: 1, crumples: 0, crumpleCount: 12, - crumpleSoftness: 0, test: 0, testSize: 0.65, - testLayers: 2, - testDensity: 0.5, - testSoftness: 0, folds: 0.43, foldSizeX: 1, foldSizeY: 0.89, @@ -188,12 +172,8 @@ export const PaperTexture: React.FC = memo(function PaperText roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, - crumpleSoftness = defaultPreset.params.crumpleSoftness, test = defaultPreset.params.test, testSize = defaultPreset.params.testSize, - testLayers = defaultPreset.params.testLayers, - testDensity = defaultPreset.params.testDensity, - testSoftness = defaultPreset.params.testSoftness, folds = defaultPreset.params.folds, foldSizeX = defaultPreset.params.foldSizeX, foldSizeY = defaultPreset.params.foldSizeY, @@ -238,7 +218,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_fiber: fiber, u_fiberSize: fiberSize, u_crumpleCount: crumpleCount, - u_crumpleSoftness: crumpleSoftness, u_foldSizeX: foldSizeX, u_foldSizeY: foldSizeY, u_foldOffsetX: foldOffsetX, @@ -247,9 +226,6 @@ export const PaperTexture: React.FC = memo(function PaperText u_crumples: crumples, u_test: test, u_testSize: testSize, - u_testLayers: testLayers, - u_testDensity: testDensity, - u_testSoftness: testSoftness, u_folds: folds, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index d03056770..f601ceb31 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -24,12 +24,8 @@ export const paperTextureMeta = { * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) - * - u_crumpleSoftness (float): Softens randomly picked crumple cell borders into gradients, leaving the rest as sharp creases; higher values soften more borders and soften them further, needs u_crumples > 0 (0 to 1) * - u_test (float): Depth of a field of fine facets, independent of the crumples (0 to 1) * - u_testSize (float): Size of the fine facets, needs u_test > 0 (0 to 1) - * - u_testLayers (float): Number of facet layers stacked at falling size and depth, needs u_test > 0 (1 to 3) - * - u_testDensity (float): How many facets survive per layer; low values merge them into fewer, larger ones, needs u_test > 0 (0 to 1) - * - u_testSoftness (float): Softens randomly picked facet borders into gradients, leaving the rest as sharp creases, needs u_test > 0 (0 to 1) * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) * - u_foldSizeX (float): Size of the vertical folds, needs u_folds > 0 (0 to 1) * - u_foldSizeY (float): Size of the horizontal folds, needs u_folds > 0 (0 to 1) @@ -77,12 +73,8 @@ uniform float u_fiber; uniform float u_fiberSize; uniform float u_crumples; uniform float u_crumpleCount; -uniform float u_crumpleSoftness; uniform float u_test; uniform float u_testSize; -uniform float u_testLayers; -uniform float u_testDensity; -uniform float u_testSoftness; uniform float u_folds; uniform float u_foldSizeX; uniform float u_foldSizeY; @@ -236,7 +228,8 @@ vec2 getCellTilt(float idx, float radius) { return vec2(cos(an), sin(an)) * mix(.3, 1.7, rand.y * rand.y) * mix(.7, 1., radius); } -vec2 getCrumpleDetail(vec2 uv, float freq, float shift) { +vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { + depth = 0.; vec2 v = uv * freq; float pixel = max(length(dFdx(v)), length(dFdy(v))); vec2 base = floor(v); @@ -247,7 +240,7 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift) { for (int x = -1; x <= 1; x++) { vec2 cell = base + vec2(float(x), float(y)); vec2 q = hash22(vec2(cell.y * 1.9 + 3.1 + shift, cell.x * 1.3 + .11 * u_seed)); - float keep = mix(.5, .95, u_testDensity); + float keep = .8; if (q.y > keep) continue; vec2 r = hash22(vec2(cell.x + .37 * u_seed + shift + 11.1, cell.y - .21 * u_seed + 5.7)); @@ -276,11 +269,14 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift) { if (n1 > 8.) return vec2(0.); vec2 span = s1 - s2; - float toEdge = (n2 - n1) / (2. * max(length(span), 1e-4)); + float spanLen = max(length(span), 1e-4); + float toEdge = (n2 - n1) / (2. * spanLen); + vec2 pair = hash22(s1 + s2 + 1.7 * abs(s1 - s2)); - float pairSoft = .01 + .4 * u_testSoftness * step(pair.x, .4); + float pairSoft = .01 + .1 * step(pair.x, .4); float b = clamp(toEdge / max(1.5 * pixel, .5 * pairSoft), 0., 1.); float d1 = max(sqrt(n1), 1e-4), d2 = max(sqrt(n2), 1e-4); + depth = .2 * d1; vec2 shoulder = max(0., 1. - toEdge / .42) * b * ((v - s2) / d2 - (v - s1) / d1); vec2 mid = .5 * (t1 + t2); @@ -341,7 +337,7 @@ vec4 getCrumples(vec2 uv) { } float lo = min(idx, float(i)), hi = max(idx, float(i)); - float pairSoft = .003 + .15 * step(.2, rand.y); + float pairSoft = .003 + .2 * step(.2, rand.y); float toBisector = (dsq - near) / (2. * max(length(p - nearP), 1e-4)); if (toBisector < toEdge) { @@ -380,7 +376,7 @@ void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, o vec2 parity = (1. - 2. * mod(foldIdx, 2.)); slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))) * parity; dark = smoothstep(0., foldWidth * .5, abs(dx)); - lift = pow(1. - clamp(abs(dx) / foldWidth, 0., 1.), vec2(4.)) * parity; + lift = pow(1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.), vec2(4.)) * parity; } vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { @@ -390,10 +386,12 @@ vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { void main() { vec2 patternUV = (v_imageUV - .5) * vec2(u_imageAspectRatio, 1.); + vec2 fromCenter = v_imageUV - .5; float pattern = 0.; float crumpleDepth = 0.; + float testDepth = 0.; vec2 foldShift = vec2(0.); float roughness = 0.; float fiber = 0.; @@ -425,7 +423,7 @@ void main() { vec2 crumpleTilt = .5 * u_crumples * crumples.xy; crumpleTilt -= .65 * max(dot(crumpleTilt, lightDir), 0.) * lightDir; relief += crumpleTilt; - reliefAmount += .5 * u_crumples; + reliefAmount += u_crumples; crumpleFlow = crumples.w * crumpleTilt; crumpleDepth = clamp(5. * crumples.z, 0., 1.) * u_crumples; @@ -433,19 +431,24 @@ void main() { if (u_test > 0.) { float detailFreq = mix(6., 1.5, u_testSize); - float detailAmp = .3; + float detailAmp = .2; vec2 detailGrad = vec2(0.); + float detailDepth = 0., depthSum = 0.; for (int i = 0; i < ${paperTextureMeta.maxTestLayers}; i++) { - if (float(i) >= floor(u_testLayers + .5)) break; - detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i)); - detailAmp *= .5; - detailFreq *= 2.35; + float layerDepth; + detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i), layerDepth); + detailDepth += detailAmp * layerDepth; + depthSum += detailAmp; + detailAmp *= (.5 + .2 * detailGrad.x); + detailFreq *= 2.1; } vec2 detailTilt = 1.5 * u_test * detailGrad; detailTilt -= .65 * max(dot(detailTilt, lightDir), 0.) * lightDir; relief += detailTilt; - reliefAmount += 1.3 * u_test; + reliefAmount += .6 * u_test; + + testDepth = clamp(5. * detailDepth / max(depthSum, 1e-4), 0., 1.); } if (u_folds > 0.) { @@ -453,27 +456,25 @@ void main() { vec2 foldCount = vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)); vec2 foldNoise = .002 * warpNoise; vec2 uv = 1. + patternUV + .03 * crumpleFlow; - float xFan = 2. * (v_imageUV.x - .5) * abs(lightDir.y); vec2 slope, dark, lift; getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); if (u_isImage) { - vec2 shift = u_folds * vec2(lift.y * xFan, lift.x * abs(lightDir.x)); + vec2 shift = u_folds * vec2(lift.y * lightDir.y, lift.x * lightDir.x); uv += .022 * u_distortion * shift * vec2(u_imageAspectRatio, 1.); getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); } - + relief += u_folds * slope; reliefAmount += 1. * u_folds; vec2 ink = mix(vec2(.96), vec2(1.), dark); float flatness = dark.x * dark.y; - foldInk *= ink.x * ink.y * mix(.5, .3, flatness); + foldInk *= mix(1., ink.x * ink.y * mix(.5, .3, flatness), u_folds); - foldShift = u_folds * vec2(lift.y * xFan, lift.x * abs(lightDir.x)); + foldShift = u_folds * vec2(lift.y * lightDir.y, lift.x * lightDir.x); - patternUV.x -= .0044 * foldShift.x; - patternUV.y += .0044 * foldShift.y; + patternUV += .0044 * foldShift; } float unlit = cos(grazing); @@ -513,20 +514,18 @@ void main() { float shadowOpacity = u_colorShadow.a; float notClipped = u_clip ? .1 : 1.; - float scaleDistortion = .15 * crumpleDepth; + float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_test * (testDepth - .5); vec2 linearDistortion = -.022 * foldShift + notClipped * .05 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; vec2 imageUV = .5 + centeredUV * (1. - abs(u_distortion) * radialDistortion * dot(centeredUV, centeredUV)); - - + vec3 color = shadowColor * pattern; float opacity = shadowOpacity * pattern; color += baseColor * (1. - opacity); opacity += baseOpacity * (1. - opacity); - if (u_isImage) { float frame = getUvFrame(imageUV, .001); vec4 image = texture(u_image, imageUV); @@ -543,7 +542,7 @@ void main() { vec3 paper = vec3(1.) - opacity + color; vec3 pic = blendMultiply(image.rgb, paper, u_blending); - pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); + pic = mix(pic, vec3(1.), .6 * pow(dampen, 2. + 3. * pattern)); color = mix(color, pic, frame); opacity = frame + opacity * (1. - frame); @@ -574,12 +573,8 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_fiberSize: number; u_crumples: number; u_crumpleCount: number; - u_crumpleSoftness: number; u_test: number; u_testSize: number; - u_testLayers: number; - u_testDensity: number; - u_testSoftness: number; u_folds: number; u_foldSizeX: number; u_foldSizeY: number; @@ -604,12 +599,8 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara fiberSize?: number; crumples?: number; crumpleCount?: number; - crumpleSoftness?: number; test?: number; testSize?: number; - testLayers?: number; - testDensity?: number; - testSoftness?: number; folds?: number; foldSizeX?: number; foldSizeY?: number; From 4c45cdde8c4ec26329594e52de6c03bc1263af64 Mon Sep 17 00:00:00 2001 From: ksenia Date: Sun, 30 Aug 2026 19:16:15 +0100 Subject: [PATCH 161/186] - --- docs/src/app/(shaders)/paper-texture/page.tsx | 4 +- docs/src/shader-defs/paper-texture-def.ts | 12 +++--- .../src/shaders/paper-texture.tsx | 24 +++++------ packages/shaders/src/shaders/paper-texture.ts | 43 ++++++++++--------- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index ab659ea74..0a5f25706 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -84,8 +84,8 @@ const PaperTextureWithControls = () => { step: 1, order: 321, }, - test: { value: defaults.test, min: 0, max: 1, order: 325 }, - testSize: { value: defaults.testSize, min: 0, max: 1, order: 326 }, + wrinkles: { value: defaults.wrinkles, min: 0, max: 1, order: 325 }, + wrinkleSize: { value: defaults.wrinkleSize, min: 0, max: 1, order: 326 }, folds: { value: defaults.folds, min: 0, max: 1, order: 330 }, foldSizeX: { value: defaults.foldSizeX, min: 0, max: 1, order: 331 }, foldOffsetX: { value: defaults.foldOffsetX, min: 0, max: 1, order: 332 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 7b99c111f..e2f5a6d20 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -109,20 +109,20 @@ export const paperTextureDef: ShaderDef = { description: 'Number of crumples (15 max), needs crumples > 0', }, { - name: 'test', + name: 'wrinkles', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.test, + defaultValue: defaultParams.wrinkles, description: 'Depth of a field of fine facets, independent of the crumples', }, { - name: 'testSize', + name: 'wrinkleSize', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.testSize, - description: 'Size of the fine facets (needs test > 0)', + defaultValue: defaultParams.wrinkleSize, + description: 'Size of the fine facets (needs wrinkles > 0)', }, { name: 'folds', @@ -171,7 +171,7 @@ export const paperTextureDef: ShaderDef = { max: 360, defaultValue: defaultParams.angle, description: - 'Direction the surface is lit from, clockwise from the top of the canvas, needs crumples, folds or test > 0', + 'Direction the surface is lit from, clockwise from the top of the canvas, needs crumples, folds or wrinkles > 0', }, { name: 'drops', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 4deb62044..c044349ec 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -40,8 +40,8 @@ export const defaultPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleCount: 6, - test: 1, - testSize: 0.65, + wrinkles: 1, + wrinkleSize: 0.65, folds: 0, foldSizeX: 0.6, foldSizeY: 0.44, @@ -73,8 +73,8 @@ export const gridPreset: PaperTexturePreset = { fiberSize: 0.5, crumples: 0, crumpleCount: 12, - test: 0, - testSize: 0.65, + wrinkles: 0, + wrinkleSize: 0.65, folds: 0.25, foldSizeX: 0.6, foldSizeY: 0.89, @@ -106,8 +106,8 @@ export const spreadPreset: PaperTexturePreset = { fiberSize: 0.58, crumples: 0.25, crumpleCount: 9, - test: 0, - testSize: 0.65, + wrinkles: 0, + wrinkleSize: 0.65, folds: 0.55, foldSizeX: 0, foldSizeY: 0.89, @@ -139,8 +139,8 @@ export const coloredPreset: PaperTexturePreset = { fiberSize: 1, crumples: 0, crumpleCount: 12, - test: 0, - testSize: 0.65, + wrinkles: 0, + wrinkleSize: 0.65, folds: 0.43, foldSizeX: 1, foldSizeY: 0.89, @@ -172,8 +172,8 @@ export const PaperTexture: React.FC = memo(function PaperText roughnessSize = defaultPreset.params.roughnessSize, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, - test = defaultPreset.params.test, - testSize = defaultPreset.params.testSize, + wrinkles = defaultPreset.params.wrinkles, + wrinkleSize = defaultPreset.params.wrinkleSize, folds = defaultPreset.params.folds, foldSizeX = defaultPreset.params.foldSizeX, foldSizeY = defaultPreset.params.foldSizeY, @@ -224,8 +224,8 @@ export const PaperTexture: React.FC = memo(function PaperText u_foldOffsetY: foldOffsetY, u_angle: angle, u_crumples: crumples, - u_test: test, - u_testSize: testSize, + u_wrinkles: wrinkles, + u_wrinkleSize: wrinkleSize, u_folds: folds, u_drops: drops, u_seed: seed, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index f601ceb31..3a69e1084 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -24,14 +24,14 @@ export const paperTextureMeta = { * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) - * - u_test (float): Depth of a field of fine facets, independent of the crumples (0 to 1) - * - u_testSize (float): Size of the fine facets, needs u_test > 0 (0 to 1) + * - u_wrinkles (float): Depth of a field of fine facets, independent of the crumples (0 to 1) + * - u_wrinkleSize (float): Size of the fine facets, needs u_wrinkles > 0 (0 to 1) * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) * - u_foldSizeX (float): Size of the vertical folds, needs u_folds > 0 (0 to 1) * - u_foldSizeY (float): Size of the horizontal folds, needs u_folds > 0 (0 to 1) * - u_foldOffsetX (float): Shifts the vertical folds across the surface, needs u_folds > 0 (0 to 1) * - u_foldOffsetY (float): Shifts the horizontal folds across the surface, needs u_folds > 0 (0 to 1) - * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples, u_folds or u_test > 0 (0 to 360) + * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples, u_folds or u_wrinkles > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to crumples and drops (0 to 1000) * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorBase toned by colorShadow), needs image (0 to 1) @@ -73,8 +73,8 @@ uniform float u_fiber; uniform float u_fiberSize; uniform float u_crumples; uniform float u_crumpleCount; -uniform float u_test; -uniform float u_testSize; +uniform float u_wrinkles; +uniform float u_wrinkleSize; uniform float u_folds; uniform float u_foldSizeX; uniform float u_foldSizeY; @@ -364,7 +364,7 @@ vec4 getCrumples(vec2 uv) { vec2 axis = vec2(cos(bendAngle), sin(bendAngle)); vec2 curl = (dot(uv - nearP, axis) / max(l + toEdge, 1e-4)) * axis; - return vec4(tilt / tiltSum + curl + rounding * blend * radial, .2 * l, edge); + return vec4(tilt / tiltSum + curl + rounding * blend * radial, radial, edge); } @@ -391,7 +391,7 @@ void main() { float pattern = 0.; float crumpleDepth = 0.; - float testDepth = 0.; + float wrinkleDepth = 0.; vec2 foldShift = vec2(0.); float roughness = 0.; float fiber = 0.; @@ -406,12 +406,12 @@ void main() { vec2 crumpleFlow = vec2(0.); vec2 warpNoise = vec2(0.); - if (u_crumples > 0. || u_folds > 0. || u_test > 0.) { + if (u_crumples > 0. || u_folds > 0. || u_wrinkles > 0.) { warpNoise = smoothNoise(patternUV * .07 + .2 + .6 * fract(.017 * u_seed)) - .5; } vec2 crumplesUV = vec2(0.); - if (u_crumples > 0. || u_test > 0.) { + if (u_crumples > 0. || u_wrinkles > 0.) { float crumpleRad = 4. * u_seed; float crumpleCos = cos(crumpleRad), crumpleSin = sin(crumpleRad); crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + .012 * warpNoise; @@ -423,14 +423,14 @@ void main() { vec2 crumpleTilt = .5 * u_crumples * crumples.xy; crumpleTilt -= .65 * max(dot(crumpleTilt, lightDir), 0.) * lightDir; relief += crumpleTilt; - reliefAmount += u_crumples; + reliefAmount += .6 * u_crumples; crumpleFlow = crumples.w * crumpleTilt; - crumpleDepth = clamp(5. * crumples.z, 0., 1.) * u_crumples; + crumpleDepth = clamp(crumples.z, 0., 1.); } - if (u_test > 0.) { - float detailFreq = mix(6., 1.5, u_testSize); + if (u_wrinkles > 0.) { + float detailFreq = mix(6., 1.5, u_wrinkleSize); float detailAmp = .2; vec2 detailGrad = vec2(0.); float detailDepth = 0., depthSum = 0.; @@ -443,12 +443,12 @@ void main() { detailFreq *= 2.1; } - vec2 detailTilt = 1.5 * u_test * detailGrad; + vec2 detailTilt = 1.5 * u_wrinkles * detailGrad; detailTilt -= .65 * max(dot(detailTilt, lightDir), 0.) * lightDir; relief += detailTilt; - reliefAmount += .6 * u_test; + reliefAmount += .6 * u_wrinkles; - testDepth = clamp(5. * detailDepth / max(depthSum, 1e-4), 0., 1.); + wrinkleDepth = clamp(5. * detailDepth / max(depthSum, 1e-4), 0., 1.); } if (u_folds > 0.) { @@ -514,7 +514,8 @@ void main() { float shadowOpacity = u_colorShadow.a; float notClipped = u_clip ? .1 : 1.; - float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_test * (testDepth - .5); + float scaleDistortion = .1 * u_crumples * (crumpleDepth - .5) * mix(.2, 1., crumpleDepth) + + .15 * u_wrinkles * (wrinkleDepth - .5); vec2 linearDistortion = -.022 * foldShift + notClipped * .05 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); @@ -573,8 +574,8 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_fiberSize: number; u_crumples: number; u_crumpleCount: number; - u_test: number; - u_testSize: number; + u_wrinkles: number; + u_wrinkleSize: number; u_folds: number; u_foldSizeX: number; u_foldSizeY: number; @@ -599,8 +600,8 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara fiberSize?: number; crumples?: number; crumpleCount?: number; - test?: number; - testSize?: number; + wrinkles?: number; + wrinkleSize?: number; folds?: number; foldSizeX?: number; foldSizeY?: number; From 536e21517fffb23ba3c693aab78a3bd4e60a49d9 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 14:14:34 +0100 Subject: [PATCH 162/186] - --- .../src/shaders/paper-texture.tsx | 42 +++++++++---------- packages/shaders/src/shaders/paper-texture.ts | 38 ++++++++--------- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index c044349ec..7e9037cc5 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -34,9 +34,9 @@ export const defaultPreset: PaperTexturePreset = { colorBack: '#d3d2ab', colorBase: '#ffffffb5', colorShadow: '#b3b3b3', - roughness: 0, + roughness: 0.4, roughnessSize: 0.25, - fiber: 0, + fiber: 0.4, fiberSize: 0.5, crumples: 0, crumpleCount: 6, @@ -64,23 +64,23 @@ export const gridPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorBack: '#ffffff', - colorBase: '#ffffff', - colorShadow: '#605b52', - roughness: 0.16, + colorBack: '#d3d2ab', + colorBase: '#ffffffb5', + colorShadow: '#b3b3b3', + roughness: 0.4, roughnessSize: 0.25, - fiber: 0.32, + fiber: 0.4, fiberSize: 0.5, - crumples: 0, + crumples: 1, crumpleCount: 12, wrinkles: 0, wrinkleSize: 0.65, - folds: 0.25, + folds: 0, foldSizeX: 0.6, foldSizeY: 0.89, foldOffsetX: 0.59, foldOffsetY: 1, - angle: 140, + angle: 236, drops: 0, seed: 613, clip: false, @@ -97,26 +97,26 @@ export const spreadPreset: PaperTexturePreset = { scale: 0.9, speed: 0, frame: 0, - colorBack: '#d4d3ab00', - colorBase: '#ffffff', - colorShadow: '#c2c2c2', - roughness: 0.16, + colorBack: '#d3d2ab', + colorBase: '#ffffffb5', + colorShadow: '#b3b3b3', + roughness: 0.4, roughnessSize: 0.25, - fiber: 0.62, - fiberSize: 0.58, - crumples: 0.25, + fiber: 0.4, + fiberSize: 0.5, + crumples: 0, crumpleCount: 9, wrinkles: 0, wrinkleSize: 0.65, - folds: 0.55, + folds: 1, foldSizeX: 0, foldSizeY: 0.89, foldOffsetX: 0, foldOffsetY: 0, - angle: 88, + angle: 236, drops: 0, seed: 563, - clip: true, + clip: false, }, }; @@ -146,7 +146,7 @@ export const coloredPreset: PaperTexturePreset = { foldSizeY: 0.89, foldOffsetX: 0.62, foldOffsetY: 0, - angle: 316, + angle: 236, drops: 1, seed: 476, clip: false, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 3a69e1084..4b0000c3b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -336,7 +336,6 @@ vec4 getCrumples(vec2 uv) { wideSum += shoulder; } - float lo = min(idx, float(i)), hi = max(idx, float(i)); float pairSoft = .003 + .2 * step(.2, rand.y); float toBisector = (dsq - near) / (2. * max(length(p - nearP), 1e-4)); @@ -364,19 +363,23 @@ vec4 getCrumples(vec2 uv) { vec2 axis = vec2(cos(bendAngle), sin(bendAngle)); vec2 curl = (dot(uv - nearP, axis) / max(l + toEdge, 1e-4)) * axis; - return vec4(tilt / tiltSum + curl + rounding * blend * radial, radial, edge); + return vec4(tilt / tiltSum + curl + rounding * blend * radial, .2 * l, edge); } -void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift) { +void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift, out vec2 seam) { vec2 g = (coord - 1.) * count + .5 * offset + noise; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; float foldWidth = .9; vec2 parity = (1. - 2. * mod(foldIdx, 2.)); - slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))) * parity; + slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))); dark = smoothstep(0., foldWidth * .5, abs(dx)); - lift = pow(1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.), vec2(4.)) * parity; + lift = pow(1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.), vec2(3.)) * parity; + vec2 nearCross = smoothstep(vec2(0.), vec2(.3), abs(dx.yx)); + vec2 lineWidth = .02 * count * mix(vec2(2.5), vec2(.8), nearCross); + vec2 line = 1. - smoothstep(vec2(0.), lineWidth, abs(dx)); + slope -= line; } vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { @@ -386,7 +389,6 @@ vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { void main() { vec2 patternUV = (v_imageUV - .5) * vec2(u_imageAspectRatio, 1.); - vec2 fromCenter = v_imageUV - .5; float pattern = 0.; @@ -403,6 +405,7 @@ void main() { vec2 relief = vec2(0.); float reliefAmount = 0.; float foldInk = 1.; + float foldSeam = 0.; vec2 crumpleFlow = vec2(0.); vec2 warpNoise = vec2(0.); @@ -426,7 +429,7 @@ void main() { reliefAmount += .6 * u_crumples; crumpleFlow = crumples.w * crumpleTilt; - crumpleDepth = clamp(crumples.z, 0., 1.); + crumpleDepth = clamp(5. * crumples.z, 0., 1.) * u_crumples; } if (u_wrinkles > 0.) { @@ -454,17 +457,11 @@ void main() { if (u_folds > 0.) { vec2 foldOffset = vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY); vec2 foldCount = vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)); - vec2 foldNoise = .002 * warpNoise; + vec2 foldNoise = .003 * warpNoise * foldCount; vec2 uv = 1. + patternUV + .03 * crumpleFlow; - vec2 slope, dark, lift; - getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); - - if (u_isImage) { - vec2 shift = u_folds * vec2(lift.y * lightDir.y, lift.x * lightDir.x); - uv += .022 * u_distortion * shift * vec2(u_imageAspectRatio, 1.); - getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); - } + vec2 slope, dark, lift, seam; + getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift, seam); relief += u_folds * slope; reliefAmount += 1. * u_folds; @@ -472,8 +469,9 @@ void main() { float flatness = dark.x * dark.y; foldInk *= mix(1., ink.x * ink.y * mix(.5, .3, flatness), u_folds); - foldShift = u_folds * vec2(lift.y * lightDir.y, lift.x * lightDir.x); + foldSeam = u_folds * (1. - (1. - seam.x) * (1. - seam.y)); + foldShift = u_folds * vec2(lift.y * lightDir.y, lift.x * lightDir.x); patternUV += .0044 * foldShift; } @@ -485,6 +483,7 @@ void main() { lit = max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk; + pattern += .15 * foldSeam; } patternUV += .04 * crumpleFlow; @@ -514,9 +513,8 @@ void main() { float shadowOpacity = u_colorShadow.a; float notClipped = u_clip ? .1 : 1.; - float scaleDistortion = .1 * u_crumples * (crumpleDepth - .5) * mix(.2, 1., crumpleDepth) + - .15 * u_wrinkles * (wrinkleDepth - .5); - vec2 linearDistortion = -.022 * foldShift + notClipped * .05 * lightDir * drops; + float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_wrinkles * (wrinkleDepth - .5); + vec2 linearDistortion = -.015 * foldShift + notClipped * .05 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; From 9124fcfee85d718aed4a898e05a2ca7169bff135 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 14:36:50 +0100 Subject: [PATCH 163/186] - --- packages/shaders/src/shaders/paper-texture.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 4b0000c3b..0cb9ca0ff 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -336,9 +336,8 @@ vec4 getCrumples(vec2 uv) { wideSum += shoulder; } - float pairSoft = .003 + .2 * step(.2, rand.y); - - float toBisector = (dsq - near) / (2. * max(length(p - nearP), 1e-4)); + float pairSoft = .01 + .1 * step(.2, rand.x) + .1 * step(.4, rand.y); + float toBisector = (dsq - near) / (.2 * max(length(p - nearP), 1e-4)); if (toBisector < toEdge) { toEdge = toBisector; edgeSoft = pairSoft; @@ -352,18 +351,12 @@ vec4 getCrumples(vec2 uv) { } } - float blend = clamp(toEdge / max(1.5 * pixel, .5 * edgeSoft), 0., 1.); - blend = blend * blend * (3. - 2. * blend); - + float blend = clamp(toEdge / edgeSoft, 0., 1.); vec2 sharp = (1. - edge) * ((uv - nearPb) / max(lb, 1e-4) - dir); vec2 rounding = mix(sharp, wide / max(wideSum, 1.), min(2. * edgeSoft, 1.)); - float radial = l / max(l + toEdge, 1e-4); - float bendAngle = hash22(vec2(idx + 7.7, idx * u_seed + 2.2)).x * PI; - vec2 axis = vec2(cos(bendAngle), sin(bendAngle)); - vec2 curl = (dot(uv - nearP, axis) / max(l + toEdge, 1e-4)) * axis; - - return vec4(tilt / tiltSum + curl + rounding * blend * radial, .2 * l, edge); + + return vec4(tilt / tiltSum + rounding * blend * radial, .2 * l, edge); } @@ -424,7 +417,7 @@ void main() { vec4 crumples = getCrumples(crumplesUV); vec2 crumpleTilt = .5 * u_crumples * crumples.xy; - crumpleTilt -= .65 * max(dot(crumpleTilt, lightDir), 0.) * lightDir; + crumpleTilt -= .1 * max(dot(crumpleTilt, lightDir), 0.) * lightDir; relief += crumpleTilt; reliefAmount += .6 * u_crumples; crumpleFlow = crumples.w * crumpleTilt; @@ -478,11 +471,14 @@ void main() { float unlit = cos(grazing); float lit = unlit; + float lightFalloff = clamp(.5 + dot(v_imageUV - .5, lightDir), 0., 1.); + float lightPower = mix(.5, 1., lightFalloff); + if (reliefAmount > 0.) { float slope = clamp(dot(relief, lightDir), -1.2, 1.2); lit = max(cos(slope + grazing), 0.); - pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk; + pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk * lightPower; pattern += .15 * foldSeam; } From 9ef350d62ecb1d50573987a8a3fab2e60d862394 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 15:50:59 +0100 Subject: [PATCH 164/186] - --- .../src/shaders/paper-texture.tsx | 64 +++++++++---------- packages/shaders/src/shaders/paper-texture.ts | 39 +++++------ 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 7e9037cc5..e89e131e9 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -48,14 +48,14 @@ export const defaultPreset: PaperTexturePreset = { foldOffsetX: 0, foldOffsetY: 0, angle: 300, - drops: 0, + drops: 1, seed: 234, clip: false, }, }; -export const gridPreset: PaperTexturePreset = { - name: 'Grid', +export const creasedPreset: PaperTexturePreset = { + name: 'Creased', params: { ...defaultObjectSizing, blending: 1, @@ -65,14 +65,14 @@ export const gridPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#d3d2ab', - colorBase: '#ffffffb5', - colorShadow: '#b3b3b3', + colorBase: '#ffffff', + colorShadow: '#bcc3cd', roughness: 0.4, roughnessSize: 0.25, fiber: 0.4, fiberSize: 0.5, crumples: 1, - crumpleCount: 12, + crumpleCount: 7, wrinkles: 0, wrinkleSize: 0.65, folds: 0, @@ -82,7 +82,7 @@ export const gridPreset: PaperTexturePreset = { foldOffsetY: 1, angle: 236, drops: 0, - seed: 613, + seed: 13, clip: false, }, }; @@ -92,15 +92,15 @@ export const spreadPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: -0.38, + distortion: -0.5, fit: 'contain', scale: 0.9, speed: 0, frame: 0, - colorBack: '#d3d2ab', + colorBack: '#ffffff00', colorBase: '#ffffffb5', colorShadow: '#b3b3b3', - roughness: 0.4, + roughness: 0.15, roughnessSize: 0.25, fiber: 0.4, fiberSize: 0.5, @@ -109,46 +109,46 @@ export const spreadPreset: PaperTexturePreset = { wrinkles: 0, wrinkleSize: 0.65, folds: 1, - foldSizeX: 0, + foldSizeX: 0.1, foldSizeY: 0.89, foldOffsetX: 0, foldOffsetY: 0, angle: 236, drops: 0, seed: 563, - clip: false, + clip: true, }, }; -export const coloredPreset: PaperTexturePreset = { - name: 'Colored', +export const flatPreset: PaperTexturePreset = { + name: 'Flat', params: { ...defaultObjectSizing, blending: 1, - distortion: -0.5, + distortion: 0, fit: 'contain', scale: 0.9, speed: 0, frame: 0, - colorBack: '#d3d2ab', - colorBase: '#f5b689', - colorShadow: '#7654c9', - roughness: 0, - roughnessSize: 0.08, - fiber: 0.52, + colorBack: '#d4cdab', + colorBase: '#ffffffa8', + colorShadow: '#b3b3b3', + roughness: 1, + roughnessSize: 0.74, + fiber: 0.57, fiberSize: 1, crumples: 0, - crumpleCount: 12, + crumpleCount: 6, wrinkles: 0, - wrinkleSize: 0.65, - folds: 0.43, - foldSizeX: 1, - foldSizeY: 0.89, - foldOffsetX: 0.62, + wrinkleSize: 0, + folds: 0, + foldSizeX: 0, + foldSizeY: 0.44, + foldOffsetX: 0, foldOffsetY: 0, - angle: 236, - drops: 1, - seed: 476, + angle: 300, + drops: 0.56, + seed: 455, clip: false, }, }; @@ -156,8 +156,8 @@ export const coloredPreset: PaperTexturePreset = { export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, spreadPreset, - gridPreset, - coloredPreset, + creasedPreset, + flatPreset, ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 0cb9ca0ff..c99f57b86 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, proceduralHash22 } from '../shader-utils.js'; +import { declarePI, proceduralHash11, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { maxCrumpleCount: 15, @@ -105,6 +105,7 @@ float lst(float edge0, float edge1, float x) { } ${declarePI} +${proceduralHash11} ${proceduralHash22} float getRoughness(vec2 p) { @@ -123,9 +124,9 @@ float getRoughness(vec2 p) { vec2 sum = vec2(0.); float norm = 0., amp = .5; + float freq = exp2(baseLevel * logLac); for (int i = 0; i < 4; i++) { float absIdx = baseLevel + float(i); - float freq = exp2(absIdx * logLac); vec2 qx = size * px * freq; float qy = size * py * freq; @@ -148,6 +149,7 @@ float getRoughness(vec2 p) { sum += amp * wi * n; norm += amp * wi; amp *= .8; + freq *= 2.1; } vec2 r = sum / norm; @@ -165,9 +167,9 @@ float getFiber(vec2 p) { vec2 grad = vec2(0.); float scale = 1.; float amp = 1.; + float freq = pow(1.7, baseLevel); for (int i = 0; i < 5; i++) { float absIdx = baseLevel + float(i); - float freq = pow(1.7, absIdx); vec2 q = size * p * freq; float an = absIdx * .8; @@ -193,6 +195,7 @@ float getFiber(vec2 p) { grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); scale *= 1.7; amp *= .5; + freq *= 1.7; } return clamp(.333 * length(grad), 0., 1.); @@ -284,11 +287,12 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { } vec4 getCrumples(vec2 uv) { + float crumpleN = floor(u_crumpleCount + .5); float near = 9., nearB = 9.; float idx = 0., rad = 0.; vec2 nearP = vec2(0.), nearPb = vec2(0.); for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { - if (float(i) >= floor(u_crumpleCount + .5)) break; + if (float(i) >= crumpleN) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; @@ -311,8 +315,6 @@ vec4 getCrumples(vec2 uv) { vec2 dir = (uv - nearP) / max(l, 1e-4); float edge = lst(0., .5, lb - l); - float pixel = max(length(dFdx(uv)), length(dFdy(uv))); - vec2 tilt = getCellTilt(idx, rad); float tiltSum = 1.; vec2 wide = vec2(0.); @@ -320,7 +322,7 @@ vec4 getCrumples(vec2 uv) { float toEdge = 9., edgeSoft = 0.; for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { - if (float(i) >= floor(u_crumpleCount + .5)) break; + if (float(i) >= crumpleN) break; if (float(i) == idx) continue; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; @@ -364,13 +366,13 @@ void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, o vec2 g = (coord - 1.) * count + .5 * offset + noise; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; - float foldWidth = .9; - vec2 parity = (1. - 2. * mod(foldIdx, 2.)); + float foldWidth = .6; slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))); dark = smoothstep(0., foldWidth * .5, abs(dx)); - lift = pow(1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.), vec2(3.)) * parity; + lift = 1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.); + lift *= lift; vec2 nearCross = smoothstep(vec2(0.), vec2(.3), abs(dx.yx)); - vec2 lineWidth = .02 * count * mix(vec2(2.5), vec2(.8), nearCross); + vec2 lineWidth = .02 * count * mix(vec2(2.5), vec2(.8), nearCross) + .1 * noise; vec2 line = 1. - smoothstep(vec2(0.), lineWidth, abs(dx)); slope -= line; } @@ -388,6 +390,7 @@ void main() { float crumpleDepth = 0.; float wrinkleDepth = 0.; vec2 foldShift = vec2(0.); + float foldDepth = 0.; float roughness = 0.; float fiber = 0.; float drops = 0.; @@ -403,7 +406,7 @@ void main() { vec2 warpNoise = vec2(0.); if (u_crumples > 0. || u_folds > 0. || u_wrinkles > 0.) { - warpNoise = smoothNoise(patternUV * .07 + .2 + .6 * fract(.017 * u_seed)) - .5; + warpNoise = smoothNoise(patternUV * .1 + .2 + .6 * fract(.017 * u_seed)) - .5; } vec2 crumplesUV = vec2(0.); @@ -450,7 +453,7 @@ void main() { if (u_folds > 0.) { vec2 foldOffset = vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY); vec2 foldCount = vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)); - vec2 foldNoise = .003 * warpNoise * foldCount; + vec2 foldNoise = .005 * warpNoise * foldCount; vec2 uv = 1. + patternUV + .03 * crumpleFlow; vec2 slope, dark, lift, seam; @@ -463,18 +466,16 @@ void main() { foldInk *= mix(1., ink.x * ink.y * mix(.5, .3, flatness), u_folds); foldSeam = u_folds * (1. - (1. - seam.x) * (1. - seam.y)); - - foldShift = u_folds * vec2(lift.y * lightDir.y, lift.x * lightDir.x); + foldDepth = u_folds * (lift.y * lightDir.y - lift.x * lightDir.x); patternUV += .0044 * foldShift; } float unlit = cos(grazing); float lit = unlit; - float lightFalloff = clamp(.5 + dot(v_imageUV - .5, lightDir), 0., 1.); - float lightPower = mix(.5, 1., lightFalloff); - if (reliefAmount > 0.) { + float lightFalloff = clamp(.5 + dot(v_imageUV - .5, lightDir), 0., 1.); + float lightPower = mix(.5, 1., lightFalloff); float slope = clamp(dot(relief, lightDir), -1.2, 1.2); lit = max(cos(slope + grazing), 0.); @@ -509,7 +510,7 @@ void main() { float shadowOpacity = u_colorShadow.a; float notClipped = u_clip ? .1 : 1.; - float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_wrinkles * (wrinkleDepth - .5); + float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_wrinkles * (wrinkleDepth - .5) + .05 * (foldDepth); vec2 linearDistortion = -.015 * foldShift + notClipped * .05 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); From 14cd384e557047a2795c142d204f836ace0a0b45 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 16:24:19 +0100 Subject: [PATCH 165/186] colorBase => colorPaper --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 4 ++-- .../shaders-react/src/shaders/paper-texture.tsx | 12 ++++++------ packages/shaders/src/shaders/paper-texture.ts | 14 +++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 0a5f25706..45330b0b1 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -68,7 +68,7 @@ const PaperTextureWithControls = () => { ); return { colorBack: { value: toHsla(defaults.colorBack), order: 100 }, - colorBase: { value: toHsla(defaults.colorBase), order: 101 }, + colorPaper: { value: toHsla(defaults.colorPaper), order: 101 }, colorShadow: { value: toHsla(defaults.colorShadow), order: 102 }, seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 200 }, angle: { value: defaults.angle, min: 0, max: 360, order: 201 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index e2f5a6d20..9927f5395 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -22,9 +22,9 @@ export const paperTextureDef: ShaderDef = { description: 'The color of the bottom layer, behind the sheet; visible where clip cuts the sheet away', }, { - name: 'colorBase', + name: 'colorPaper', type: 'string', - defaultValue: defaultParams.colorBase, + defaultValue: defaultParams.colorPaper, isColor: true, description: 'The color of the paper sheet, usually light; multiplied into the image by blending', }, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index e89e131e9..9291dad21 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -32,7 +32,7 @@ export const defaultPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#d3d2ab', - colorBase: '#ffffffb5', + colorPaper: '#ffffffb5', colorShadow: '#b3b3b3', roughness: 0.4, roughnessSize: 0.25, @@ -65,7 +65,7 @@ export const creasedPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#d3d2ab', - colorBase: '#ffffff', + colorPaper: '#ffffff', colorShadow: '#bcc3cd', roughness: 0.4, roughnessSize: 0.25, @@ -98,7 +98,7 @@ export const spreadPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#ffffff00', - colorBase: '#ffffffb5', + colorPaper: '#ffffffb5', colorShadow: '#b3b3b3', roughness: 0.15, roughnessSize: 0.25, @@ -131,7 +131,7 @@ export const flatPreset: PaperTexturePreset = { speed: 0, frame: 0, colorBack: '#d4cdab', - colorBase: '#ffffffa8', + colorPaper: '#ffffffa8', colorShadow: '#b3b3b3', roughness: 1, roughnessSize: 0.74, @@ -165,7 +165,7 @@ export const PaperTexture: React.FC = memo(function PaperText speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, colorBack = defaultPreset.params.colorBack, - colorBase = defaultPreset.params.colorBase, + colorPaper = defaultPreset.params.colorPaper, colorShadow = defaultPreset.params.colorShadow, image = '', roughness = defaultPreset.params.roughness, @@ -211,7 +211,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_image: image, u_isImage: Boolean(image), u_colorBack: getShaderColorFromString(colorBack), - u_colorBase: getShaderColorFromString(colorBase), + u_colorPaper: getShaderColorFromString(colorPaper), u_colorShadow: getShaderColorFromString(colorShadow), u_roughness: roughness, u_roughnessSize: roughnessSize, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index c99f57b86..24e728317 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -16,7 +16,7 @@ export const paperTextureMeta = { * - u_isImage (bool): Whether a source image was provided * - u_imageAspectRatio (float): Aspect ratio of the source image * - u_colorBack (vec4): Color of the bottom layer, behind the sheet; visible where u_clip cuts the sheet away, in RGBA - * - u_colorBase (vec4): Color of the paper sheet, usually light; multiplied into the image by u_blending, in RGBA + * - u_colorPaper (vec4): Color of the paper sheet, usually light; multiplied into the image by u_blending, in RGBA * - u_colorShadow (vec4): Color used for crumples, folds, grain and speckles, blends into the image, in RGBA * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) @@ -34,7 +34,7 @@ export const paperTextureMeta = { * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples, u_folds or u_wrinkles > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) * - u_seed (float): Seed applied to crumples and drops (0 to 1000) - * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorBase toned by colorShadow), needs image (0 to 1) + * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorPaper toned by colorShadow), needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Cuts the paper sheet to the distorted image frame, revealing u_colorBack outside it, needs image * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture @@ -61,7 +61,7 @@ export const paperTextureFragmentShader: string = `#version 300 es precision mediump float; uniform vec4 u_colorBack; -uniform vec4 u_colorBase; +uniform vec4 u_colorPaper; uniform vec4 u_colorShadow; uniform sampler2D u_image; @@ -504,8 +504,8 @@ void main() { vec3 backColor = u_colorBack.rgb * u_colorBack.a; float backOpacity = u_colorBack.a; - vec3 baseColor = u_colorBase.rgb * u_colorBase.a; - float baseOpacity = u_colorBase.a; + vec3 baseColor = u_colorPaper.rgb * u_colorPaper.a; + float baseOpacity = u_colorPaper.a; vec3 shadowColor = u_colorShadow.rgb * u_colorShadow.a; float shadowOpacity = u_colorShadow.a; @@ -561,7 +561,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_isImage: boolean; u_noiseTexture?: HTMLImageElement; u_colorBack: [number, number, number, number]; - u_colorBase: [number, number, number, number]; + u_colorPaper: [number, number, number, number]; u_colorShadow: [number, number, number, number]; u_roughness: number; u_roughnessSize: number; @@ -587,7 +587,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string; colorBack?: string; - colorBase?: string; + colorPaper?: string; colorShadow?: string; roughness?: number; roughnessSize?: number; From ef4e185baf14284c50dc728034c49a57ddea4621 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 16:28:21 +0100 Subject: [PATCH 166/186] reduce drops shift --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 24e728317..294b5123b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -511,7 +511,7 @@ void main() { float notClipped = u_clip ? .1 : 1.; float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_wrinkles * (wrinkleDepth - .5) + .05 * (foldDepth); - vec2 linearDistortion = -.015 * foldShift + notClipped * .05 * lightDir * drops; + vec2 linearDistortion = -.015 * foldShift + notClipped * .002 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; From ab659d0c47ca934dbb637015c57077ff72a0f121 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 16:58:17 +0100 Subject: [PATCH 167/186] - --- .../src/shaders/paper-texture.tsx | 2 +- packages/shaders/src/shaders/paper-texture.ts | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 9291dad21..db675cbec 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -134,7 +134,7 @@ export const flatPreset: PaperTexturePreset = { colorPaper: '#ffffffa8', colorShadow: '#b3b3b3', roughness: 1, - roughnessSize: 0.74, + roughnessSize: 0.5, fiber: 0.57, fiberSize: 1, crumples: 0, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 294b5123b..ad34050a3 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -4,7 +4,6 @@ import { declarePI, proceduralHash11, proceduralHash22 } from '../shader-utils.j export const paperTextureMeta = { maxCrumpleCount: 15, - maxTestLayers: 3, } as const; /** @@ -291,11 +290,13 @@ vec4 getCrumples(vec2 uv) { float near = 9., nearB = 9.; float idx = 0., rad = 0.; vec2 nearP = vec2(0.), nearPb = vec2(0.); + vec4 seeds[${paperTextureMeta.maxCrumpleCount}]; for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { if (float(i) >= crumpleN) break; vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); float an = rand.x * TWO_PI; vec2 p = vec2(cos(an), sin(an)) * rand.y; + seeds[i] = vec4(p, rand); vec2 d = uv - p; float dsq = dot(d, d); @@ -324,9 +325,9 @@ vec4 getCrumples(vec2 uv) { for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { if (float(i) >= crumpleN) break; if (float(i) == idx) continue; - vec2 rand = hash22(vec2(float(i), float(i) * u_seed)); - float an = rand.x * TWO_PI; - vec2 p = vec2(cos(an), sin(an)) * rand.y; + vec4 seed = seeds[i]; + vec2 p = seed.xy; + vec2 rand = seed.zw; vec2 d = uv - p; float dsq = dot(d, d); @@ -362,7 +363,7 @@ vec4 getCrumples(vec2 uv) { } -void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift, out vec2 seam) { +void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift) { vec2 g = (coord - 1.) * count + .5 * offset + noise; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; @@ -401,7 +402,6 @@ void main() { vec2 relief = vec2(0.); float reliefAmount = 0.; float foldInk = 1.; - float foldSeam = 0.; vec2 crumpleFlow = vec2(0.); vec2 warpNoise = vec2(0.); @@ -433,7 +433,7 @@ void main() { float detailAmp = .2; vec2 detailGrad = vec2(0.); float detailDepth = 0., depthSum = 0.; - for (int i = 0; i < ${paperTextureMeta.maxTestLayers}; i++) { + for (int i = 0; i < 3; i++) { float layerDepth; detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i), layerDepth); detailDepth += detailAmp * layerDepth; @@ -456,8 +456,8 @@ void main() { vec2 foldNoise = .005 * warpNoise * foldCount; vec2 uv = 1. + patternUV + .03 * crumpleFlow; - vec2 slope, dark, lift, seam; - getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift, seam); + vec2 slope, dark, lift; + getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); relief += u_folds * slope; reliefAmount += 1. * u_folds; @@ -465,7 +465,6 @@ void main() { float flatness = dark.x * dark.y; foldInk *= mix(1., ink.x * ink.y * mix(.5, .3, flatness), u_folds); - foldSeam = u_folds * (1. - (1. - seam.x) * (1. - seam.y)); foldDepth = u_folds * (lift.y * lightDir.y - lift.x * lightDir.x); patternUV += .0044 * foldShift; } @@ -480,7 +479,6 @@ void main() { lit = max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk * lightPower; - pattern += .15 * foldSeam; } patternUV += .04 * crumpleFlow; From d4d046dd91df332ab2b81580b5bc2ece3f38e1e9 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 18:45:59 +0100 Subject: [PATCH 168/186] less wrinkles distortion --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index ad34050a3..55098747e 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -508,7 +508,7 @@ void main() { float shadowOpacity = u_colorShadow.a; float notClipped = u_clip ? .1 : 1.; - float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .15 * u_wrinkles * (wrinkleDepth - .5) + .05 * (foldDepth); + float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .09 * u_wrinkles * (wrinkleDepth - .5) + .05 * (foldDepth); vec2 linearDistortion = -.015 * foldShift + notClipped * .002 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); From 199ead363af7f10410ffac0202e37293baaef581 Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 18:58:50 +0100 Subject: [PATCH 169/186] fold lines AA --- packages/shaders/src/shaders/paper-texture.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 55098747e..6d4ce86de 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -364,11 +364,12 @@ vec4 getCrumples(vec2 uv) { void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift) { - vec2 g = (coord - 1.) * count + .5 * offset + noise; + vec2 g = coord * count + .5 * offset + noise; vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; float foldWidth = .6; - slope = sign(dx) * (1. - smoothstep(0., .5 * foldWidth, abs(dx))); + vec2 crease = clamp(dx / max(fwidth(g), 1e-5), -1., 1.); + slope = crease * (1. - smoothstep(0., .5 * foldWidth, abs(dx))); dark = smoothstep(0., foldWidth * .5, abs(dx)); lift = 1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.); lift *= lift; @@ -454,7 +455,7 @@ void main() { vec2 foldOffset = vec2(1. - 2. * u_foldOffsetX, 1. - 2. * u_foldOffsetY); vec2 foldCount = vec2(mix(3., .5, u_foldSizeX), mix(3., .5, u_foldSizeY)); vec2 foldNoise = .005 * warpNoise * foldCount; - vec2 uv = 1. + patternUV + .03 * crumpleFlow; + vec2 uv = patternUV + .03 * crumpleFlow; vec2 slope, dark, lift; getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); From 3650c2f2a567636b30ab7332bfb384f57bedc72b Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 19:08:54 +0100 Subject: [PATCH 170/186] img frame AA --- packages/shaders/src/shaders/paper-texture.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 6d4ce86de..9664e305e 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -91,12 +91,11 @@ uniform sampler2D u_noiseTexture; in vec2 v_imageUV; out vec4 fragColor; -float getUvFrame(vec2 uv, float blur) { - float left = smoothstep(0., blur, uv.x); - float right = 1. - smoothstep(1. - blur, 1., uv.x); - float bottom = smoothstep(0., blur, uv.y); - float top = 1. - smoothstep(1. - blur, 1., uv.y); - return left * right * bottom * top; +float getUvFrame(vec2 uv) { + vec2 invAA = .5 / clamp(fwidth(uv), 1e-5, .02); + vec2 lo = clamp(uv * invAA + .5, 0., 1.); + vec2 hi = clamp((1. - uv) * invAA + .5, 0., 1.); + return lo.x * hi.x * lo.y * hi.y; } float lst(float edge0, float edge1, float x) { @@ -522,7 +521,7 @@ void main() { opacity += baseOpacity * (1. - opacity); if (u_isImage) { - float frame = getUvFrame(imageUV, .001); + float frame = getUvFrame(imageUV); vec4 image = texture(u_image, imageUV); frame *= image.a; From 81d0bdb2d77f1998950834307839060027a2a0dc Mon Sep 17 00:00:00 2001 From: ksenia Date: Mon, 31 Aug 2026 19:33:56 +0100 Subject: [PATCH 171/186] drops always dark --- packages/shaders/src/shaders/paper-texture.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 9664e305e..797b73113 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -495,7 +495,6 @@ void main() { if (u_drops > 0.) { drops = u_drops * getDrops(patternUV * 10.); - pattern += drops; } pattern = clamp(pattern, 0., 1.); @@ -547,6 +546,8 @@ void main() { } } + color *= mix(vec3(1.), .5 * u_colorShadow.rgb, drops); + color += backColor * (1. - opacity); opacity += backOpacity * (1. - opacity); From 40e92c381f7e4afddaa56a5cad3ed350e1e61840 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 13:54:22 +0100 Subject: [PATCH 172/186] roughnessRows --- docs/src/app/(shaders)/paper-texture/page.tsx | 1 + docs/src/shader-defs/paper-texture-def.ts | 9 ++++ .../src/shaders/paper-texture.tsx | 41 +++++++++++++++++++ packages/shaders/src/shaders/paper-texture.ts | 11 ++++- 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 45330b0b1..4ca0adc87 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -74,6 +74,7 @@ const PaperTextureWithControls = () => { angle: { value: defaults.angle, min: 0, max: 360, order: 201 }, roughness: { value: defaults.roughness, min: 0, max: 1, order: 300 }, roughnessSize: { value: defaults.roughnessSize, min: 0, max: 1, order: 301 }, + roughnessRows: { value: defaults.roughnessRows, min: 0, max: 1, order: 302 }, fiber: { value: defaults.fiber, min: 0, max: 1, order: 310 }, fiberSize: { value: defaults.fiberSize, min: 0, max: 1, order: 311 }, crumples: { value: defaults.crumples, min: 0, max: 1, order: 320 }, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 9927f5395..823cc4f3b 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -75,6 +75,15 @@ export const paperTextureDef: ShaderDef = { defaultValue: defaultParams.roughnessSize, description: 'Scale of the roughness noise (needs roughness > 0)', }, + { + name: 'roughnessRows', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.roughnessRows, + description: + 'Lines the grain up into horizontal rows: 0 = even scatter, 1 = laid-paper rows (needs roughness > 0)', + }, { name: 'fiber', type: 'number', diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index db675cbec..c968b8527 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -36,6 +36,7 @@ export const defaultPreset: PaperTexturePreset = { colorShadow: '#b3b3b3', roughness: 0.4, roughnessSize: 0.25, + roughnessRows: 0, fiber: 0.4, fiberSize: 0.5, crumples: 0, @@ -69,6 +70,7 @@ export const creasedPreset: PaperTexturePreset = { colorShadow: '#bcc3cd', roughness: 0.4, roughnessSize: 0.25, + roughnessRows: 0, fiber: 0.4, fiberSize: 0.5, crumples: 1, @@ -102,6 +104,7 @@ export const spreadPreset: PaperTexturePreset = { colorShadow: '#b3b3b3', roughness: 0.15, roughnessSize: 0.25, + roughnessRows: 0, fiber: 0.4, fiberSize: 0.5, crumples: 0, @@ -135,6 +138,7 @@ export const flatPreset: PaperTexturePreset = { colorShadow: '#b3b3b3', roughness: 1, roughnessSize: 0.5, + roughnessRows: 0, fiber: 0.57, fiberSize: 1, crumples: 0, @@ -153,7 +157,42 @@ export const flatPreset: PaperTexturePreset = { }, }; +export const testPreset: PaperTexturePreset = { + name: 'test', + params: { + ...defaultObjectSizing, + blending: 1, + distortion: 0, + fit: 'contain', + scale: 0.9, + speed: 0, + frame: 0, + colorBack: '#d4cdab', + colorPaper: '#ffffffa8', + colorShadow: '#000000', + roughness: 1, + roughnessSize: 1, + roughnessRows: 0, + fiber: 0, + fiberSize: 1, + crumples: 0, + crumpleCount: 6, + wrinkles: 0, + wrinkleSize: 0, + folds: 0, + foldSizeX: 0, + foldSizeY: 0.44, + foldOffsetX: 0, + foldOffsetY: 0, + angle: 300, + drops: 0, + seed: 455, + clip: false, + }, +}; + export const paperTexturePresets: PaperTexturePreset[] = [ + testPreset, defaultPreset, spreadPreset, creasedPreset, @@ -170,6 +209,7 @@ export const PaperTexture: React.FC = memo(function PaperText image = '', roughness = defaultPreset.params.roughness, roughnessSize = defaultPreset.params.roughnessSize, + roughnessRows = defaultPreset.params.roughnessRows, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, wrinkles = defaultPreset.params.wrinkles, @@ -215,6 +255,7 @@ export const PaperTexture: React.FC = memo(function PaperText u_colorShadow: getShaderColorFromString(colorShadow), u_roughness: roughness, u_roughnessSize: roughnessSize, + u_roughnessRows: roughnessRows, u_fiber: fiber, u_fiberSize: fiberSize, u_crumpleCount: crumpleCount, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 797b73113..5bbd21688 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -19,6 +19,7 @@ export const paperTextureMeta = { * - u_colorShadow (vec4): Color used for crumples, folds, grain and speckles, blends into the image, in RGBA * - u_roughness (float): Grain noise, sized independently of scaling, with its level of detail depending on the scale (0 to 1) * - u_roughnessSize (float): Scale of the roughness noise, needs u_roughness > 0 (0 to 1) + * - u_roughnessRows (float): Lines the grain up into horizontal rows; 0 = even scatter, 1 = laid-paper rows, needs u_roughness > 0 (0 to 1) * - u_fiber (float): Curly fiber noise, simulating real paper (0 to 1) * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) @@ -85,6 +86,7 @@ uniform float u_seed; uniform float u_blending; uniform float u_distortion; uniform float u_roughnessSize; +uniform float u_roughnessRows; uniform bool u_clip; uniform sampler2D u_noiseTexture; @@ -111,7 +113,7 @@ float getRoughness(vec2 p) { float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); float eps = 4. * w; - float size = mix(3.2, .6, u_roughnessSize); + float size = mix(3.2, .05, u_roughnessSize); float logLac = log2(2.1); float level = -log2(w + 1e-8) / logLac; float baseLevel = floor(level) - 2.; @@ -152,7 +154,10 @@ float getRoughness(vec2 p) { vec2 r = sum / norm; float dx = .5 + r.x - r.y; - return 3. * dx * dx - .7; + float grain = 3. * dx * dx - .7; + + float rowBand = fract(p.y * .05 * size); + return grain + .6 * u_roughnessRows * (rowBand - .5); } float getFiber(vec2 p) { @@ -564,6 +569,7 @@ export interface PaperTextureUniforms extends ShaderSizingUniforms { u_colorShadow: [number, number, number, number]; u_roughness: number; u_roughnessSize: number; + u_roughnessRows: number; u_fiber: number; u_fiberSize: number; u_crumples: number; @@ -590,6 +596,7 @@ export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionPara colorShadow?: string; roughness?: number; roughnessSize?: number; + roughnessRows?: number; fiber?: number; fiberSize?: number; crumples?: number; From 0ecc1477ab743f7e1fde51cc90484eca16ab35fe Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 14:01:44 +0100 Subject: [PATCH 173/186] angle to roughnessRows --- packages/shaders/src/shaders/paper-texture.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 5bbd21688..b5a45dd46 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -108,7 +108,7 @@ ${declarePI} ${proceduralHash11} ${proceduralHash22} -float getRoughness(vec2 p) { +float getRoughness(vec2 p, vec2 lightDir) { vec2 u = p / vec2(2, 4); float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); float eps = 4. * w; @@ -156,7 +156,7 @@ float getRoughness(vec2 p) { float dx = .5 + r.x - r.y; float grain = 3. * dx * dx - .7; - float rowBand = fract(p.y * .05 * size); + float rowBand = fract(dot(p, lightDir) * .05 * size); return grain + .6 * u_roughnessRows * (rowBand - .5); } @@ -489,7 +489,7 @@ void main() { patternUV += .04 * crumpleFlow; if (u_roughness > 0.) { - roughness = u_roughness * getRoughness(1000. * patternUV); + roughness = u_roughness * getRoughness(1000. * patternUV, lightDir); pattern += roughness; } From a951868df764667a90036137c43e883180c50582 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 14:05:06 +0100 Subject: [PATCH 174/186] drops adjustment --- packages/shaders/src/shaders/paper-texture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index b5a45dd46..ec7985803 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -499,7 +499,7 @@ void main() { } if (u_drops > 0.) { - drops = u_drops * getDrops(patternUV * 10.); + drops = .6 * u_drops * getDrops(patternUV * 10.); } pattern = clamp(pattern, 0., 1.); @@ -551,7 +551,7 @@ void main() { } } - color *= mix(vec3(1.), .5 * u_colorShadow.rgb, drops); + color *= mix(vec3(1.), .5 * u_colorShadow.rgb, drops * shadowOpacity); color += backColor * (1. - opacity); opacity += backOpacity * (1. - opacity); From 2e3f573cbea82bf625629abda0100f4cfc5b3f53 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 14:30:46 +0100 Subject: [PATCH 175/186] back to roughness size limits + seed to all patterns + presets --- docs/src/shader-defs/paper-texture-def.ts | 2 +- .../src/shaders/paper-texture.tsx | 51 +++---------------- packages/shaders/src/shaders/paper-texture.ts | 30 ++++++----- 3 files changed, 25 insertions(+), 58 deletions(-) diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 823cc4f3b..cbfb9e9f5 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -196,7 +196,7 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1000, defaultValue: defaultParams.seed, - description: 'Seed applied to crumples and drops', + description: 'Seed applied to every pattern', }, ...staticImageCommonParams, ], diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index c968b8527..4c77d5948 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -67,7 +67,7 @@ export const creasedPreset: PaperTexturePreset = { frame: 0, colorBack: '#d3d2ab', colorPaper: '#ffffff', - colorShadow: '#bcc3cd', + colorShadow: '#b3b3b3', roughness: 0.4, roughnessSize: 0.25, roughnessRows: 0, @@ -94,7 +94,7 @@ export const spreadPreset: PaperTexturePreset = { params: { ...defaultObjectSizing, blending: 1, - distortion: -0.5, + distortion: -0.3, fit: 'contain', scale: 0.9, speed: 0, @@ -105,14 +105,14 @@ export const spreadPreset: PaperTexturePreset = { roughness: 0.15, roughnessSize: 0.25, roughnessRows: 0, - fiber: 0.4, + fiber: 0.5, fiberSize: 0.5, crumples: 0, crumpleCount: 9, wrinkles: 0, wrinkleSize: 0.65, folds: 1, - foldSizeX: 0.1, + foldSizeX: 0.4, foldSizeY: 0.89, foldOffsetX: 0, foldOffsetY: 0, @@ -138,42 +138,8 @@ export const flatPreset: PaperTexturePreset = { colorShadow: '#b3b3b3', roughness: 1, roughnessSize: 0.5, - roughnessRows: 0, - fiber: 0.57, - fiberSize: 1, - crumples: 0, - crumpleCount: 6, - wrinkles: 0, - wrinkleSize: 0, - folds: 0, - foldSizeX: 0, - foldSizeY: 0.44, - foldOffsetX: 0, - foldOffsetY: 0, - angle: 300, - drops: 0.56, - seed: 455, - clip: false, - }, -}; - -export const testPreset: PaperTexturePreset = { - name: 'test', - params: { - ...defaultObjectSizing, - blending: 1, - distortion: 0, - fit: 'contain', - scale: 0.9, - speed: 0, - frame: 0, - colorBack: '#d4cdab', - colorPaper: '#ffffffa8', - colorShadow: '#000000', - roughness: 1, - roughnessSize: 1, - roughnessRows: 0, - fiber: 0, + roughnessRows: 0.6, + fiber: 0.7, fiberSize: 1, crumples: 0, crumpleCount: 6, @@ -184,15 +150,14 @@ export const testPreset: PaperTexturePreset = { foldSizeY: 0.44, foldOffsetX: 0, foldOffsetY: 0, - angle: 300, - drops: 0, + angle: 0, + drops: 0.5, seed: 455, clip: false, }, }; export const paperTexturePresets: PaperTexturePreset[] = [ - testPreset, defaultPreset, spreadPreset, creasedPreset, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index ec7985803..761753568 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -33,7 +33,7 @@ export const paperTextureMeta = { * - u_foldOffsetY (float): Shifts the horizontal folds across the surface, needs u_folds > 0 (0 to 1) * - u_angle (float): Direction the surface is lit from in degrees, clockwise from the top of the canvas, needs u_crumples, u_folds or u_wrinkles > 0 (0 to 360) * - u_drops (float): Visibility of the speckle pattern (0 to 1) - * - u_seed (float): Seed applied to crumples and drops (0 to 1000) + * - u_seed (float): Seed applied to every pattern (0 to 1000) * - u_blending (float): Amount of image-to-paper blending; 0 = original image color, 1 = image multiplied with the paper (colorPaper toned by colorShadow), needs image (0 to 1) * - u_distortion (float): How much the image bends with the paper surface; negative values bend it the opposite direction, needs image (-1 to 1) * - u_clip (bool): Cuts the paper sheet to the distorted image frame, revealing u_colorBack outside it, needs image @@ -108,12 +108,12 @@ ${declarePI} ${proceduralHash11} ${proceduralHash22} -float getRoughness(vec2 p, vec2 lightDir) { +float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift) { vec2 u = p / vec2(2, 4); float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); float eps = 4. * w; - float size = mix(3.2, .05, u_roughnessSize); + float size = mix(3.2, .8, u_roughnessSize); float logLac = log2(2.1); float level = -log2(w + 1e-8) / logLac; float baseLevel = floor(level) - 2.; @@ -136,12 +136,12 @@ float getRoughness(vec2 p, vec2 lightDir) { vec2 fx = fract(qx); float fy = fract(qy); - float shift = .5 + absIdx * .3; - float uvY = floor(qy) / 50. + shift; - vec2 s0a = texture(u_noiseTexture, fract(vec2(floor(qx.x) / 50. + shift, uvY))).rg; - vec2 s1a = texture(u_noiseTexture, fract(vec2( ceil(qx.x) / 50. + shift, uvY))).rg; - vec2 s0b = texture(u_noiseTexture, fract(vec2(floor(qx.y) / 50. + shift, uvY))).rg; - vec2 s1b = texture(u_noiseTexture, fract(vec2( ceil(qx.y) / 50. + shift, uvY))).rg; + vec2 shift = .5 + absIdx * .3 + seedShift; + float uvY = floor(qy) / 50. + shift.y; + vec2 s0a = texture(u_noiseTexture, fract(vec2(floor(qx.x) / 50. + shift.x, uvY))).rg; + vec2 s1a = texture(u_noiseTexture, fract(vec2( ceil(qx.x) / 50. + shift.x, uvY))).rg; + vec2 s0b = texture(u_noiseTexture, fract(vec2(floor(qx.y) / 50. + shift.x, uvY))).rg; + vec2 s1b = texture(u_noiseTexture, fract(vec2( ceil(qx.y) / 50. + shift.x, uvY))).rg; vec2 ny0 = mix(vec2(s0a.r, s0b.r), vec2(s0a.g, s0b.g), fy); vec2 ny1 = mix(vec2(s1a.r, s1b.r), vec2(s1a.g, s1b.g), fy); vec2 n = mix(ny0, ny1, fx); @@ -160,7 +160,7 @@ float getRoughness(vec2 p, vec2 lightDir) { return grain + .6 * u_roughnessRows * (rowBand - .5); } -float getFiber(vec2 p) { +float getFiber(vec2 p, vec2 seedShift) { float size = mix(4., 1., u_fiberSize); float w = max(length(dFdx(p)), length(dFdy(p))); float level = -log2(w + 1e-8); @@ -186,7 +186,7 @@ float getFiber(vec2 p) { vec2 iq = floor(q); vec2 fq = fract(q); float shift = absIdx * .3; - vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift); + vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift + seedShift.xyxy); float aF = texture(u_noiseTexture, uv.xy).b; float bF = texture(u_noiseTexture, uv.zy).b; float cF = texture(u_noiseTexture, uv.xw).b; @@ -434,7 +434,7 @@ void main() { } if (u_wrinkles > 0.) { - float detailFreq = mix(6., 1.5, u_wrinkleSize); + float detailFreq = mix(10., 1., u_wrinkleSize); float detailAmp = .2; vec2 detailGrad = vec2(0.); float detailDepth = 0., depthSum = 0.; @@ -488,13 +488,15 @@ void main() { patternUV += .04 * crumpleFlow; + vec2 seedShift = floor(fract(u_seed * vec2(.7548776662, .5698402909)) * 50.) / 50.; + if (u_roughness > 0.) { - roughness = u_roughness * getRoughness(1000. * patternUV, lightDir); + roughness = u_roughness * getRoughness(1000. * patternUV, lightDir, seedShift); pattern += roughness; } if (u_fiber > 0.) { - fiber = u_fiber * getFiber(50. * patternUV); + fiber = u_fiber * getFiber(50. * patternUV, seedShift); pattern += fiber; } From 127f8e31e74ea6ad7983a60cc1171862ac9df61a Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 14:33:01 +0100 Subject: [PATCH 176/186] GLSL formatting --- packages/shaders/src/shaders/paper-texture.ts | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 761753568..bfeae0487 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -161,47 +161,47 @@ float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift) { } float getFiber(vec2 p, vec2 seedShift) { - float size = mix(4., 1., u_fiberSize); - float w = max(length(dFdx(p)), length(dFdy(p))); - float level = -log2(w + 1e-8); - float baseLevel = floor(level) - 3.; - float fade = fract(level); - - vec2 grad = vec2(0.); - float scale = 1.; - float amp = 1.; - float freq = pow(1.7, baseLevel); - for (int i = 0; i < 5; i++) { - float absIdx = baseLevel + float(i); - vec2 q = size * p * freq; - - float an = absIdx * .8; - float rc = cos(an), rs = sin(an); - q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); - - float wi = 1.; - if (i == 0) wi = 1. - fade; - if (i == 4) wi = fade; - - vec2 iq = floor(q); - vec2 fq = fract(q); - float shift = absIdx * .3; - vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift + seedShift.xyxy); - float aF = texture(u_noiseTexture, uv.xy).b; - float bF = texture(u_noiseTexture, uv.zy).b; - float cF = texture(u_noiseTexture, uv.xw).b; - float dF = texture(u_noiseTexture, uv.zw).b; - vec2 u = fq * fq * (3. - 2. * fq); - vec2 du = 8. * fq * (1. - fq); - float dx = du.x * mix(bF - aF, dF - cF, u.y); - float dy = du.y * mix(cF - aF, dF - bF, u.x); - grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); - scale *= 1.7; - amp *= .5; - freq *= 1.7; - } + float size = mix(4., 1., u_fiberSize); + float w = max(length(dFdx(p)), length(dFdy(p))); + float level = -log2(w + 1e-8); + float baseLevel = floor(level) - 3.; + float fade = fract(level); + + vec2 grad = vec2(0.); + float scale = 1.; + float amp = 1.; + float freq = pow(1.7, baseLevel); + for (int i = 0; i < 5; i++) { + float absIdx = baseLevel + float(i); + vec2 q = size * p * freq; + + float an = absIdx * .8; + float rc = cos(an), rs = sin(an); + q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); + + float wi = 1.; + if (i == 0) wi = 1. - fade; + if (i == 4) wi = fade; + + vec2 iq = floor(q); + vec2 fq = fract(q); + float shift = absIdx * .3; + vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift + seedShift.xyxy); + float aF = texture(u_noiseTexture, uv.xy).b; + float bF = texture(u_noiseTexture, uv.zy).b; + float cF = texture(u_noiseTexture, uv.xw).b; + float dF = texture(u_noiseTexture, uv.zw).b; + vec2 u = fq * fq * (3. - 2. * fq); + vec2 du = 8. * fq * (1. - fq); + float dx = du.x * mix(bF - aF, dF - cF, u.y); + float dy = du.y * mix(cF - aF, dF - bF, u.x); + grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); + scale *= 1.7; + amp *= .5; + freq *= 1.7; + } - return clamp(.333 * length(grad), 0., 1.); + return clamp(.333 * length(grad), 0., 1.); } vec2 smoothNoise(vec2 p) { @@ -362,11 +362,10 @@ vec4 getCrumples(vec2 uv) { vec2 sharp = (1. - edge) * ((uv - nearPb) / max(lb, 1e-4) - dir); vec2 rounding = mix(sharp, wide / max(wideSum, 1.), min(2. * edgeSoft, 1.)); float radial = l / max(l + toEdge, 1e-4); - + return vec4(tilt / tiltSum + rounding * blend * radial, .2 * l, edge); } - void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift) { vec2 g = coord * count + .5 * offset + noise; vec2 foldIdx = floor(g); @@ -451,7 +450,7 @@ void main() { detailTilt -= .65 * max(dot(detailTilt, lightDir), 0.) * lightDir; relief += detailTilt; reliefAmount += .6 * u_wrinkles; - + wrinkleDepth = clamp(5. * detailDepth / max(depthSum, 1e-4), 0., 1.); } @@ -463,7 +462,7 @@ void main() { vec2 slope, dark, lift; getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); - + relief += u_folds * slope; reliefAmount += 1. * u_folds; vec2 ink = mix(vec2(.96), vec2(1.), dark); @@ -503,7 +502,7 @@ void main() { if (u_drops > 0.) { drops = .6 * u_drops * getDrops(patternUV * 10.); } - + pattern = clamp(pattern, 0., 1.); vec3 backColor = u_colorBack.rgb * u_colorBack.a; From ed6e64c534ca56090ec39daf335f3ffeb8d61373 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 15:18:33 +0100 Subject: [PATCH 177/186] crumpleCount start from 2 + drops opt --- docs/src/app/(shaders)/paper-texture/page.tsx | 2 +- docs/src/shader-defs/paper-texture-def.ts | 2 +- packages/shaders/src/shaders/paper-texture.ts | 27 ++++++++++--------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 4ca0adc87..b749503ae 100644 --- a/docs/src/app/(shaders)/paper-texture/page.tsx +++ b/docs/src/app/(shaders)/paper-texture/page.tsx @@ -80,7 +80,7 @@ const PaperTextureWithControls = () => { crumples: { value: defaults.crumples, min: 0, max: 1, order: 320 }, crumpleCount: { value: defaults.crumpleCount, - min: 1, + min: 2, max: paperTextureMeta.maxCrumpleCount, step: 1, order: 321, diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index cbfb9e9f5..22bae073d 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -111,7 +111,7 @@ export const paperTextureDef: ShaderDef = { { name: 'crumpleCount', type: 'number', - min: 1, + min: 2, max: 15, step: 1, defaultValue: defaultParams.crumpleCount, diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index bfeae0487..cefa204ac 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -23,7 +23,7 @@ export const paperTextureMeta = { * - u_fiber (float): Curly fiber noise, simulating real paper (0 to 1) * - u_fiberSize (float): Scale of the fiber noise, needs u_fiber > 0 (0 to 1) * - u_crumples (float): Depth of the centered, irregular field of facets across the sheet (0 to 1) - * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (1 to 15) + * - u_crumpleCount (float): Number of crumples, needs u_crumples > 0 (2 to 15) * - u_wrinkles (float): Depth of a field of fine facets, independent of the crumples (0 to 1) * - u_wrinkleSize (float): Size of the fine facets, needs u_wrinkles > 0 (0 to 1) * - u_folds (float): Depth of the straight folds, alternating between ridges and valleys (0 to 1) @@ -212,7 +212,7 @@ vec2 smoothNoise(vec2 p) { return texture(u_noiseTexture, fract((i + f + .5) / vec2(50.))).rg; } -float getDrops(vec2 uv) { +float getDrops(vec2 uv, vec2 seedShift) { vec2 iDropsUV = floor(uv); vec2 fDropsUV = fract(uv); float dropsMinDist = 1.; @@ -220,12 +220,13 @@ float getDrops(vec2 uv) { for (int x = -1; x < 2; x += 1) { vec2 neighbor = vec2(float(y), float(x)); vec2 offset = hash22(iDropsUV + neighbor); - offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); + if (offset.x > .5) continue; + offset = .5 + .5 * sin(u_seed + TWO_PI * offset); vec2 pos = neighbor + offset - fDropsUV; dropsMinDist *= min(1., dot(pos, pos)); } } - return 1. - lst(.05, .09, sqrt(sqrt(dropsMinDist))); + return .25 * lst(.0004, .0003, dropsMinDist); } vec2 getCellTilt(float idx, float radius) { @@ -290,7 +291,7 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { } vec4 getCrumples(vec2 uv) { - float crumpleN = floor(u_crumpleCount + .5); + float crumpleN = max(2., floor(u_crumpleCount + .5)); float near = 9., nearB = 9.; float idx = 0., rad = 0.; vec2 nearP = vec2(0.), nearPb = vec2(0.); @@ -320,6 +321,8 @@ vec4 getCrumples(vec2 uv) { vec2 dir = (uv - nearP) / max(l, 1e-4); float edge = lst(0., .5, lb - l); + float shoulderLimit = l + .5; + vec2 tilt = getCellTilt(idx, rad); float tiltSum = 1.; vec2 wide = vec2(0.); @@ -335,10 +338,10 @@ vec4 getCrumples(vec2 uv) { vec2 d = uv - p; float dsq = dot(d, d); - float di = sqrt(dsq); - float shoulder = 1. - lst(0., .5, di - l); - if (shoulder > 0.) { + if (dsq < shoulderLimit * shoulderLimit) { + float di = sqrt(dsq); + float shoulder = 1. - lst(0., .5, di - l); wide += shoulder * (d / max(di, 1e-4) - dir); wideSum += shoulder; } @@ -394,7 +397,6 @@ void main() { float crumpleDepth = 0.; float wrinkleDepth = 0.; - vec2 foldShift = vec2(0.); float foldDepth = 0.; float roughness = 0.; float fiber = 0.; @@ -429,7 +431,7 @@ void main() { reliefAmount += .6 * u_crumples; crumpleFlow = crumples.w * crumpleTilt; - crumpleDepth = clamp(5. * crumples.z, 0., 1.) * u_crumples; + crumpleDepth = clamp(5. * crumples.z, 0., 1.); } if (u_wrinkles > 0.) { @@ -470,7 +472,6 @@ void main() { foldInk *= mix(1., ink.x * ink.y * mix(.5, .3, flatness), u_folds); foldDepth = u_folds * (lift.y * lightDir.y - lift.x * lightDir.x); - patternUV += .0044 * foldShift; } float unlit = cos(grazing); @@ -500,7 +501,7 @@ void main() { } if (u_drops > 0.) { - drops = .6 * u_drops * getDrops(patternUV * 10.); + drops = u_drops * getDrops(patternUV * 10., seedShift); } pattern = clamp(pattern, 0., 1.); @@ -514,7 +515,7 @@ void main() { float notClipped = u_clip ? .1 : 1.; float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .09 * u_wrinkles * (wrinkleDepth - .5) + .05 * (foldDepth); - vec2 linearDistortion = -.015 * foldShift + notClipped * .002 * lightDir * drops; + vec2 linearDistortion = notClipped * .002 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; From 370c87012522edc3600e36c1c6862d742903961a Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 15:38:28 +0100 Subject: [PATCH 178/186] drops back to orig --- packages/shaders/src/shaders/paper-texture.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index cefa204ac..74cb2bf5b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -220,13 +220,12 @@ float getDrops(vec2 uv, vec2 seedShift) { for (int x = -1; x < 2; x += 1) { vec2 neighbor = vec2(float(y), float(x)); vec2 offset = hash22(iDropsUV + neighbor); - if (offset.x > .5) continue; - offset = .5 + .5 * sin(u_seed + TWO_PI * offset); + offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); vec2 pos = neighbor + offset - fDropsUV; dropsMinDist *= min(1., dot(pos, pos)); } } - return .25 * lst(.0004, .0003, dropsMinDist); + return .5 * lst(.09, .08, sqrt(sqrt(dropsMinDist))); } vec2 getCellTilt(float idx, float radius) { @@ -241,7 +240,7 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { float pixel = max(length(dFdx(v)), length(dFdy(v))); vec2 base = floor(v); float n1 = 9., n2 = 9.; - vec2 s1 = vec2(0.), s2 = vec2(0.), t1 = vec2(0.), t2 = vec2(0.); + vec2 s1 = vec2(0.), s2 = vec2(0.), q1 = vec2(0.), q2 = vec2(0.); for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { @@ -252,29 +251,31 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { vec2 r = hash22(vec2(cell.x + .37 * u_seed + shift + 11.1, cell.y - .21 * u_seed + 5.7)); vec2 s = cell + .06 + .88 * r; - float mag = q.y / keep; - float an = q.x * TWO_PI; - vec2 t = vec2(cos(an), sin(an)) * mix(.3, 1.7, mag * mag); vec2 d = v - s; float dsq = dot(d, d); if (dsq < n1) { n2 = n1; s2 = s1; - t2 = t1; + q2 = q1; n1 = dsq; s1 = s; - t1 = t; + q1 = q; } else if (dsq < n2) { n2 = dsq; s2 = s; - t2 = t; + q2 = q; } } } if (n1 > 8.) return vec2(0.); + float an1 = q1.x * TWO_PI, mag1 = q1.y / .8; + vec2 t1 = vec2(cos(an1), sin(an1)) * mix(.3, 1.7, mag1 * mag1); + float an2 = q2.x * TWO_PI, mag2 = q2.y / .8; + vec2 t2 = vec2(cos(an2), sin(an2)) * mix(.3, 1.7, mag2 * mag2); + vec2 span = s1 - s2; float spanLen = max(length(span), 1e-4); float toEdge = (n2 - n1) / (2. * spanLen); @@ -282,9 +283,14 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { vec2 pair = hash22(s1 + s2 + 1.7 * abs(s1 - s2)); float pairSoft = .01 + .1 * step(pair.x, .4); float b = clamp(toEdge / max(1.5 * pixel, .5 * pairSoft), 0., 1.); - float d1 = max(sqrt(n1), 1e-4), d2 = max(sqrt(n2), 1e-4); + float d1 = max(sqrt(n1), 1e-4); depth = .2 * d1; - vec2 shoulder = max(0., 1. - toEdge / .42) * b * ((v - s2) / d2 - (v - s1) / d1); + float shoulderAmt = max(0., 1. - toEdge / .42) * b; + vec2 shoulder = vec2(0.); + if (shoulderAmt > 0.) { + float d2 = max(sqrt(n2), 1e-4); + shoulder = shoulderAmt * ((v - s2) / d2 - (v - s1) / d1); + } vec2 mid = .5 * (t1 + t2); return mid + (t1 - mid) * b + .9 * shoulder; From 670bf73eaf6ea5d7f647292e727d75ffcf047d45 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 15:42:51 +0100 Subject: [PATCH 179/186] - --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 74cb2bf5b..fb9790d6b 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -347,7 +347,7 @@ vec4 getCrumples(vec2 uv) { if (dsq < shoulderLimit * shoulderLimit) { float di = sqrt(dsq); - float shoulder = 1. - lst(0., .5, di - l); + float shoulder = lst(.5, 0., di - l); wide += shoulder * (d / max(di, 1e-4) - dir); wideSum += shoulder; } From 8359ae2a08a72230a7fa967f0ed29ffb9a1d70bc Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 16:10:13 +0100 Subject: [PATCH 180/186] opt --- .../src/shaders/paper-texture.tsx | 4 +- packages/shaders/src/shaders/paper-texture.ts | 59 ++++++++----------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index 4c77d5948..21c67d5fc 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -84,7 +84,7 @@ export const creasedPreset: PaperTexturePreset = { foldOffsetY: 1, angle: 236, drops: 0, - seed: 13, + seed: 185, clip: false, }, }; @@ -118,7 +118,7 @@ export const spreadPreset: PaperTexturePreset = { foldOffsetY: 0, angle: 236, drops: 0, - seed: 563, + seed: 613, clip: true, }, }; diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index fb9790d6b..8ee5c138f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -108,9 +108,9 @@ ${declarePI} ${proceduralHash11} ${proceduralHash22} -float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift) { +float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift, float basePixel) { vec2 u = p / vec2(2, 4); - float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); + float w = 100. * basePixel; float eps = 4. * w; float size = mix(3.2, .8, u_roughnessSize); @@ -160,9 +160,9 @@ float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift) { return grain + .6 * u_roughnessRows * (rowBand - .5); } -float getFiber(vec2 p, vec2 seedShift) { +float getFiber(vec2 p, vec2 seedShift, float basePixel) { float size = mix(4., 1., u_fiberSize); - float w = max(length(dFdx(p)), length(dFdy(p))); + float w = 50. * basePixel; float level = -log2(w + 1e-8); float baseLevel = floor(level) - 3.; float fade = fract(level); @@ -234,10 +234,10 @@ vec2 getCellTilt(float idx, float radius) { return vec2(cos(an), sin(an)) * mix(.3, 1.7, rand.y * rand.y) * mix(.7, 1., radius); } -vec2 getCrumpleDetail(vec2 uv, float freq, float shift, out float depth) { +vec2 getCrumpleDetail(vec2 uv, float freq, float shift, float basePixel, out float depth) { depth = 0.; vec2 v = uv * freq; - float pixel = max(length(dFdx(v)), length(dFdy(v))); + float pixel = .9 * freq * basePixel; vec2 base = floor(v); float n1 = 9., n2 = 9.; vec2 s1 = vec2(0.), s2 = vec2(0.), q1 = vec2(0.), q2 = vec2(0.); @@ -352,7 +352,7 @@ vec4 getCrumples(vec2 uv) { wideSum += shoulder; } - float pairSoft = .01 + .1 * step(.2, rand.x) + .1 * step(.4, rand.y); + float pairSoft = .01 + .05 * step(.2, rand.x) + .3 * rand.y; float toBisector = (dsq - near) / (.2 * max(length(p - nearP), 1e-4)); if (toBisector < toEdge) { toEdge = toBisector; @@ -375,29 +375,28 @@ vec4 getCrumples(vec2 uv) { return vec4(tilt / tiltSum + rounding * blend * radial, .2 * l, edge); } -void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, out vec2 slope, out vec2 dark, out vec2 lift) { +void getFolds(vec2 coord, vec2 offset, vec2 count, vec2 noise, vec2 baseFwidth, out vec2 slope, out vec2 dark, out vec2 lift) { vec2 g = coord * count + .5 * offset + noise; - vec2 foldIdx = floor(g); vec2 dx = fract(g) - .5; - float foldWidth = .6; - vec2 crease = clamp(dx / max(fwidth(g), 1e-5), -1., 1.); - slope = crease * (1. - smoothstep(0., .5 * foldWidth, abs(dx))); - dark = smoothstep(0., foldWidth * .5, abs(dx)); - lift = 1. - clamp(abs(dx) / (.5 * foldWidth), 0., 1.); + vec2 adx = abs(dx); + float foldRadius = .3; + vec2 t = clamp(adx / foldRadius, 0., 1.); + dark = t * t * (3. - 2. * t); + lift = 1. - t; lift *= lift; - vec2 nearCross = smoothstep(vec2(0.), vec2(.3), abs(dx.yx)); - vec2 lineWidth = .02 * count * mix(vec2(2.5), vec2(.8), nearCross) + .1 * noise; - vec2 line = 1. - smoothstep(vec2(0.), lineWidth, abs(dx)); - slope -= line; -} -vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { - return base * blend * opacity + base * (1. - opacity); + vec2 crease = clamp(dx / max(count * baseFwidth, 1e-5), -1., 1.); + slope = crease * (1. - dark); + + vec2 lineWidth = .02 * count * mix(vec2(2.5), vec2(.8), dark.yx) + .1 * noise; + slope -= 1. - smoothstep(vec2(0.), lineWidth, adx); } void main() { vec2 patternUV = (v_imageUV - .5) * vec2(u_imageAspectRatio, 1.); + float basePixel = max(length(dFdx(patternUV)), length(dFdy(patternUV))); + vec2 baseFwidth = fwidth(patternUV); float pattern = 0.; @@ -420,13 +419,7 @@ void main() { if (u_crumples > 0. || u_folds > 0. || u_wrinkles > 0.) { warpNoise = smoothNoise(patternUV * .1 + .2 + .6 * fract(.017 * u_seed)) - .5; } - - vec2 crumplesUV = vec2(0.); - if (u_crumples > 0. || u_wrinkles > 0.) { - float crumpleRad = 4. * u_seed; - float crumpleCos = cos(crumpleRad), crumpleSin = sin(crumpleRad); - crumplesUV = mat2(crumpleCos, crumpleSin, -crumpleSin, crumpleCos) * (patternUV * .9) + .012 * warpNoise; - } + vec2 crumplesUV = (patternUV * .9) + .012 * warpNoise; if (u_crumples > 0.) { vec4 crumples = getCrumples(crumplesUV); @@ -447,7 +440,7 @@ void main() { float detailDepth = 0., depthSum = 0.; for (int i = 0; i < 3; i++) { float layerDepth; - detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i), layerDepth); + detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i), basePixel, layerDepth); detailDepth += detailAmp * layerDepth; depthSum += detailAmp; detailAmp *= (.5 + .2 * detailGrad.x); @@ -469,7 +462,7 @@ void main() { vec2 uv = patternUV + .03 * crumpleFlow; vec2 slope, dark, lift; - getFolds(uv, foldOffset, foldCount, foldNoise, slope, dark, lift); + getFolds(uv, foldOffset, foldCount, foldNoise, baseFwidth, slope, dark, lift); relief += u_folds * slope; reliefAmount += 1. * u_folds; @@ -497,12 +490,12 @@ void main() { vec2 seedShift = floor(fract(u_seed * vec2(.7548776662, .5698402909)) * 50.) / 50.; if (u_roughness > 0.) { - roughness = u_roughness * getRoughness(1000. * patternUV, lightDir, seedShift); + roughness = u_roughness * getRoughness(1000. * patternUV, lightDir, seedShift, basePixel); pattern += roughness; } if (u_fiber > 0.) { - fiber = u_fiber * getFiber(50. * patternUV, seedShift); + fiber = u_fiber * getFiber(50. * patternUV, seedShift, basePixel); pattern += fiber; } @@ -547,7 +540,7 @@ void main() { float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); vec3 paper = vec3(1.) - opacity + color; - vec3 pic = blendMultiply(image.rgb, paper, u_blending); + vec3 pic = image.rgb * paper * u_blending + image.rgb * (1. - u_blending); pic = mix(pic, vec3(1.), .6 * pow(dampen, 2. + 3. * pattern)); color = mix(color, pic, frame); From 55a3ef19c51b440c6b32297fcbb4625f227b99a5 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 16:16:50 +0100 Subject: [PATCH 181/186] opt --- packages/shaders/src/shaders/paper-texture.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 8ee5c138f..65fd42b0c 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,6 +1,6 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { declarePI, proceduralHash11, proceduralHash22 } from '../shader-utils.js'; +import { declarePI, proceduralHash21, proceduralHash22 } from '../shader-utils.js'; export const paperTextureMeta = { maxCrumpleCount: 15, @@ -105,7 +105,7 @@ float lst(float edge0, float edge1, float x) { } ${declarePI} -${proceduralHash11} +${proceduralHash21} ${proceduralHash22} float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift, float basePixel) { @@ -280,8 +280,8 @@ vec2 getCrumpleDetail(vec2 uv, float freq, float shift, float basePixel, out flo float spanLen = max(length(span), 1e-4); float toEdge = (n2 - n1) / (2. * spanLen); - vec2 pair = hash22(s1 + s2 + 1.7 * abs(s1 - s2)); - float pairSoft = .01 + .1 * step(pair.x, .4); + float pair = hash21(s1 + s2 + 1.7 * abs(s1 - s2)); + float pairSoft = .01 + .1 * step(pair, .4); float b = clamp(toEdge / max(1.5 * pixel, .5 * pairSoft), 0., 1.); float d1 = max(sqrt(n1), 1e-4); depth = .2 * d1; From 303c9cd20b80baf0d0c29db47957ea51cc36a098 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 20:07:08 +0100 Subject: [PATCH 182/186] wrinkle distortion down to edges --- packages/shaders/src/shaders/paper-texture.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 65fd42b0c..49bc5a647 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -513,10 +513,13 @@ void main() { float shadowOpacity = u_colorShadow.a; float notClipped = u_clip ? .1 : 1.; - float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + .09 * u_wrinkles * (wrinkleDepth - .5) + .05 * (foldDepth); + vec2 imageCenteredUV = v_imageUV - .5; + float edgeDist = 2. * max(abs(imageCenteredUV.x), abs(imageCenteredUV.y)); + float wrinkleDistortion = mix(.35, .0175, smoothstep(0., 1., edgeDist)); + float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + wrinkleDistortion * u_wrinkles * (wrinkleDepth - .5) + .05 * foldDepth; vec2 linearDistortion = notClipped * .002 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); - vec2 centeredUV = (v_imageUV - .5) * (1. - u_distortion * scaleDistortion); + vec2 centeredUV = imageCenteredUV * (1. - u_distortion * scaleDistortion); centeredUV -= u_distortion * linearDistortion; vec2 imageUV = .5 + centeredUV * (1. - abs(u_distortion) * radialDistortion * dot(centeredUV, centeredUV)); From 2ae318610ff66a2988446ec25eb9bea481ad365d Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 20:11:12 +0100 Subject: [PATCH 183/186] wrinkle distortion down to edges --- packages/shaders/src/shaders/paper-texture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 49bc5a647..04cdc5963 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -515,7 +515,7 @@ void main() { float notClipped = u_clip ? .1 : 1.; vec2 imageCenteredUV = v_imageUV - .5; float edgeDist = 2. * max(abs(imageCenteredUV.x), abs(imageCenteredUV.y)); - float wrinkleDistortion = mix(.35, .0175, smoothstep(0., 1., edgeDist)); + float wrinkleDistortion = mix(.38, .0175, smoothstep(0., 1., edgeDist)); float scaleDistortion = .15 * u_crumples * (crumpleDepth - .5) + wrinkleDistortion * u_wrinkles * (wrinkleDepth - .5) + .05 * foldDepth; vec2 linearDistortion = notClipped * .002 * lightDir * drops; float radialDistortion = notClipped * .02 * (roughness + fiber); From 3247ef8f3ad58060ffe7baa017a465548143fd22 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 20:36:05 +0100 Subject: [PATCH 184/186] u_angle to skip the peaks --- packages/shaders/src/shaders/paper-texture.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index 04cdc5963..c6f2027b4 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -408,7 +408,8 @@ void main() { float drops = 0.; float grazing = 0.7; - float lightRad = radians(u_angle); + float angleInSector = mod(u_angle, 90.); + float lightRad = radians(u_angle - angleInSector + mix(12., 78., angleInSector / 90.)); vec2 lightDir = vec2(sin(lightRad), -cos(lightRad)); vec2 relief = vec2(0.); float reliefAmount = 0.; From 821d62d22b20c6b56797879dfd2ed9006aab1654 Mon Sep 17 00:00:00 2001 From: ksenia Date: Tue, 1 Sep 2026 20:43:45 +0100 Subject: [PATCH 185/186] u_angle to skip the peaks for folds --- packages/shaders/src/shaders/paper-texture.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/shaders/src/shaders/paper-texture.ts b/packages/shaders/src/shaders/paper-texture.ts index c6f2027b4..d37c8a7f9 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -408,10 +408,13 @@ void main() { float drops = 0.; float grazing = 0.7; - float angleInSector = mod(u_angle, 90.); - float lightRad = radians(u_angle - angleInSector + mix(12., 78., angleInSector / 90.)); + float lightRad = radians(u_angle); vec2 lightDir = vec2(sin(lightRad), -cos(lightRad)); + float foldAngleInSector = mod(u_angle, 90.); + float foldLightRad = radians(u_angle - foldAngleInSector + mix(12., 78., foldAngleInSector / 90.)); + vec2 foldLightDir = vec2(sin(foldLightRad), -cos(foldLightRad)); vec2 relief = vec2(0.); + vec2 foldSlope = vec2(0.); float reliefAmount = 0.; float foldInk = 1.; vec2 crumpleFlow = vec2(0.); @@ -465,13 +468,13 @@ void main() { vec2 slope, dark, lift; getFolds(uv, foldOffset, foldCount, foldNoise, baseFwidth, slope, dark, lift); - relief += u_folds * slope; + foldSlope = u_folds * slope; reliefAmount += 1. * u_folds; vec2 ink = mix(vec2(.96), vec2(1.), dark); float flatness = dark.x * dark.y; foldInk *= mix(1., ink.x * ink.y * mix(.5, .3, flatness), u_folds); - foldDepth = u_folds * (lift.y * lightDir.y - lift.x * lightDir.x); + foldDepth = u_folds * (lift.y * foldLightDir.y - lift.x * foldLightDir.x); } float unlit = cos(grazing); @@ -480,7 +483,7 @@ void main() { if (reliefAmount > 0.) { float lightFalloff = clamp(.5 + dot(v_imageUV - .5, lightDir), 0., 1.); float lightPower = mix(.5, 1., lightFalloff); - float slope = clamp(dot(relief, lightDir), -1.2, 1.2); + float slope = clamp(dot(relief, lightDir) + dot(foldSlope, foldLightDir), -1.2, 1.2); lit = max(cos(slope + grazing), 0.); pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk * lightPower; From 40b5d4cf06489064e44ce42a7b3402809c611ed7 Mon Sep 17 00:00:00 2001 From: ksenia Date: Wed, 2 Sep 2026 12:01:10 +0100 Subject: [PATCH 186/186] publishing test --- docs/src/shader-defs/paper-texture-def.ts | 2 +- .../shaders/src/shaders/paper-texture---.ts | 512 ------------------ 2 files changed, 1 insertion(+), 513 deletions(-) delete mode 100644 packages/shaders/src/shaders/paper-texture---.ts diff --git a/docs/src/shader-defs/paper-texture-def.ts b/docs/src/shader-defs/paper-texture-def.ts index 22bae073d..8f8f625db 100644 --- a/docs/src/shader-defs/paper-texture-def.ts +++ b/docs/src/shader-defs/paper-texture-def.ts @@ -180,7 +180,7 @@ export const paperTextureDef: ShaderDef = { max: 360, defaultValue: defaultParams.angle, description: - 'Direction the surface is lit from, clockwise from the top of the canvas, needs crumples, folds or wrinkles > 0', + 'Direction the surface is lit from, clockwise from the top of the canvas; also rotation of roughness rows pattern', }, { name: 'drops', diff --git a/packages/shaders/src/shaders/paper-texture---.ts b/packages/shaders/src/shaders/paper-texture---.ts deleted file mode 100644 index 0cee737dc..000000000 --- a/packages/shaders/src/shaders/paper-texture---.ts +++ /dev/null @@ -1,512 +0,0 @@ -// language=GLSL -export const declarePI = ` -#define TWO_PI 6.28318530718 -#define PI 3.14159265358979323846 -`; - -// language=GLSL -export const rotation2 = ` -vec2 rotate(vec2 uv, float th) { - return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv; -} -`; - -// language=GLSL -export const proceduralHash11 = ` - float hash11(float p) { - p = fract(p * 0.3183099) + 0.1; - p *= p + 19.19; - return fract(p * p); - } -`; - -// language=GLSL -export const proceduralHash21 = ` - float hash21(vec2 p) { - p = fract(p * vec2(0.3183099, 0.3678794)) + 0.1; - p += dot(p, p + 19.19); - return fract(p.x * p.y); - } -`; - -// language=GLSL -export const proceduralHash22 = ` - vec2 hash22(vec2 p) { - p = fract(p * vec2(0.3183099, 0.3678794)) + 0.1; - p += dot(p, p.yx + 19.19); - return fract(vec2(p.x * p.y, p.x + p.y)); - } -`; - -// language=GLSL -export const textureRandomizerR = ` - float randomR(vec2 p) { - vec2 uv = floor(p) / 100. + .5; - return texture(u_noiseTexture, fract(uv)).r; - } -`; - -// language=GLSL -export const textureRandomizerGB = ` - vec2 randomGB(vec2 p) { - vec2 uv = floor(p) / 100. + .5; - return texture(u_noiseTexture, fract(uv)).gb; - } -`; - -// language=GLSL -export const colorBandingFix = ` - color += 1. / 256. * (fract(sin(dot(.014 * gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453123) - .5); -`; - -// language=GLSL -export const simplexNoise = ` -vec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); } -float snoise(vec2 v) { - const vec4 C = vec4(0.211324865405187, 0.366025403784439, - -0.577350269189626, 0.024390243902439); - vec2 i = floor(v + dot(v, C.yy)); - vec2 x0 = v - i + dot(i, C.xx); - vec2 i1; - i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); - vec4 x12 = x0.xyxy + C.xxzz; - x12.xy -= i1; - i = mod(i, 289.0); - vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) - + i.x + vec3(0.0, i1.x, 1.0)); - vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), - dot(x12.zw, x12.zw)), 0.0); - m = m * m; - m = m * m; - vec3 x = 2.0 * fract(p * C.www) - 1.0; - vec3 h = abs(x) - 0.5; - vec3 ox = floor(x + 0.5); - vec3 a0 = x - ox; - m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h); - vec3 g; - g.x = a0.x * x0.x + h.x * x0.y; - g.yz = a0.yz * x12.xz + h.yz * x12.yw; - return 130.0 * dot(m, g); -} -`; - -// language=GLSL -export const fiberNoise = ` -float fiberRandom(vec2 p) { - vec2 uv = floor(p) / 100.; - return texture(u_noiseTexture, fract(uv)).b; -} - -float fiberValueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = fiberRandom(i); - float b = fiberRandom(i + vec2(1.0, 0.0)); - float c = fiberRandom(i + vec2(0.0, 1.0)); - float d = fiberRandom(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} - -float fiberNoiseFbm(in vec2 n, vec2 seedOffset) { - float total = 0.0, amplitude = 1.; - for (int i = 0; i < 4; i++) { - n = rotate(n, .7); - total += fiberValueNoise(n + seedOffset) * amplitude; - n *= 2.; - amplitude *= 0.6; - } - return total; -} - -float fiberNoise(vec2 uv, vec2 seedOffset) { - float epsilon = 0.001; - float n1 = fiberNoiseFbm(uv + vec2(epsilon, 0.0), seedOffset); - float n2 = fiberNoiseFbm(uv - vec2(epsilon, 0.0), seedOffset); - float n3 = fiberNoiseFbm(uv + vec2(0.0, epsilon), seedOffset); - float n4 = fiberNoiseFbm(uv - vec2(0.0, epsilon), seedOffset); - return length(vec2(n1 - n2, n3 - n4)) / (2.0 * epsilon); -} -`; - - -// language=GLSL -export const paperTextureeeeFragmentShader: string = `#version 300 es -precision mediump float; - -uniform vec2 u_resulution; -uniform vec4 u_colorFront; -uniform vec4 u_colorBack; - -uniform sampler2D u_image; -uniform float u_imageAspectRatio; - -uniform float u_roughness; -uniform float u_fiber; -uniform float u_fiberSize; -uniform float u_folds; -uniform float u_foldType; -uniform float u_foldCount; -uniform float u_foldSize; -uniform bool u_foldY; -uniform float u_foldsShape; -uniform float u_foldOffset; -uniform float u_drops; -uniform float u_seed; -uniform float u_fade; -uniform float u_blending; -uniform float u_distortion; -uniform float u_roughnessSize; -uniform bool u_background; -uniform sampler2D u_noiseTexture; - -in vec2 v_imageUV; -out vec4 fragColor; - -float getUvFrame(vec2 uv, float blur) { - float left = smoothstep(0., blur, uv.x); - float right = 1. - smoothstep(1. - blur, 1., uv.x); - float bottom = smoothstep(0., blur, uv.y); - float top = 1. - smoothstep(1. - blur, 1., uv.y); - return left * right * bottom * top; -} - -float lst(float edge0, float edge1, float x) { - return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); -} -float sst(float edge0, float edge1, float x) { - return smoothstep(edge0, edge1, x); -} - -${ declarePI } -${ rotation2 } -float randomR(vec2 p) { - vec2 uv = floor(p) / 100. + .5; - return texture(u_noiseTexture, fract(uv)).r; -} -float valueNoise(vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - float a = randomR(i); - float b = randomR(i + vec2(1.0, 0.0)); - float c = randomR(i + vec2(0.0, 1.0)); - float d = randomR(i + vec2(1.0, 1.0)); - vec2 u = f * f * (3.0 - 2.0 * f); - float x1 = mix(a, b, u.x); - float x2 = mix(c, d, u.x); - return mix(x1, x2, u.y); -} -float getFadeMask(vec2 n) { - float total = 0.0, amplitude = .4; - for (int i = 0; i < 2; i++) { - total += valueNoise(n) * amplitude; - n *= 1.99; - amplitude *= 0.65; - } - return total; -} - -float getRoughness(vec2 p) { - vec2 u = p / vec2(2, 4); - float w = max(length(dFdx(p * .1)), length(dFdy(p * .1))); - float eps = 4. * w; - - float size = mix(3.2, .6, u_roughnessSize); - float logLac = log2(2.1); - float level = -log2(w + 1e-8) / logLac; - float baseLevel = floor(level) - 2.; - float fade = fract(level); - - vec2 px = (u.x + vec2(eps, -eps)) * .1; - float py = u.y * .1; - - vec2 sum = vec2(0.); - float norm = 0., amp = .5; - for (int i = 0; i < 4; i++) { - float absIdx = baseLevel + float(i); - float freq = pow(2.1, absIdx); - vec2 qx = size * px * freq; - float qy = size * py * freq; - - float wi = 1.; - if (i == 0) wi = 1. - fade; - if (i == 4) wi = fade; - - vec2 fx = fract(qx); - float fy = fract(qy); - float shift = .5 + absIdx * .3; - float uvY = floor(qy) / 50. + shift; - vec2 s0a = texture(u_noiseTexture, fract(vec2(floor(qx.x) / 50. + shift, uvY))).rg; - vec2 s1a = texture(u_noiseTexture, fract(vec2( ceil(qx.x) / 50. + shift, uvY))).rg; - vec2 s0b = texture(u_noiseTexture, fract(vec2(floor(qx.y) / 50. + shift, uvY))).rg; - vec2 s1b = texture(u_noiseTexture, fract(vec2( ceil(qx.y) / 50. + shift, uvY))).rg; - vec2 ny0 = mix(vec2(s0a.r, s0b.r), vec2(s0a.g, s0b.g), fy); - vec2 ny1 = mix(vec2(s1a.r, s1b.r), vec2(s1a.g, s1b.g), fy); - vec2 n = mix(ny0, ny1, fx); - - sum += amp * wi * n; - norm += amp * wi; - amp *= .8; - } - - vec2 r = sum / norm; - float dx = .5 + r.x - r.y; - return 3. * dx * dx; -} - -float getFiber(vec2 p) { - float size = mix(2., .4, u_fiberSize); - float w = max(length(dFdx(p)), length(dFdy(p))); - float level = -log2(w + 1e-8); - float baseLevel = floor(level) - 3.; - float fade = fract(level); - - vec2 grad = vec2(0.); - float scale = 1.; - float amp = 1.; - for (int i = 0; i < 5; i++) { - float absIdx = baseLevel + float(i); - float freq = pow(1.7, absIdx); - vec2 q = size * p * freq; - - float an = absIdx * .8; - float rc = cos(an), rs = sin(an); - q = vec2(rc * q.x - rs * q.y, rs * q.x + rc * q.y); - - float wi = 1.; - if (i == 0) wi = 1. - fade; - if (i == 4) wi = fade; - - vec2 iq = floor(q); - vec2 fq = fract(q); - float shift = absIdx * .3; - vec4 uv = fract(vec4(iq, iq + 1.) / 50. + .5 + shift); - float aF = texture(u_noiseTexture, uv.xy).b; - float bF = texture(u_noiseTexture, uv.zy).b; - float cF = texture(u_noiseTexture, uv.xw).b; - float dF = texture(u_noiseTexture, uv.zw).b; - vec2 u = fq * fq * (3. - 2. * fq); - vec2 du = 8. * fq * (1. - fq); - float dx = du.x * mix(bF - aF, dF - cF, u.y); - float dy = du.y * mix(cF - aF, dF - bF, u.x); - grad += wi * amp * scale * vec2(rc * dx + rs * dy, -rs * dx + rc * dy); - scale *= 1.7; - amp *= .5; - } - - return min(1., .5 * length(grad)); -} - -vec2 randomGB(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).gb; -} - -float getDrops(vec2 uv) { - vec2 iDropsUV = floor(uv); - vec2 fDropsUV = fract(uv); - float dropsMinDist = 1.; - for (int y = -1; y < 2; y += 1) { - for (int x = -1; x < 2; x += 1) { - vec2 neighbor = vec2(float(y), float(x)); - vec2 offset = randomGB(iDropsUV + neighbor); - offset = .5 + .5 * sin(10. * u_seed + TWO_PI * offset); - vec2 pos = neighbor + offset - fDropsUV; - float dist = length(pos); - dropsMinDist = min(dropsMinDist, dropsMinDist*dist); - } - } - return 1. - lst(.05, .09, sqrt(dropsMinDist)); -} - -vec4 getFolds(vec2 uv1, vec2 uv2) { - vec3 pp1 = vec3(0.), pp2 = vec3(0.); - float l1 = 9., l2 = 9.; - float cellRand = 0.; - for (int i = 0; i < 20; i++) { - if (float(i) >= floor(u_foldCount + .5)) break; - vec2 rand = randomGB(vec2(float(i), float(i) * u_seed)); - float an = rand.x * TWO_PI; - vec2 p = vec2(cos(an), sin(an)) * rand.y; - float dist1 = distance(uv1, p); - if (dist1 < l1) { - l1 = dist1; - pp1 = vec3(uv1.x - p.x, dist1, rand.y); - } - float dist2 = distance(uv2, p); - if (dist2 < l2) { - l2 = dist2; - pp2 = vec3(uv2.x - p.x, dist2, rand.y); - cellRand = .5 * (rand.x + rand.y); - } - } - float mult2 = mix(.22, .02, 1.); - return vec4( - mix(pp1.x, .17 * pp1.z, pow(pp1.y, mult2)), - mix(pp2.x, .18 * pp2.z, pow(pp2.y, mult2)), - .2 * pp2.y, - cellRand - ); -} - -vec4 getCrease(float coord, float offset, float count) { - float g = (coord - 1.) * count + .5 * offset; - float crX = fract(g); - float creaseIdx = floor(g); - float depthMod = .7 + .3 * fract(sin(creaseIdx * 127.1 + u_seed * 3.7) * 43758.5453); - float dx = crX - .5; - float foldWidth = mix(.1, .5, u_foldsShape); - float foldAmount = (1. - smoothstep(0., foldWidth, abs(dx))) * depthMod; - float slope = sign(dx) * foldAmount; - float creaseDark = smoothstep(0., foldWidth * .5, abs(dx)); - return vec4(slope, creaseDark, abs(dx), foldAmount); -} - -vec3 blendMultiply(vec3 base, vec3 blend) { - return base * blend; -} -vec3 blendMultiply(vec3 base, vec3 blend, float opacity) { - return blendMultiply(base, blend) * opacity + base * (1. - opacity); -} - -void main() { - - vec2 imageUV = v_imageUV; - vec2 fromCenter = imageUV - .5; - vec2 patternUV = v_imageUV - .5; - patternUV *= 5. * vec2(u_imageAspectRatio, 1.); - - float pattern = 0.; - float radialDistortion = 0.; - float xDistortion = 0.; - float yShift = 0.; - float scaleDistortion = 0.; - - float fade = u_fade * getFadeMask(.3 * patternUV + 10. * u_seed); - fade = clamp(8. * fade * fade * fade, 0., 1.); - - float drops = 0.; - if (u_drops > 0.) { - drops = getDrops(patternUV * 2.); - drops = mix(drops, 0., fade); - } - - if (u_folds > 0.) { - if (u_foldType < .5) { - vec2 foldsUV1 = rotate(patternUV * .18, 4. * u_seed); - vec2 foldsUV2 = foldsUV1 + .02 * sin(2. * u_seed) * (texture(u_noiseTexture, fract(patternUV * .02 + u_seed)).rg - .5); - vec4 foldsRaw = getFolds(foldsUV1, foldsUV2); - vec4 radialFolds = vec4(clamp(5. * foldsRaw.xyz, 0., 1.), foldsRaw.w); - radialFolds.xyz = mix(radialFolds.xyz, vec3(.5), .4 * fade); - float foldsPattern = radialFolds.x + radialFolds.y; - - pattern += u_folds * foldsPattern; - - vec2 fromCenter = imageUV - .5; - scaleDistortion = .22 * radialFolds.z * u_folds; - } else { - vec2 uv = imageUV + .5; - float countX = mix(25., 1., pow(u_foldSize, .4)); - vec4 h = getCrease(uv.x, 1. - u_foldOffset, countX); - - if (u_foldY) { - vec4 v = getCrease(uv.y, 1., 1.); - float ax = h.x * 1.1345; - float ay = v.x * 1.1345; - float gridDark = h.y * v.y; - float crLight = max(-.7 * sin(ax) - .2 * sin(ay) + .5 * cos(ax) * cos(ay), 0.) * mix(.9, 1., gridDark); - drops *= mix(1., gridDark, u_folds); - - pattern += u_folds * crLight; - float distortBaseH = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); - float distortBaseV = mix(pow(v.y, .2), v.z, .5 * u_foldsShape); - xDistortion += .022 * u_folds * (1. - distortBaseV) * 2. * (imageUV.x - .5); - patternUV.x += .022 * u_folds * (1. - distortBaseV); - yShift += .022 * u_folds * (1. - distortBaseH); - } else { - float angle = h.x * 1.1345; - float crLight = max(-.5 * sin(angle) + .5 * cos(angle), 0.) * mix(.9, 1., h.y); - drops *= mix(1., h.y, u_folds); - - pattern += u_folds * crLight; - float distortBase = mix(pow(h.y, .2), h.z, .5 * u_foldsShape); - yShift += .022 * u_folds * (1. - distortBase); - } - patternUV.y += yShift; - } - } - - if (u_roughness > 0.) { - float roughness = getRoughness(200. * patternUV); - roughness *= mix(1., .3, fade); - roughness *= u_roughness; - pattern += roughness; - radialDistortion += .02 * roughness; - } - - if (u_fiber > 0.) { - float fiber = getFiber(10. * patternUV); - fiber *= mix(1., .3, fade); - fiber *= u_fiber; - pattern += fiber; - radialDistortion += .02 * fiber; - } - - if (u_drops > 0.) { - drops *= u_drops; - pattern += drops; - xDistortion += .03 * drops; - } - - vec3 fgColor = u_colorFront.rgb * u_colorFront.a; - float fgOpacity = u_colorFront.a; - vec3 bgColor = u_colorBack.rgb * u_colorBack.a; - float bgOpacity = u_colorBack.a; - - imageUV = .5 + fromCenter * (1. + u_distortion * scaleDistortion); - imageUV.x += u_distortion * xDistortion; - imageUV.y -= u_distortion * yShift; - vec2 dc = imageUV - .5; - float r2 = dot(dc, dc); - imageUV = .5 + dc * (1. - abs(u_distortion) * radialDistortion * r2); - - float frameSoftness = .002 + .005 * abs(u_distortion) * (.7 * u_fiber + u_roughness); - float frame = getUvFrame(imageUV, frameSoftness); - vec4 image = texture(u_image, imageUV); - frame *= image.a; - - vec3 color = fgColor * pattern; - float opacity = fgOpacity * pattern; - - color += bgColor * (1. - opacity); - opacity += bgOpacity * (1. - opacity); - - float maxC = max(max(image.r, image.g), image.b); - float minC = min(min(image.r, image.g), image.b); - float sat = maxC > 0. ? (maxC - minC) / maxC : 0.; - float midC = image.r + image.g + image.b - maxC - minC; - float secondaryness = maxC > minC ? (midC - minC) / (maxC - minC) : 0.; - float satDampen = sat * (1. - .5 * secondaryness); - float darkDampen = 1. - dot(vec3(.2126, .7152, .0722), image.rgb); - float dampen = mix(0., .7, u_blending) * max(satDampen, darkDampen); - - vec3 pic = blendMultiply(image.rgb, color, u_blending); - pic = mix(pic, vec3(1.), .4 * pow(dampen, 2. + 3. * pattern)); - - color = mix(color, pic, frame); - - if (!u_background) { - vec2 shadowUV = imageUV + vec2(-.01, -.015); - float shadowFrame = getUvFrame(shadowUV, .05) * texture(u_image, shadowUV).a; - float shadow = 1.2 * shadowFrame * (1. - frame); - opacity = opacity * frame + shadow; - color *= frame; - } - frame = mix(frame, 0., .2 * fade); - - - fragColor = vec4(color, opacity); -} -`;