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;