diff --git a/docs/registry.json b/docs/registry.json index 27a75da89..a208bfbc2 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -327,6 +327,19 @@ "type": "registry:component" } ] + }, + { + "name": "grain-and-noise", + "type": "registry:component", + "title": "Grain And Noise Example", + "description": "Grain And Noise shader example.", + "dependencies": ["@paper-design/shaders-react"], + "files": [ + { + "path": "registry/grain-and-noise-example.tsx", + "type": "registry:component" + } + ] } ] } diff --git a/docs/registry/grain-and-noise-example.tsx b/docs/registry/grain-and-noise-example.tsx new file mode 100644 index 000000000..cd7dbb7bb --- /dev/null +++ b/docs/registry/grain-and-noise-example.tsx @@ -0,0 +1,5 @@ +import { GrainAndNoise, type GrainAndNoiseProps } from '@paper-design/shaders-react'; + +export function GrainAndNoiseExample(props: GrainAndNoiseProps) { + return ; +} diff --git a/docs/src/app/grain-and-noise/layout.tsx b/docs/src/app/grain-and-noise/layout.tsx new file mode 100644 index 000000000..1857e0868 --- /dev/null +++ b/docs/src/app/grain-and-noise/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Grain And Noise Shader | Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/grain-and-noise/page.tsx b/docs/src/app/grain-and-noise/page.tsx new file mode 100644 index 000000000..96a382c58 --- /dev/null +++ b/docs/src/app/grain-and-noise/page.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { GrainAndNoise, grainAndNoisePresets } 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 Link from 'next/link'; +import { BackButton } from '@/components/back-button'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { ShaderFit, ShaderFitOptions, grainAndNoiseNoiseMeta } from '@paper-design/shaders'; +import { useState, useEffect, useCallback } from 'react'; +import { useColors } from '@/helpers/use-colors'; + +/** + * You can copy/paste this example to use GrainAndNoise in your app + */ +const GrainAndNoiseExample = () => { + return ; +}; + +/** + * This example has controls added so you can play with settings in the example app + */ + +const { worldWidth, worldHeight, ...defaults } = grainAndNoisePresets[0].params; + +const GrainAndNoiseWithControls = () => { + const [imageIdx, setImageIdx] = useState(0); + const [showImage, setShowImage] = useState(false); + + 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', + ] as const; + + const fileName = imageIdx >= 0 ? imageFiles[imageIdx] : null; + + const handleClick = useCallback(() => { + const randomIdx = Math.floor(Math.random() * imageFiles.length); + setImageIdx(randomIdx); + }, []); + + const blendModes = [ + 'normal', + 'darken', + 'multiply', + 'color-burn', + 'lighten', + 'screen', + 'color-dodge', + 'overlay', + 'soft-light', + 'hard-light', + 'difference', + 'exclusion', + 'hue', + 'saturation', + 'color', + 'luminosity', + ] as const satisfies ReadonlyArray; + + type BlendMode = (typeof blendModes)[number]; + + const { blendMode } = useControls('Blend', { + blendMode: { + value: 'overlay', + options: blendModes, + }, + }) as { blendMode: BlendMode }; + + // Add image visibility control + useControls('Image', { + 'Toggle Image': button(() => setShowImage((prev) => !prev)), + 'Random Image': button(() => { + const randomIdx = Math.floor(Math.random() * imageFiles.length); + setImageIdx(randomIdx); + }), + }); + + const { colors, setColors } = useColors({ + defaultColors: defaults.colors, + maxColorCount: grainAndNoiseNoiseMeta.maxColorCount, + }); + + const [params, setParams] = useControls(() => { + return { + Parameters: folder( + { + grain: { value: defaults.grain, min: 0, max: 1, order: 300 }, + fiber: { value: defaults.fiber, min: 0, max: 1, order: 300 }, + speed: { value: defaults.speed, min: 0, max: 5, order: 351 }, + scale: { value: defaults.scale, min: 0.1, max: 5, order: 400 }, + }, + { order: 1 } + ), + }; + }, [colors.length]); + + useControls(() => { + const presets = Object.fromEntries( + grainAndNoisePresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => { + const { colors, ...presetParams } = preset; + setColors(colors); + setParamsSafe(params, setParams, presetParams); + }), + ]) + ); + return { + 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); + usePresetHighlight(grainAndNoisePresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + + {showImage && ( + + )} + + + ); +}; + +export default GrainAndNoiseWithControls; diff --git a/docs/src/home-shaders.ts b/docs/src/home-shaders.ts index 089f0aa2a..0146d244c 100644 --- a/docs/src/home-shaders.ts +++ b/docs/src/home-shaders.ts @@ -75,6 +75,8 @@ import { waterPresets, ImageDithering, imageDitheringPresets, + GrainAndNoise, + grainAndNoisePresets, } from '@paper-design/shaders-react'; import { StaticImageData } from 'next/image'; @@ -263,4 +265,10 @@ export const homeShaders = [ image: waterImg, shaderConfig: { ...waterPresets[0].params, scale: 0.8 }, }, + { + name: 'grain and noise', + url: '/grain-and-noise', + ShaderComponent: GrainAndNoise, + shaderConfig: { ...grainAndNoisePresets[0].params }, + }, ] satisfies HomeShaderConfig[]; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 665aa5e90..db197755b 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -103,6 +103,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 { GrainAndNoise, grainAndNoisePresets } from './shaders/grain-and-noise.js'; +export type { GrainAndNoiseProps } from './shaders/grain-and-noise.js'; +export type { GrainAndNoiseUniforms, GrainAndNoiseParams } from '@paper-design/shaders'; + export { isPaperShaderElement, getShaderColorFromString } from '@paper-design/shaders'; export type { PaperShaderElement, ShaderFit, ShaderSizingParams, ShaderSizingUniforms } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/grain-and-noise.tsx b/packages/shaders-react/src/shaders/grain-and-noise.tsx new file mode 100644 index 000000000..168f863fb --- /dev/null +++ b/packages/shaders-react/src/shaders/grain-and-noise.tsx @@ -0,0 +1,124 @@ +import { memo } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; +import { + defaultObjectSizing, + getShaderColorFromString, + getShaderNoiseTexture, + grainAndNoiseFragmentShader, + ShaderFitOptions, + type GrainAndNoiseParams, + type GrainAndNoiseUniforms, + type ShaderPreset, +} from '@paper-design/shaders'; + +export interface GrainAndNoiseProps extends ShaderComponentProps, GrainAndNoiseParams {} + +type GrainAndNoisePreset = ShaderPreset; + +export const defaultPreset: GrainAndNoisePreset = { + name: 'Default', + params: { + ...defaultObjectSizing, + speed: 1, + frame: 0, + colors: ['#ff0000', '#00ff00', '#0000ff'], + grain: 0.5, + fiber: 0.5, + scale: 1, + }, +}; + +export const monochromeFiberPreset: GrainAndNoisePreset = { + name: 'Monochrome fiber', + params: { + ...defaultObjectSizing, + speed: 1, + frame: 0, + colors: ['#000000', '#ffffff'], + grain: 0, + fiber: 1, + scale: 1, + }, +}; + +export const smallGrainPreset: GrainAndNoisePreset = { + name: 'Small grain', + params: { + ...defaultObjectSizing, + speed: 3, + frame: 0, + colors: ['#ff0000', '#00ff00', '#0000ff'], + grain: 1, + fiber: 0, + scale: 0.5, + }, +}; + +export const staticPreset: GrainAndNoisePreset = { + name: 'Static color', + params: { + ...defaultObjectSizing, + speed: 0, + frame: 0, + colors: ['#ff00d452'], + grain: 1, + fiber: 0.5, + scale: 1, + }, +}; + +export const grainAndNoisePresets: GrainAndNoisePreset[] = [defaultPreset, smallGrainPreset, monochromeFiberPreset, staticPreset] as const; + +export const GrainAndNoise: React.FC = memo(function GrainAndNoiseImpl({ + // Own props + speed = defaultPreset.params.speed, + frame = defaultPreset.params.frame, + colors = defaultPreset.params.colors, + grain = defaultPreset.params.grain, + fiber = defaultPreset.params.fiber, + + // 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 +}: GrainAndNoiseProps) { + const noiseTexture = typeof window !== 'undefined' && { u_noiseTexture: getShaderNoiseTexture() }; + + const uniforms = { + // Own uniforms + u_colors: colors.map(getShaderColorFromString), + u_colorsCount: colors.length, + u_grain: grain, + u_fiber: fiber, + ...noiseTexture, + + // Sizing uniforms + u_fit: ShaderFitOptions[fit], + u_scale: scale, + u_rotation: rotation, + u_offsetX: offsetX, + u_offsetY: offsetY, + u_originX: originX, + u_originY: originY, + u_worldWidth: worldWidth, + u_worldHeight: worldHeight, + } satisfies GrainAndNoiseUniforms; + + return ( + + ); +}, colorPropsAreEqual); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index f5818cbfb..9944c65f0 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -195,6 +195,13 @@ export { type ImageDitheringUniforms, } from './shaders/image-dithering.js'; +export { + grainAndNoiseFragmentShader, + grainAndNoiseNoiseMeta, + type GrainAndNoiseParams, + type GrainAndNoiseUniforms, +} from './shaders/grain-and-noise.js'; + // ----- Utils ----- // export { getShaderColorFromString } from './get-shader-color-from-string.js'; export { getShaderNoiseTexture } from './get-shader-noise-texture.js'; diff --git a/packages/shaders/src/shader-mount.ts b/packages/shaders/src/shader-mount.ts index 15fdc6c11..f4232de99 100644 --- a/packages/shaders/src/shader-mount.ts +++ b/packages/shaders/src/shader-mount.ts @@ -322,8 +322,8 @@ export class ShaderMount { this.gl.bindTexture(this.gl.TEXTURE_2D, texture); // Set texture parameters - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE); - this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.REPEAT); + this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.REPEAT); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR); this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR); diff --git a/packages/shaders/src/shaders/grain-and-noise.ts b/packages/shaders/src/shaders/grain-and-noise.ts new file mode 100644 index 000000000..20e035980 --- /dev/null +++ b/packages/shaders/src/shaders/grain-and-noise.ts @@ -0,0 +1,136 @@ +import type { vec4 } from '../types.js'; +import type { ShaderMotionParams } from '../shader-mount.js'; +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { rotation2, declarePI, simplexNoise, textureRandomizerR } from '../shader-utils.js'; + +export const grainAndNoiseNoiseMeta = { + maxColorCount: 3, +} as const; + +// language=GLSL +export const grainAndNoiseFragmentShader: string = `#version 300 es +precision lowp float; + +uniform float u_time; +uniform vec4 u_colors[${grainAndNoiseNoiseMeta.maxColorCount}]; +uniform float u_colorsCount; +uniform float u_grain; +uniform float u_fiber; + +uniform sampler2D u_noiseTexture; + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${declarePI} +${rotation2} +${simplexNoise} + +vec3 random(vec2 p) { + vec2 uv = p / 100.; + return texture(u_noiseTexture, fract(uv + .003 * u_time)).rgb; +} + +vec3 valueNoise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + vec3 a = random(i); + vec3 b = random(i + vec2(1.0, 0.0)); + vec3 c = random(i + vec2(0.0, 1.0)); + vec3 d = random(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + vec3 x1 = mix(a, b, u.x); + vec3 x2 = mix(c, d, u.x); + return mix(x1, x2, u.y); +} +vec3 fbm(vec2 n) { + vec3 total = vec3(0.); + float amplitude = 1.; + for (int i = 0; i < 5; i++) { + n = rotate(n, .7); + + total += valueNoise(n) * amplitude; + n *= 2.; + amplitude *= 0.4; + } + return total; +} + +vec3 fiberShape(vec2 uv) { + float epsilon = 0.01; + vec3 n1 = fbm(uv + vec2(epsilon, 0.0)); + vec3 n2 = fbm(uv - vec2(epsilon, 0.0)); + vec3 n3 = fbm(uv + vec2(0.0, epsilon)); + vec3 n4 = fbm(uv - vec2(0.0, epsilon)); + vec3 n12 = n1 - n2; + vec3 n34 = n3 - n4; + float epsilon2 = 2. * epsilon; + return vec3( + length(vec2(n12.x, n34.x)) / epsilon2, + length(vec2(n12.y, n34.y)) / epsilon2, + length(vec2(n12.z, n34.z)) / epsilon2 + ); +} + + +void main() { + vec2 fiberUV = 10. * v_patternUV; + vec3 fiber = u_fiber / u_colorsCount * fiberShape(fiberUV); + + + + + vec2 grainUV = 20. * v_patternUV; + float grainShapeNoise = snoise(grainUV + vec2(0., -.3 * u_time)) + snoise(1.4 * grainUV + vec2(0., .4 * u_time)); + float grainShape = smoothstep(0., .5 + .5 * u_grain, u_grain * grainShapeNoise); + + vec3 grainColor; + float grainOpacity; + int cc = int(u_colorsCount); + float grainColorMixer = .5 + .5 * snoise(2. * grainUV); + + if (cc == 1) { + grainColor = u_colors[0].rgb; + grainOpacity = u_colors[0].a; + } else if (cc == 2) { + float t = smoothstep(0., 1., grainColorMixer); + grainColor = mix(u_colors[0].rgb, u_colors[1].rgb, t); + grainOpacity = mix(u_colors[0].a, u_colors[1].a, t); + } else { + vec3 m1 = mix(u_colors[0].rgb, u_colors[1].rgb, smoothstep(0.0, 0.7, grainColorMixer)); + grainColor = mix(m1, u_colors[2].rgb, smoothstep(0.3, 1.0, grainColorMixer)); + float a1 = mix(u_colors[0].a, u_colors[1].a, smoothstep(0.0, 0.7, grainColorMixer)); + grainOpacity = mix(a1, u_colors[2].a, smoothstep(0.3, 1.0, grainColorMixer)); + } + + vec3 color = vec3(0.); + float opacity = 0.; + + for (int i = 0; i < cc && i < 3; i++) { + float fiberContribution = fiber[i] * u_colors[i].a; + color += u_colors[i].rgb * fiberContribution; + opacity += fiberContribution; + } + + float grainContribution = grainShape * grainOpacity; + color += grainColor * grainContribution; + opacity = min(opacity + grainContribution, 1.); + + fragColor = vec4(color, opacity); +} +`; + +export interface GrainAndNoiseUniforms extends ShaderSizingUniforms { + u_noiseTexture?: HTMLImageElement; + u_colors: vec4[]; + u_colorsCount: number; + u_grain: number; + u_fiber: number; +} + +export interface GrainAndNoiseParams extends ShaderSizingParams, ShaderMotionParams { + colors?: string[]; + grain?: number; + fiber?: number; +} diff --git a/packages/shaders/src/shaders/grain-gradient.ts b/packages/shaders/src/shaders/grain-gradient.ts index c861e2acd..ce3a30bac 100644 --- a/packages/shaders/src/shaders/grain-gradient.ts +++ b/packages/shaders/src/shaders/grain-gradient.ts @@ -73,7 +73,7 @@ uniform mediump float u_offsetX; uniform mediump float u_offsetY; ${sizingVariablesDeclaration} -${ sizingDebugVariablesDeclaration } +${sizingDebugVariablesDeclaration} out vec4 fragColor;