diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 8ce59307..b749503a 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 } 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'; @@ -68,20 +68,37 @@ 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 }, - 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 }, - seed: { value: defaults.seed, min: 0, step: 1, max: 1000, order: 250 }, - scale: { value: defaults.scale, min: 0.1, max: 4, order: 300 }, - fit: { value: defaults.fit, options: ['contain', 'cover'] as ShaderFit[], order: 301 }, + 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 }, + 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 }, + crumpleCount: { + value: defaults.crumpleCount, + min: 2, + max: paperTextureMeta.maxCrumpleCount, + step: 1, + order: 321, + }, + 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 }, + 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 }, + 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 }, 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 935b495d..8f8f625d 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 fold patterns. Works as an image filter or as a standalone texture.', params: [ { name: 'image', @@ -19,22 +19,45 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBack, isColor: true, - description: 'Background color', + description: 'The color of the bottom layer, behind the sheet; visible where clip cuts the sheet away', }, { - name: 'colorFront', + name: 'colorPaper', type: 'string', - defaultValue: defaultParams.colorFront, + defaultValue: defaultParams.colorPaper, isColor: true, - description: 'Foreground color', + description: 'The color of the paper sheet, usually light; multiplied into the image by blending', }, { - name: 'contrast', + name: 'colorShadow', + type: 'string', + defaultValue: defaultParams.colorShadow, + isColor: true, + description: 'The color used for crumples, folds, grain and speckles, blends into the image', + }, + { + name: 'blending', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.contrast, - description: 'Blending behavior (sharper vs. smoother color transitions)', + defaultValue: defaultParams.blending, + description: + 'Amount of image-to-paper blending (0 = original image color, 1 = image multiplied with the paper), needs image', + }, + { + name: 'distortion', + type: 'number', + min: -1, + max: 1, + defaultValue: defaultParams.distortion, + description: + 'How much the image bends with the paper surface; negative values bend it the opposite direction (needs image)', + }, + { + name: 'clip', + type: 'boolean', + defaultValue: defaultParams.clip, + description: 'Cuts the paper sheet to the distorted image frame, revealing colorBack outside it (needs image)', }, { name: 'roughness', @@ -42,7 +65,24 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.roughness, - description: 'Pixel noise, related to canvas (not scalable)', + description: 'Grain noise, sized independently of scaling, with its level of detail depending on the scale', + }, + { + name: 'roughnessSize', + type: 'number', + min: 0, + max: 1, + 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', @@ -50,15 +90,15 @@ 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', type: 'number', - min: 0.01, + 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: 'crumples', @@ -66,15 +106,32 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.crumples, - description: 'Cell-based crumple pattern', + description: 'Depth of the centered, irregular field of facets across the sheet', + }, + { + name: 'crumpleCount', + type: 'number', + min: 2, + max: 15, + step: 1, + defaultValue: defaultParams.crumpleCount, + description: 'Number of crumples (15 max), needs crumples > 0', }, { - name: 'crumpleSize', + name: 'wrinkles', type: 'number', - min: 0.01, + min: 0, + max: 1, + defaultValue: defaultParams.wrinkles, + description: 'Depth of a field of fine facets, independent of the crumples', + }, + { + name: 'wrinkleSize', + type: 'number', + min: 0, max: 1, - defaultValue: defaultParams.crumpleSize, - description: 'Cell-based crumple pattern scale (needs crumples > 0)', + defaultValue: defaultParams.wrinkleSize, + description: 'Size of the fine facets (needs wrinkles > 0)', }, { name: 'folds', @@ -82,24 +139,48 @@ export const paperTextureDef: ShaderDef = { min: 0, max: 1, defaultValue: defaultParams.folds, - description: 'Depth of the folds (needs contrast > 0 to shade the surface)', + description: 'Depth of the straight folds, alternating between ridges and valleys', }, { - name: 'foldCount', + name: 'foldSizeX', type: 'number', - min: 1, - max: 15, - step: 1, - defaultValue: defaultParams.foldCount, - description: 'Number of folds (15 max), needs folds > 0', + min: 0, + max: 1, + defaultValue: defaultParams.foldSizeX, + description: 'Size of the vertical folds (needs folds > 0)', }, { - name: 'fade', + name: 'foldSizeY', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.fade, - description: 'Big-scale noise mask applied to the pattern', + defaultValue: defaultParams.foldSizeY, + description: 'Size of the horizontal folds (needs folds > 0)', + }, + { + name: 'foldOffsetX', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.foldOffsetX, + description: 'Shifts the vertical folds across the surface (needs folds > 0)', + }, + { + name: 'foldOffsetY', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.foldOffsetY, + description: 'Shifts the horizontal folds across the surface (needs folds > 0)', + }, + { + name: 'angle', + type: 'number', + min: 0, + max: 360, + defaultValue: defaultParams.angle, + description: + 'Direction the surface is lit from, clockwise from the top of the canvas; also rotation of roughness rows pattern', }, { name: 'drops', @@ -107,7 +188,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', @@ -115,7 +196,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 every pattern', }, ...staticImageCommonParams, ], diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index a9d97a3b..32e27a07 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -133,6 +133,7 @@ export { grainGradientMeta, meshGradientMeta, metaballsMeta, + paperTextureMeta, pulsingBorderMeta, simplexNoiseMeta, smokeRingMeta, diff --git a/packages/shaders-react/src/shaders/paper-texture.tsx b/packages/shaders-react/src/shaders/paper-texture.tsx index aba693e9..21c67d5f 100644 --- a/packages/shaders-react/src/shaders/paper-texture.tsx +++ b/packages/shaders-react/src/shaders/paper-texture.tsx @@ -15,12 +15,8 @@ import { export interface PaperTextureProps extends ShaderComponentProps, PaperTextureParams { /** @deprecated use `fiberSize` instead */ fiberScale?: number; - /** @deprecated use `crumpleSize` instead */ - crumplesScale?: number; - /** @deprecated use `foldCount` instead */ + /** @deprecated use `crumpleCount` instead */ foldsNumber?: number; - /** @deprecated use `fade` instead */ - blur?: number; } type PaperTexturePreset = ImageShaderPreset; @@ -29,129 +25,177 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - fit: 'cover', - scale: 0.6, + blending: 1, + distortion: 0.75, + fit: 'contain', + scale: 0.9, speed: 0, frame: 0, - colorFront: '#9fadbc', - colorBack: '#ffffff', - contrast: 0.3, + colorBack: '#d3d2ab', + colorPaper: '#ffffffb5', + colorShadow: '#b3b3b3', roughness: 0.4, - fiber: 0.3, - fiberSize: 0.2, - crumples: 0.3, - crumpleSize: 0.35, - folds: 0.65, - foldCount: 5, - fade: 0, - drops: 0.2, - seed: 5.8, + roughnessSize: 0.25, + roughnessRows: 0, + fiber: 0.4, + fiberSize: 0.5, + crumples: 0, + crumpleCount: 6, + wrinkles: 1, + wrinkleSize: 0.65, + folds: 0, + foldSizeX: 0.6, + foldSizeY: 0.44, + foldOffsetX: 0, + foldOffsetY: 0, + angle: 300, + drops: 1, + seed: 234, + clip: false, }, }; -export const abstractPreset: PaperTexturePreset = { - name: 'Abstract', +export const creasedPreset: PaperTexturePreset = { + name: 'Creased', params: { ...defaultObjectSizing, - fit: 'cover', + blending: 1, + distortion: -0.5, + fit: 'contain', + scale: 0.9, speed: 0, frame: 0, - scale: 0.6, - colorFront: '#00eeff', - colorBack: '#ff0a81', - contrast: 0.85, - roughness: 0, - fiber: 0.1, - fiberSize: 0.2, - crumples: 0, - crumpleSize: 0.3, - folds: 1, - foldCount: 3, - fade: 0, - drops: 0.2, - seed: 2.2, + colorBack: '#d3d2ab', + colorPaper: '#ffffff', + colorShadow: '#b3b3b3', + roughness: 0.4, + roughnessSize: 0.25, + roughnessRows: 0, + fiber: 0.4, + fiberSize: 0.5, + crumples: 1, + crumpleCount: 7, + wrinkles: 0, + wrinkleSize: 0.65, + folds: 0, + foldSizeX: 0.6, + foldSizeY: 0.89, + foldOffsetX: 0.59, + foldOffsetY: 1, + angle: 236, + drops: 0, + seed: 185, + clip: false, }, }; -export const cardboardPreset: PaperTexturePreset = { - name: 'Cardboard', +export const spreadPreset: PaperTexturePreset = { + name: 'Spread', params: { ...defaultObjectSizing, - fit: 'cover', + blending: 1, + distortion: -0.3, + fit: 'contain', + scale: 0.9, speed: 0, frame: 0, - scale: 0.6, - colorFront: '#c7b89e', - colorBack: '#999180', - contrast: 0.4, - roughness: 0, - fiber: 0.35, - fiberSize: 0.14, - crumples: 0.7, - crumpleSize: 0.1, - folds: 0, - foldCount: 1, - fade: 0, - drops: 0.1, - seed: 1.6, + colorBack: '#ffffff00', + colorPaper: '#ffffffb5', + colorShadow: '#b3b3b3', + roughness: 0.15, + roughnessSize: 0.25, + roughnessRows: 0, + fiber: 0.5, + fiberSize: 0.5, + crumples: 0, + crumpleCount: 9, + wrinkles: 0, + wrinkleSize: 0.65, + folds: 1, + foldSizeX: 0.4, + foldSizeY: 0.89, + foldOffsetX: 0, + foldOffsetY: 0, + angle: 236, + drops: 0, + seed: 613, + clip: true, }, }; -export const detailsPreset: PaperTexturePreset = { - name: 'Details', +export const flatPreset: PaperTexturePreset = { + name: 'Flat', params: { ...defaultObjectSizing, + blending: 1, + distortion: 0, + fit: 'contain', + scale: 0.9, speed: 0, frame: 0, - fit: 'cover', - scale: 3, - colorFront: '#00000000', - colorBack: '#00000000', - contrast: 0, + colorBack: '#d4cdab', + colorPaper: '#ffffffa8', + colorShadow: '#b3b3b3', roughness: 1, - fiber: 0.27, - fiberSize: 0.22, - crumples: 1, - crumpleSize: 0.5, - folds: 1, - foldCount: 15, - fade: 0, - drops: 0, - seed: 6, + roughnessSize: 0.5, + roughnessRows: 0.6, + fiber: 0.7, + fiberSize: 1, + crumples: 0, + crumpleCount: 6, + wrinkles: 0, + wrinkleSize: 0, + folds: 0, + foldSizeX: 0, + foldSizeY: 0.44, + foldOffsetX: 0, + foldOffsetY: 0, + angle: 0, + drops: 0.5, + seed: 455, + clip: false, }, }; export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, - cardboardPreset, - abstractPreset, - detailsPreset, + spreadPreset, + creasedPreset, + flatPreset, ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ // Own props speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, - colorFront = defaultPreset.params.colorFront, colorBack = defaultPreset.params.colorBack, + colorPaper = defaultPreset.params.colorPaper, + colorShadow = defaultPreset.params.colorShadow, image = '', - contrast = defaultPreset.params.contrast, roughness = defaultPreset.params.roughness, + roughnessSize = defaultPreset.params.roughnessSize, + roughnessRows = defaultPreset.params.roughnessRows, fiber = defaultPreset.params.fiber, crumples = defaultPreset.params.crumples, + wrinkles = defaultPreset.params.wrinkles, + wrinkleSize = defaultPreset.params.wrinkleSize, folds = defaultPreset.params.folds, + 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, + blending = defaultPreset.params.blending, + distortion = defaultPreset.params.distortion, + clip = defaultPreset.params.clip, // 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, - foldCount = foldsNumber === undefined ? defaultPreset.params.foldCount : foldsNumber, + crumpleCount = foldsNumber === undefined ? defaultPreset.params.crumpleCount : foldsNumber, // Sizing props fit = defaultPreset.params.fit, @@ -170,19 +214,30 @@ export const PaperTexture: React.FC = memo(function PaperText const uniforms = { // Own uniforms u_image: image, - u_colorFront: getShaderColorFromString(colorFront), + u_isImage: Boolean(image), u_colorBack: getShaderColorFromString(colorBack), - u_contrast: contrast, + u_colorPaper: getShaderColorFromString(colorPaper), + u_colorShadow: getShaderColorFromString(colorShadow), u_roughness: roughness, + u_roughnessSize: roughnessSize, + u_roughnessRows: roughnessRows, u_fiber: fiber, u_fiberSize: fiberSize, + u_crumpleCount: crumpleCount, + u_foldSizeX: foldSizeX, + u_foldSizeY: foldSizeY, + u_foldOffsetX: foldOffsetX, + u_foldOffsetY: foldOffsetY, + u_angle: angle, u_crumples: crumples, - u_crumpleSize: crumpleSize, - u_foldCount: foldCount, + u_wrinkles: wrinkles, + u_wrinkleSize: wrinkleSize, u_folds: folds, - u_fade: fade, u_drops: drops, u_seed: seed, + u_blending: blending, + u_distortion: distortion, + u_clip: clip, ...noiseTexture, // Sizing uniforms diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index f89812d7..904e32d6 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, type PaperTextureParams, type PaperTextureUniforms, } from './shaders/paper-texture.js'; @@ -182,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 c46e93d4..d37c8a7f 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,29 +1,42 @@ import type { ShaderMotionParams } from '../shader-mount.js'; import { type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; -import { rotation2, declarePI, fiberNoise, textureRandomizerR } from '../shader-utils.js'; +import { declarePI, proceduralHash21, proceduralHash22 } from '../shader-utils.js'; + +export const paperTextureMeta = { + maxCrumpleCount: 15, +} 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_resolution (vec2): Canvas resolution in pixels - * - u_pixelRatio (float): Device pixel ratio * - 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 - * - 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_fiber (float): Curly-shaped noise intensity (0 to 1) - * - u_fiberSize (float): Curly-shaped noise scale, needs fiber > 0 (0.01 to 1) - * - u_crumples (float): Cell-based crumple pattern intensity (0 to 1) - * - u_crumpleSize (float): Cell-based crumple pattern scale, needs crumples > 0 (0.01 to 1) - * - u_folds (float): Depth of the folds, needs contrast > 0 to shade the surface (0 to 1) - * - u_foldCount (float): Number of folds, needs folds > 0 (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_seed (float): Seed applied to folds, crumples, dots and fade (0 to 1000) + * - u_colorBack (vec4): Color of the bottom layer, behind the sheet; visible where u_clip cuts the sheet away, 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) + * - 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) + * - 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) + * - 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_wrinkles > 0 (0 to 360) + * - u_drops (float): Visibility of the speckle pattern (0 to 1) + * - 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 * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -34,10 +47,8 @@ import { rotation2, declarePI, fiberNoise, textureRandomizerR } from '../shader- * - 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.1 to 4) + * - 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) @@ -49,258 +60,509 @@ import { rotation2, declarePI, fiberNoise, textureRandomizerR } from '../shader- 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; +uniform vec4 u_colorPaper; +uniform vec4 u_colorShadow; uniform sampler2D u_image; +uniform bool u_isImage; uniform float u_imageAspectRatio; -uniform float u_contrast; uniform float u_roughness; uniform float u_fiber; uniform float u_fiberSize; uniform float u_crumples; -uniform float u_crumpleSize; +uniform float u_crumpleCount; +uniform float u_wrinkles; +uniform float u_wrinkleSize; uniform float u_folds; -uniform float u_foldCount; +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; -uniform float u_fade; - +uniform float u_blending; +uniform float u_distortion; +uniform float u_roughnessSize; +uniform float u_roughnessRows; +uniform bool u_clip; uniform sampler2D u_noiseTexture; 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); - - return left * right * bottom * top; + 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; } -${ declarePI } -${ rotation2 } -${ textureRandomizerR } -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 lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } -float fbm(vec2 n) { - float total = 0.0, amplitude = .4; - for (int i = 0; i < 3; i++) { - total += valueNoise(n) * amplitude; - n *= 1.99; - amplitude *= 0.65; + +${declarePI} +${proceduralHash21} +${proceduralHash22} + +float getRoughness(vec2 p, vec2 lightDir, vec2 seedShift, float basePixel) { + vec2 u = p / vec2(2, 4); + float w = 100. * basePixel; + float eps = 4. * w; + + 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.; + 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; + float freq = exp2(baseLevel * logLac); + for (int i = 0; i < 4; i++) { + float absIdx = baseLevel + float(i); + vec2 qx = size * px * freq; + float qy = size * py * freq; + + float wi = 1.; + if (i == 0) wi = 1. - fade; + if (i == 3) wi = fade; + + vec2 fx = fract(qx); + float fy = fract(qy); + 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); + + sum += amp * wi * n; + norm += amp * wi; + amp *= .8; + freq *= 2.1; } - return total; -} + vec2 r = sum / norm; + float dx = .5 + r.x - r.y; + float grain = 3. * dx * dx - .7; -float randomG(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).g; + float rowBand = fract(dot(p, lightDir) * .05 * size); + return grain + .6 * u_roughnessRows * (rowBand - .5); } -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))); + +float getFiber(vec2 p, vec2 seedShift, float basePixel) { + float size = mix(4., 1., u_fiberSize); + float w = 50. * basePixel; + 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 o / 3.; -} -${ fiberNoise } + return clamp(.333 * length(grad), 0., 1.); +} -vec2 randomGB(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).gb; +vec2 smoothNoise(vec2 p) { + 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) / vec2(50.))).rg; } -float crumpledNoise(vec2 t, float pw) { - vec2 p = floor(t); - float wsum = 0.; - float cl = 0.; + +float getDrops(vec2 uv, vec2 seedShift) { + 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 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; + vec2 neighbor = vec2(float(y), float(x)); + 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)); } } - return pow(wsum != 0.0 ? cl / wsum : 0.0, .5) * 2.; + return .5 * lst(.09, .08, sqrt(sqrt(dropsMinDist))); } -float crumplesShape(vec2 uv) { - return crumpledNoise(uv * .25, 16.) * crumpledNoise(uv * .5, 2.); + +vec2 getCellTilt(float idx, float radius) { + vec2 rand = hash22(vec2(idx + 31., idx * u_seed + 17.)); + 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, float basePixel, out float depth) { + depth = 0.; + vec2 v = uv * freq; + 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.); + + 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 = .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)); + vec2 s = cell + .06 + .88 * r; + + vec2 d = v - s; + float dsq = dot(d, d); + if (dsq < n1) { + n2 = n1; + s2 = s1; + q2 = q1; + n1 = dsq; + s1 = s; + q1 = q; + } else if (dsq < n2) { + n2 = dsq; + s2 = s; + 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); + + 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; + 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; +} -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)); +vec4 getCrumples(vec2 uv) { + 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.); + 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; - float dist = distance(uv, p); - l = min(l, dist); - - if (l == dist) { - pp.xy = (uv - p.xy); - pp.z = dist; + seeds[i] = vec4(p, rand); + + 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; } } - return mix(pp.xy, vec2(0.), pow(pp.z, .25)); -} + float l = sqrt(near), lb = sqrt(nearB); + 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.); + float wideSum = 0.; + float toEdge = 9., edgeSoft = 0.; + + for (int i = 0; i < ${paperTextureMeta.maxCrumpleCount}; i++) { + if (float(i) >= crumpleN) break; + if (float(i) == idx) continue; + vec4 seed = seeds[i]; + vec2 p = seed.xy; + vec2 rand = seed.zw; + + vec2 d = uv - p; + float dsq = dot(d, d); + + if (dsq < shoulderLimit * shoulderLimit) { + float di = sqrt(dsq); + float shoulder = lst(.5, 0., di - l); + wide += shoulder * (d / max(di, 1e-4) - dir); + wideSum += shoulder; + } -float drops(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); + 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; + edgeSoft = pairSoft; + } + + float w = 1. - toBisector / pairSoft; + if (w > 0.) { + w *= w; + tilt += w * getCellTilt(float(i), rand.y); + tiltSum += w; } } - return 1. - smoothstep(.05, .09, pow(dropsMinDist, .5)); + + 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); + + return vec4(tilt / tiltSum + rounding * blend * radial, .2 * l, edge); } -float lst(float edge0, float edge1, float x) { - return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); +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 dx = fract(g) - .5; + 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 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 imageUV = v_imageUV; - vec2 patternUV = v_imageUV - .5; - patternUV = 5. * (patternUV * vec2(u_imageAspectRatio, 1.)); + vec2 patternUV = (v_imageUV - .5) * vec2(u_imageAspectRatio, 1.); + float basePixel = max(length(dFdx(patternUV)), length(dFdy(patternUV))); + vec2 baseFwidth = fwidth(patternUV); - // every noise layer below is skipped when its own uniform is 0: the branches are - // uniform-driven, so all the fragments take the same path (no divergence) - vec2 roughnessUv = 1.5 * (gl_FragCoord.xy - .5 * u_resolution) / u_pixelRatio; - float roughnessNoise = 0.; - if (u_roughness > 0.) { - roughnessNoise = roughness(roughnessUv + vec2(1., 0.)) - roughness(roughnessUv - vec2(1., 0.)); + float pattern = 0.; + + float crumpleDepth = 0.; + float wrinkleDepth = 0.; + float foldDepth = 0.; + float roughness = 0.; + float fiber = 0.; + float drops = 0.; + + float grazing = 0.7; + 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.); + + vec2 warpNoise = vec2(0.); + if (u_crumples > 0. || u_folds > 0. || u_wrinkles > 0.) { + warpNoise = smoothNoise(patternUV * .1 + .2 + .6 * fract(.017 * u_seed)) - .5; } - float roughness = roughnessNoise; + vec2 crumplesUV = (patternUV * .9) + .012 * warpNoise; - float crumples = 0.; if (u_crumples > 0.) { - vec2 crumplesUV = fract(patternUV * .02 / u_crumpleSize - u_seed) * 32.; - crumples = u_crumples * (crumplesShape(crumplesUV + vec2(.05, 0.)) - crumplesShape(crumplesUV)); - } + vec4 crumples = getCrumples(crumplesUV); - float fiber = 0.; - if (u_fiber > 0.) { - vec2 fiberUV = 2. / u_fiberSize * patternUV; - fiber = .5 * u_fiber * (fiberNoise(fiberUV, vec2(0.)) - 1.); + vec2 crumpleTilt = .5 * u_crumples * crumples.xy; + crumpleTilt -= .1 * max(dot(crumpleTilt, lightDir), 0.) * lightDir; + relief += crumpleTilt; + reliefAmount += .6 * u_crumples; + crumpleFlow = crumples.w * crumpleTilt; + + crumpleDepth = clamp(5. * crumples.z, 0., 1.); } - vec2 normal = vec2(0.); - vec2 normalImage = vec2(0.); + if (u_wrinkles > 0.) { + float detailFreq = mix(10., 1., u_wrinkleSize); + float detailAmp = .2; + vec2 detailGrad = vec2(0.); + float detailDepth = 0., depthSum = 0.; + for (int i = 0; i < 3; i++) { + float layerDepth; + detailGrad += detailAmp * getCrumpleDetail(crumplesUV, detailFreq, 31. * float(i), basePixel, layerDepth); + detailDepth += detailAmp * layerDepth; + depthSum += detailAmp; + detailAmp *= (.5 + .2 * detailGrad.x); + detailFreq *= 2.1; + } - vec2 w = vec2(0.); - vec2 w2 = vec2(0.); - if (u_folds > 0.) { - vec2 foldsUV = patternUV * .12; - foldsUV = rotate(foldsUV, 4. * u_seed); - w = folds(foldsUV); - foldsUV = rotate(foldsUV + .007 * cos(u_seed), .01 * sin(u_seed)); - w2 = folds(foldsUV); - } + vec2 detailTilt = 1.5 * u_wrinkles * detailGrad; + detailTilt -= .65 * max(dot(detailTilt, lightDir), 0.) * lightDir; + relief += detailTilt; + reliefAmount += .6 * u_wrinkles; - float dropsNoise = 0.; - if (u_drops > 0.) { - dropsNoise = drops(patternUV * 2.); + wrinkleDepth = clamp(5. * detailDepth / max(depthSum, 1e-4), 0., 1.); } - float drops = u_drops * dropsNoise; - float fade = 0.; - if (u_fade > 0.) { - fade = u_fade * fbm(.17 * patternUV + 10. * u_seed); - fade = clamp(8. * fade * fade * fade, 0., 1.); - } + 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 = .005 * warpNoise * foldCount; + vec2 uv = patternUV + .03 * crumpleFlow; - w = mix(w, vec2(0.), fade); - w2 = mix(w2, vec2(0.), fade); - crumples = mix(crumples, 0., fade); - drops = mix(drops, 0., fade); - fiber *= mix(1., .5, fade); - roughness *= mix(1., .5, fade); + vec2 slope, dark, lift; + getFolds(uv, foldOffset, foldCount, foldNoise, baseFwidth, slope, dark, lift); - normal.xy += u_folds * min(5. * u_contrast, 1.) * 4. * max(vec2(0.), w + w2); - normalImage.xy += u_folds * 2. * w; + 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); - normal.xy += crumples; - normalImage.xy += 1.5 * crumples; + foldDepth = u_folds * (lift.y * foldLightDir.y - lift.x * foldLightDir.x); + } - normal.xy += 3. * drops; - normalImage.xy += .2 * drops; + float unlit = cos(grazing); + float lit = unlit; - normal.xy += u_roughness * 1.5 * roughness; - normal.xy += fiber; + 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) + dot(foldSlope, foldLightDir), -1.2, 1.2); + lit = max(cos(slope + grazing), 0.); - normalImage += u_roughness * .75 * roughness; - normalImage += .2 * fiber; + pattern += (clamp(reliefAmount, 0., 1.) * unlit + (lit - unlit)) * foldInk * lightPower; + } - vec3 lightPos = vec3(1., 2., 1.); - float res = dot(normalize(vec3(normal, 9.5 - 9. * pow(u_contrast, .1))), normalize(lightPos)); + patternUV += .04 * crumpleFlow; - 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; + vec2 seedShift = floor(fract(u_seed * vec2(.7548776662, .5698402909)) * 50.) / 50.; - imageUV += .02 * normalImage; - float frame = getUvFrame(imageUV); - vec4 image = texture(u_image, imageUV); - image.rgb += .6 * pow(u_contrast, .4) * (res - .7); + if (u_roughness > 0.) { + roughness = u_roughness * getRoughness(1000. * patternUV, lightDir, seedShift, basePixel); + pattern += roughness; + } - frame *= image.a; + if (u_fiber > 0.) { + fiber = u_fiber * getFiber(50. * patternUV, seedShift, basePixel); + pattern += fiber; + } - vec3 color = fgColor * res; - float opacity = fgOpacity * res; + if (u_drops > 0.) { + drops = u_drops * getDrops(patternUV * 10., seedShift); + } - color += bgColor * (1. - opacity); - opacity += bgOpacity * (1. - opacity); - opacity = mix(opacity, 1., frame); + pattern = clamp(pattern, 0., 1.); + + vec3 backColor = u_colorBack.rgb * u_colorBack.a; + float backOpacity = u_colorBack.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; + + float notClipped = u_clip ? .1 : 1.; + vec2 imageCenteredUV = v_imageUV - .5; + float edgeDist = 2. * max(abs(imageCenteredUV.x), abs(imageCenteredUV.y)); + 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); + vec2 centeredUV = imageCenteredUV * (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); + 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); + 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 paper = vec3(1.) - opacity + color; + 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); + opacity = frame + opacity * (1. - frame); + + if (u_clip) { + color *= frame; + opacity *= frame; + } + } - color -= .007 * drops; + color *= mix(vec3(1.), .5 * u_colorShadow.rgb, drops * shadowOpacity); - color.rgb = mix(color, image.rgb, frame); + color += backColor * (1. - opacity); + opacity += backOpacity * (1. - opacity); fragColor = vec4(color, opacity); } @@ -308,35 +570,56 @@ 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]; - u_contrast: number; + u_colorPaper: [number, number, number, number]; + u_colorShadow: [number, number, number, number]; u_roughness: number; + u_roughnessSize: number; + u_roughnessRows: number; u_fiber: number; u_fiberSize: number; u_crumples: number; - u_foldCount: number; + u_crumpleCount: number; + u_wrinkles: number; + u_wrinkleSize: number; u_folds: number; - u_fade: number; - u_crumpleSize: number; + u_foldSizeX: number; + u_foldSizeY: number; + u_foldOffsetX: number; + u_foldOffsetY: number; + u_angle: number; u_drops: number; u_seed: number; + u_blending: number; + u_distortion: number; + u_clip: boolean; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string; - colorFront?: string; colorBack?: string; - contrast?: number; + colorPaper?: string; + colorShadow?: string; roughness?: number; + roughnessSize?: number; + roughnessRows?: number; fiber?: number; fiberSize?: number; crumples?: number; - foldCount?: number; + crumpleCount?: number; + wrinkles?: number; + wrinkleSize?: number; folds?: number; - fade?: number; - crumpleSize?: number; + foldSizeX?: number; + foldSizeY?: number; + foldOffsetX?: number; + foldOffsetY?: number; + angle?: number; drops?: number; seed?: number; + blending?: number; + distortion?: number; + clip?: boolean; }