Skip to content
13 changes: 13 additions & 0 deletions docs/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
]
}
5 changes: 5 additions & 0 deletions docs/registry/grain-and-noise-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { GrainAndNoise, type GrainAndNoiseProps } from '@paper-design/shaders-react';

export function GrainAndNoiseExample(props: GrainAndNoiseProps) {
return <GrainAndNoise style={{ position: 'fixed', width: '100%', height: '100%' }} {...props} />;
}
9 changes: 9 additions & 0 deletions docs/src/app/grain-and-noise/layout.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
}
153 changes: 153 additions & 0 deletions docs/src/app/grain-and-noise/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <GrainAndNoise style={{ position: 'fixed', width: '100%', height: '100%' }} />;
};

/**
* 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<React.CSSProperties['mixBlendMode']>;

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 (
<>
<Link href="/">
<BackButton />
</Link>
{showImage && (
<img
src={fileName ? `/images/image-filters/${fileName}` : ''}
className="fixed left-0 top-0 z-0 h-full w-full object-cover"
/>
)}
<GrainAndNoise
onClick={handleClick}
{...params}
colors={colors}
className="fixed size-full"
style={{ mixBlendMode: blendMode }}
/>
</>
);
};

export default GrainAndNoiseWithControls;
8 changes: 8 additions & 0 deletions docs/src/home-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ import {
waterPresets,
ImageDithering,
imageDitheringPresets,
GrainAndNoise,
grainAndNoisePresets,
} from '@paper-design/shaders-react';
import { StaticImageData } from 'next/image';

Expand Down Expand Up @@ -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[];
4 changes: 4 additions & 0 deletions packages/shaders-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
124 changes: 124 additions & 0 deletions packages/shaders-react/src/shaders/grain-and-noise.tsx
Original file line number Diff line number Diff line change
@@ -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<GrainAndNoiseParams>;

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<GrainAndNoiseProps> = 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 (
<ShaderMount
{...props}
speed={speed}
frame={frame}
fragmentShader={grainAndNoiseFragmentShader}
uniforms={uniforms}
/>
);
}, colorPropsAreEqual);
7 changes: 7 additions & 0 deletions packages/shaders/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions packages/shaders/src/shader-mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading