diff --git a/docs/src/app/(shaders)/paper-texture/page.tsx b/docs/src/app/(shaders)/paper-texture/page.tsx index 8ce59307..7d8f78e1 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,27 @@ 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 }, + 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 }, + 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 935b495d..a5ef49d7 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,62 +19,70 @@ export const paperTextureDef: ShaderDef = { type: 'string', defaultValue: defaultParams.colorBack, isColor: true, - description: 'Background color', + description: 'The color behind the image, usually light', }, { - name: 'colorFront', + name: 'colorShadow', type: 'string', - defaultValue: defaultParams.colorFront, + defaultValue: defaultParams.colorShadow, isColor: true, - description: 'Foreground color', + description: 'The color used for folds, creases, grain and speckles, blends into the image', }, { - 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 pattern), needs image', }, { - name: 'roughness', + name: 'distortion', type: 'number', - min: 0, + min: -1, max: 1, - defaultValue: defaultParams.roughness, - description: 'Pixel noise, related to canvas (not scalable)', + defaultValue: defaultParams.distortion, + description: + 'How much the image bends with the paper surface; negative values bend it the opposite direction (needs image)', }, { - name: 'fiber', + name: 'background', + type: 'boolean', + defaultValue: defaultParams.background, + description: 'Shows or hides the paper texture outside the distorted image frame (needs image)', + }, + { + name: 'roughness', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.fiber, - description: 'Curly-shaped noise', + defaultValue: defaultParams.roughness, + description: 'Grain noise, sized independently of scaling, with its level of detail depending on the scale', }, { - name: 'fiberSize', + name: 'roughnessSize', type: 'number', - min: 0.01, + min: 0, max: 1, - defaultValue: defaultParams.fiberSize, - description: 'Curly-shaped noise scale (needs fiber > 0)', + defaultValue: defaultParams.roughnessSize, + description: 'Scale of the roughness noise (needs roughness > 0)', }, { - name: 'crumples', + name: 'fiber', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.crumples, - description: 'Cell-based crumple pattern', + defaultValue: defaultParams.fiber, + description: 'Curly fiber noise, simulating the real paper', }, { - name: 'crumpleSize', + name: 'fiberSize', type: 'number', - min: 0.01, + min: 0, max: 1, - defaultValue: defaultParams.crumpleSize, - description: 'Cell-based crumple pattern scale (needs crumples > 0)', + defaultValue: defaultParams.fiberSize, + description: 'Scale of the fiber noise (needs fiber > 0)', }, { name: 'folds', @@ -82,7 +90,7 @@ 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 centered, irregular field of facets across the sheet', }, { name: 'foldCount', @@ -94,12 +102,53 @@ export const paperTextureDef: ShaderDef = { description: 'Number of folds (15 max), needs folds > 0', }, { - name: 'fade', + name: 'creases', type: 'number', min: 0, max: 1, - defaultValue: defaultParams.fade, - description: 'Big-scale noise mask applied to the pattern', + defaultValue: defaultParams.creases, + description: 'Depth of the straight creases, alternating between ridges and valleys', + }, + { + name: 'creaseSizeX', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.creaseSizeX, + description: 'Size of the vertical creases (needs creases > 0)', + }, + { + name: 'creaseSizeY', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.creaseSizeY, + description: 'Size of the horizontal creases (needs creases > 0)', + }, + { + name: 'creaseOffsetX', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.creaseOffsetX, + description: 'Shifts the vertical creases across the surface (needs creases > 0)', + }, + { + name: 'creaseOffsetY', + type: 'number', + min: 0, + max: 1, + 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 surface is lit from, clockwise from the top of the canvas, needs folds or creases > 0', }, { name: 'drops', @@ -107,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', @@ -115,7 +164,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 and drops', }, ...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..b99326fe 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 */ foldsNumber?: number; - /** @deprecated use `fade` instead */ - blur?: number; } type PaperTexturePreset = ImageShaderPreset; @@ -29,127 +25,155 @@ export const defaultPreset: PaperTexturePreset = { name: 'Default', params: { ...defaultObjectSizing, - fit: 'cover', - scale: 0.6, + blending: 1, + distortion: 0.5, + fit: 'contain', + scale: 0.9, speed: 0, frame: 0, - colorFront: '#9fadbc', + colorShadow: '#b3b3b3', colorBack: '#ffffff', - contrast: 0.3, - 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, + roughness: 0, + 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, + lightAngle: 300, + drops: 0.7, + seed: 234, + background: true, }, }; -export const abstractPreset: PaperTexturePreset = { - name: 'Abstract', +export const gridPreset: PaperTexturePreset = { + name: 'Grid', 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, + colorShadow: '#605b52', + colorBack: '#ffffff', + roughness: 0.16, + 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, + lightAngle: 140, + drops: 0, + seed: 613, + background: true, }, }; -export const cardboardPreset: PaperTexturePreset = { - name: 'Cardboard', +export const spreadPreset: PaperTexturePreset = { + name: 'Spread', params: { ...defaultObjectSizing, - fit: 'cover', + blending: 1, + distortion: 0.38, + 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, + colorShadow: '#c2c2c2', + colorBack: '#ffffff', + roughness: 0.16, + 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, + lightAngle: 88, + drops: 0, + seed: 563, + background: false, }, }; -export const detailsPreset: PaperTexturePreset = { - name: 'Details', +export const coloredPreset: PaperTexturePreset = { + name: 'Colored', params: { ...defaultObjectSizing, + blending: 1, + distortion: 0.5, + fit: 'contain', + scale: 0.9, speed: 0, frame: 0, - fit: 'cover', - scale: 3, - colorFront: '#00000000', - colorBack: '#00000000', - contrast: 0, - roughness: 1, - fiber: 0.27, - fiberSize: 0.22, - crumples: 1, - crumpleSize: 0.5, - folds: 1, - foldCount: 15, - fade: 0, - drops: 0, - seed: 6, + colorShadow: '#7654c9', + colorBack: '#f5b689', + roughness: 0, + 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, + lightAngle: 316, + drops: 1, + seed: 476, + background: true, }, }; export const paperTexturePresets: PaperTexturePreset[] = [ defaultPreset, - cardboardPreset, - abstractPreset, - detailsPreset, + spreadPreset, + gridPreset, + coloredPreset, ] as const; export const PaperTexture: React.FC = memo(function PaperTextureImpl({ // Own props speed = defaultPreset.params.speed, frame = defaultPreset.params.frame, - colorFront = defaultPreset.params.colorFront, + colorShadow = defaultPreset.params.colorShadow, colorBack = defaultPreset.params.colorBack, image = '', - contrast = defaultPreset.params.contrast, 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, + lightAngle = defaultPreset.params.lightAngle, drops = defaultPreset.params.drops, seed = defaultPreset.params.seed, + blending = defaultPreset.params.blending, + distortion = defaultPreset.params.distortion, + background = defaultPreset.params.background, // 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, @@ -170,19 +194,26 @@ export const PaperTexture: React.FC = memo(function PaperText const uniforms = { // Own uniforms u_image: image, - u_colorFront: getShaderColorFromString(colorFront), + u_isImage: Boolean(image), + u_colorShadow: getShaderColorFromString(colorShadow), u_colorBack: getShaderColorFromString(colorBack), - u_contrast: contrast, u_roughness: roughness, + u_roughnessSize: roughnessSize, u_fiber: fiber, u_fiberSize: fiberSize, - u_crumples: crumples, - u_crumpleSize: crumpleSize, u_foldCount: foldCount, + u_creaseSizeX: creaseSizeX, + u_creaseSizeY: creaseSizeY, + u_creaseOffsetX: creaseOffsetX, + u_creaseOffsetY: creaseOffsetY, + u_lightAngle: lightAngle, u_folds: folds, - u_fade: fade, + u_creases: creases, u_drops: drops, u_seed: seed, + u_blending: blending, + u_distortion: distortion, + u_background: background, ...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..a61a59e2 100644 --- a/packages/shaders/src/shaders/paper-texture.ts +++ b/packages/shaders/src/shaders/paper-texture.ts @@ -1,29 +1,38 @@ 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, proceduralHash22 } from '../shader-utils.js'; + +export const paperTextureMeta = { + maxFoldCount: 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_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 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_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) + * - 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): @@ -34,10 +43,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 +56,387 @@ 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_colorShadow; uniform vec4 u_colorBack; 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_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_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; +uniform bool u_background; 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); - +#define CREASE_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 LIT_FLOOR 0. + #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); + float bottom = smoothstep(0., blur, uv.y); + float top = 1. - smoothstep(1. - blur, 1., uv.y); return left * right * bottom * top; } -${ 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 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; - } - return total; +float lst(float edge0, float edge1, float x) { + return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); } +${declarePI} +${proceduralHash22} + +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 = exp2(absIdx * logLac); + 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); + 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; + } -float randomG(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).g; + vec2 r = sum / norm; + float dx = .5 + r.x - r.y; + return 3. * dx * dx - .7; } -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) { + 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; } - return o / 3.; -} -${ fiberNoise } + return .5 * length(grad) - .5; +} -vec2 randomGB(vec2 p) { - vec2 uv = floor(p) / 50. + .5; - return texture(u_noiseTexture, fract(uv)).gb; +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; } -float crumpledNoise(vec2 t, float pw) { - vec2 p = floor(t); - float wsum = 0.; - float cl = 0.; + +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 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.; -} -float crumplesShape(vec2 uv) { - return crumpledNoise(uv * .25, 16.) * crumpledNoise(uv * .5, 2.); + 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 * 24.) / 24. * TWO_PI; + return vec2(cos(an), sin(an)) * mix(TILT_FLOOR, 1., rand.y) * mix(RADIAL_FALLOFF, 1., radius); +} -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 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; - float dist = distance(uv, p); - l = min(l, dist); - if (l == dist) { - pp.xy = (uv - p.xy); - pp.z = dist; + 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); + + 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); } -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); - } - } - return 1. - smoothstep(.05, .09, pow(dropsMinDist, .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; + 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; } -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. - opacity); } void main() { vec2 imageUV = v_imageUV; + vec2 fromCenter = imageUV - .5; vec2 patternUV = v_imageUV - .5; - patternUV = 5. * (patternUV * vec2(u_imageAspectRatio, 1.)); + patternUV *= 5. * vec2(u_imageAspectRatio, 1.); - // 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 radialDistortion = 0.; + float xDistortion = 0.; + float yShift = 0.; + float scaleDistortion = 0.; + + float drops = 0.; + if (u_drops > 0.) { + drops = getDrops(patternUV * 2.); } - float roughness = roughnessNoise; - 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)); + 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.; + 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) + + .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, lightDir), 0.) * lightDir; + relief += foldTilt; + reliefAmount += .5 * u_folds; + foldFlow = folds.w * foldTilt; + + scaleDistortion = .15 * clamp(5. * folds.z, 0., 1.) * u_folds; } - float fiber = 0.; - if (u_fiber > 0.) { - vec2 fiberUV = 2. / u_fiberSize * patternUV; - fiber = .5 * u_fiber * (fiberNoise(fiberUV, vec2(0.)) - 1.); + if (u_creases > 0.) { + 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(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; } - vec2 normal = vec2(0.); - vec2 normalImage = vec2(0.); + float unlit = SHADING_SCALE * cos(grazing); + float lit = unlit; - 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); + if (reliefAmount > 0.) { + 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; } - float dropsNoise = 0.; - if (u_drops > 0.) { - dropsNoise = drops(patternUV * 2.); + patternUV += GRAIN_DRAG * foldFlow; + + float detailGain = (1. + CREASE_WEAR * creaseWear); + + if (u_roughness > 0.) { + float roughness = getRoughness(200. * patternUV); + roughness *= u_roughness * detailGain; + pattern += roughness; + radialDistortion += .02 * roughness; } - 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_fiber > 0.) { + float fiber = getFiber(10. * patternUV); + fiber *= u_fiber * detailGain; + pattern += fiber; + radialDistortion += .02 * fiber; } - 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); + if (u_drops > 0.) { + drops *= u_drops; + pattern += drops; + xDistortion += .03 * drops; + } - normal.xy += u_folds * min(5. * u_contrast, 1.) * 4. * max(vec2(0.), w + w2); - normalImage.xy += u_folds * 2. * w; + vec3 bgColor = u_colorBack.rgb; + float bgOpacity = u_colorBack.a; - normal.xy += crumples; - normalImage.xy += 1.5 * crumples; + vec3 shadowColor = u_colorShadow.rgb; + float shadowOpacity = u_colorShadow.a; - normal.xy += 3. * drops; - normalImage.xy += .2 * drops; + 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); - normal.xy += u_roughness * 1.5 * roughness; - normal.xy += fiber; + float patternAlpha = clamp(shadowOpacity * pattern, 0., 1.); - normalImage += u_roughness * .75 * roughness; - normalImage += .2 * fiber; + vec3 pic = vec3(0.); + float imageFootprint = 0.; - vec3 lightPos = vec3(1., 2., 1.); - float res = dot(normalize(vec3(normal, 9.5 - 9. * pow(u_contrast, .1))), normalize(lightPos)); + if (u_isImage) { + float frame = getUvFrame(imageUV, .005); + vec4 image = texture(u_image, imageUV); - 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 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); - imageUV += .02 * normalImage; - float frame = getUvFrame(imageUV); - vec4 image = texture(u_image, imageUV); - image.rgb += .6 * pow(u_contrast, .4) * (res - .7); + float lum = dot(vec3(.2126, .7152, .0722), image.rgb); - frame *= image.a; + vec3 ink = mix(vec3(1.), shadowColor, patternAlpha); + pic = blendMultiply(image.rgb, ink, u_blending); - vec3 color = fgColor * res; - float opacity = fgOpacity * res; + 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.); - color += bgColor * (1. - opacity); - opacity += bgOpacity * (1. - opacity); - opacity = mix(opacity, 1., frame); + imageFootprint = frame * image.a; + } + + float imageAlpha = imageFootprint; - color -= .007 * drops; + vec3 overlay = pic * imageAlpha + shadowColor * patternAlpha * (1. - imageAlpha); + float overlayAlpha = imageAlpha + patternAlpha * (1. - imageAlpha); - color.rgb = mix(color, image.rgb, frame); + vec3 color = overlay + bgColor * bgOpacity * (1. - overlayAlpha); + float opacity = overlayAlpha + bgOpacity * (1. - overlayAlpha); + + if (!u_background && u_isImage) { + color *= imageFootprint; + opacity *= imageFootprint; + } fragColor = vec4(color, opacity); } @@ -308,35 +444,48 @@ 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_colorShadow: [number, number, number, number]; 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_fade: number; - u_crumpleSize: number; + u_foldCount: number; + u_creases: number; + u_creaseSizeX: number; + u_creaseSizeY: number; + u_creaseOffsetX: number; + u_creaseOffsetY: number; + u_lightAngle: number; u_drops: number; u_seed: number; + u_blending: number; + u_distortion: number; + u_background: boolean; } export interface PaperTextureParams extends ShaderSizingParams, ShaderMotionParams { image?: HTMLImageElement | string; - colorFront?: string; + colorShadow?: string; colorBack?: string; - contrast?: number; roughness?: number; + roughnessSize?: number; fiber?: number; fiberSize?: number; - crumples?: number; - foldCount?: number; folds?: number; - fade?: number; - crumpleSize?: number; + foldCount?: number; + creases?: number; + creaseSizeX?: number; + creaseSizeY?: number; + creaseOffsetX?: number; + creaseOffsetY?: number; + lightAngle?: number; drops?: number; seed?: number; + blending?: number; + distortion?: number; + background?: boolean; }