From 55d76a4c5b085a056e066488c6c9293bb4512393 Mon Sep 17 00:00:00 2001 From: dalim-in Date: Sat, 13 Sep 2025 01:44:17 +0530 Subject: [PATCH 1/2] add line grid shader --- docs/public/shaders/line-grid.webp | Bin 0 -> 24036 bytes docs/registry.json | 13 ++ docs/registry/line-grid-example.tsx | 5 + docs/src/app/(shaders)/line-grid/layout.tsx | 9 + docs/src/app/(shaders)/line-grid/page.tsx | 67 ++++++++ docs/src/app/home-thumbnails.ts | 10 ++ docs/src/shader-defs/line-grid-def.ts | 95 +++++++++++ packages/shaders-react/src/index.ts | 4 + .../shaders-react/src/shaders/line-grid.tsx | 145 ++++++++++++++++ packages/shaders/src/index.ts | 11 ++ packages/shaders/src/shaders/line-grid.ts | 159 ++++++++++++++++++ 11 files changed, 518 insertions(+) create mode 100644 docs/public/shaders/line-grid.webp create mode 100644 docs/registry/line-grid-example.tsx create mode 100644 docs/src/app/(shaders)/line-grid/layout.tsx create mode 100644 docs/src/app/(shaders)/line-grid/page.tsx create mode 100644 docs/src/shader-defs/line-grid-def.ts create mode 100644 packages/shaders-react/src/shaders/line-grid.tsx create mode 100644 packages/shaders/src/shaders/line-grid.ts diff --git a/docs/public/shaders/line-grid.webp b/docs/public/shaders/line-grid.webp new file mode 100644 index 0000000000000000000000000000000000000000..4a3f2d8223ba016fc5a93bc09c82cd56c76d2e7a GIT binary patch literal 24036 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 literal 0 HcmV?d00001 diff --git a/docs/registry.json b/docs/registry.json index 27a75da89..c852604c8 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -16,6 +16,19 @@ } ] }, + { + "name": "line-grid", + "type": "registry:component", + "title": "Line Grid Example", + "description": "Line Grid shader example.", + "dependencies": ["@paper-design/shaders-react"], + "files": [ + { + "path": "registry/line-grid-example.tsx", + "type": "registry:component" + } + ] + }, { "name": "dot-orbit", "type": "registry:component", diff --git a/docs/registry/line-grid-example.tsx b/docs/registry/line-grid-example.tsx new file mode 100644 index 000000000..bdbc298ab --- /dev/null +++ b/docs/registry/line-grid-example.tsx @@ -0,0 +1,5 @@ +import { LineGrid, type LineGridProps } from '@paper-design/shaders-react'; + +export function DotGridExample(props: LineGridProps) { + return ; +} diff --git a/docs/src/app/(shaders)/line-grid/layout.tsx b/docs/src/app/(shaders)/line-grid/layout.tsx new file mode 100644 index 000000000..d62010cf0 --- /dev/null +++ b/docs/src/app/(shaders)/line-grid/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Ripple • Paper', +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/docs/src/app/(shaders)/line-grid/page.tsx b/docs/src/app/(shaders)/line-grid/page.tsx new file mode 100644 index 000000000..cb66028e7 --- /dev/null +++ b/docs/src/app/(shaders)/line-grid/page.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { LineGrid, lineGridPresets } 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 { LineGridShape, LineGridShapes } from '@paper-design/shaders'; +import { cleanUpLevaParams } from '@/helpers/clean-up-leva-params'; +import { toHsla } from '@/helpers/color-utils'; +import { ShaderDetails } from '@/components/shader-details'; +import { lineGridDef } from '@/shader-defs/line-grid-def'; +import { ShaderContainer } from '@/components/shader-container'; +import { useUrlParams } from '@/helpers/use-url-params'; + +const { worldWidth, worldHeight, ...defaults } = lineGridPresets[0].params; + +const LineGridWithControls = () => { + const [params, setParams] = useControls(() => { + return { + colorBack: { value: toHsla(defaults.colorBack), order: 100 }, + colorFill: { value: toHsla(defaults.colorFill), order: 101 }, + colorStroke: { value: toHsla(defaults.colorStroke), order: 102 }, + size: { value: defaults.size, min: 1, max: 100, order: 200 }, + gapX: { value: defaults.gapX, min: 2, max: 500, order: 201 }, + gapY: { value: defaults.gapY, min: 2, max: 500, order: 202 }, + strokeWidth: { value: defaults.strokeWidth, min: 0, max: 50, order: 203 }, + sizeRange: { value: defaults.sizeRange, min: 0, max: 1, order: 204 }, + opacityRange: { value: defaults.opacityRange, min: 0, max: 1, order: 205 }, + shape: { + value: defaults.shape, + options: Object.keys(LineGridShapes) as LineGridShape[], + order: 199, + }, + rotation: { value: defaults.rotation, min: 0, max: 360, order: 303 }, + }; + }); + + useControls(() => { + const presets = Object.fromEntries( + lineGridPresets.map(({ name, params: { worldWidth, worldHeight, ...preset } }) => [ + name, + button(() => setParamsSafe(params, setParams, preset)), + ]) + ); + 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); + useUrlParams(params, setParams, lineGridDef); + usePresetHighlight(lineGridPresets, params); + cleanUpLevaParams(params); + + return ( + <> + + + + + + ); +}; + +export default LineGridWithControls; diff --git a/docs/src/app/home-thumbnails.ts b/docs/src/app/home-thumbnails.ts index f12533897..bdfd89984 100644 --- a/docs/src/app/home-thumbnails.ts +++ b/docs/src/app/home-thumbnails.ts @@ -10,6 +10,7 @@ import voronoiImg from '../../public/shaders/voronoi.webp'; import wavesImg from '../../public/shaders/waves.webp'; import warpImg from '../../public/shaders/warp.webp'; import godRaysImg from '../../public/shaders/god-rays.webp'; +import lineGridImg from '../../public/shaders/line-grid.webp'; import spiralImg from '../../public/shaders/spiral.webp'; import swirlImg from '../../public/shaders/swirl.webp'; import ditheringImg from '../../public/shaders/dithering.webp'; @@ -78,6 +79,8 @@ import { ShaderComponentProps, Heatmap, heatmapPresets, + LineGrid, + lineGridPresets, } from '@paper-design/shaders-react'; import { StaticImageData } from 'next/image'; @@ -148,6 +151,13 @@ export const homeThumbnails = [ image: dotGridImg, shaderConfig: { ...dotGridPresets[0].params, gapX: 24, gapY: 24, size: 1.5 }, }, + { + name: 'line grid', + url: '/line-grid', + ShaderComponent: LineGrid, + image: lineGridImg, + shaderConfig: { ...lineGridPresets[0].params }, + }, { name: 'warp', url: '/warp', diff --git a/docs/src/shader-defs/line-grid-def.ts b/docs/src/shader-defs/line-grid-def.ts new file mode 100644 index 000000000..8aebd1b1d --- /dev/null +++ b/docs/src/shader-defs/line-grid-def.ts @@ -0,0 +1,95 @@ +import { lineGridPresets } from '@paper-design/shaders-react'; +import type { ShaderDef } from './shader-def-types'; + +const defaultParams = lineGridPresets[0].params; + +export const lineGridDef: ShaderDef = { + name: 'Line Grid', + description: 'Static grid pattern made of lines, horizontal, vertical, diagonal, cross.', + params: [ + { + name: 'colorBack', + type: 'string', + defaultValue: defaultParams.colorBack, + isColor: true, + description: 'Background color', + }, + { + name: 'colorFill', + type: 'string', + defaultValue: defaultParams.colorFill, + isColor: true, + description: 'Shape fill color', + }, + { + name: 'colorStroke', + type: 'string', + defaultValue: defaultParams.colorStroke, + isColor: true, + description: 'Shape stroke color', + }, + { + name: 'shape', + type: 'enum', + defaultValue: defaultParams.shape, + description: 'The shape type', + options: ['horizontal', 'vertical', 'diagonalForward', 'diagonalBack', 'cross'], + }, + { + name: 'size', + type: 'number', + min: 1, + max: 100, + defaultValue: defaultParams.size, + description: 'Base size of each line, pixels', + }, + { + name: 'gapX', + type: 'number', + min: 2, + max: 500, + defaultValue: defaultParams.gapX, + description: 'Pattern horizontal spacing, pixels', + }, + { + name: 'gapY', + type: 'number', + min: 2, + max: 500, + defaultValue: defaultParams.gapY, + description: 'Pattern vertical spacing, pixels', + }, + { + name: 'strokeWidth', + type: 'number', + min: 0, + max: 50, + defaultValue: defaultParams.strokeWidth, + description: 'The outline stroke width, pixels', + }, + { + name: 'sizeRange', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.sizeRange, + description: 'Random variation in shape size (0 = uniform size, higher = random value up to base size)', + }, + { + name: 'opacityRange', + type: 'number', + min: 0, + max: 1, + defaultValue: defaultParams.opacityRange, + description: 'Random variation in shape opacity (0 = all shapes opaque, higher = semi-transparent dots)', + }, + { + name: 'rotation', + type: 'number', + min: 0, + max: 360, + defaultValue: defaultParams.rotation, + description: 'Overall rotation angle of the graphics', + }, + ], +}; diff --git a/packages/shaders-react/src/index.ts b/packages/shaders-react/src/index.ts index 0a6b6d222..ff6561ebd 100644 --- a/packages/shaders-react/src/index.ts +++ b/packages/shaders-react/src/index.ts @@ -23,6 +23,10 @@ export { DotGrid, dotGridPresets } from './shaders/dot-grid.js'; export type { DotGridProps } from './shaders/dot-grid.js'; export type { DotGridUniforms, DotGridParams } from '@paper-design/shaders'; +export { LineGrid, lineGridPresets } from './shaders/line-grid.js'; +export type { LineGridProps } from './shaders/line-grid.js'; +export type { LineGridUniforms, LineGridParams } from '@paper-design/shaders'; + export { SimplexNoise, simplexNoisePresets } from './shaders/simplex-noise.js'; export type { SimplexNoiseProps } from './shaders/simplex-noise.js'; export type { SimplexNoiseUniforms, SimplexNoiseParams } from '@paper-design/shaders'; diff --git a/packages/shaders-react/src/shaders/line-grid.tsx b/packages/shaders-react/src/shaders/line-grid.tsx new file mode 100644 index 000000000..cc6997674 --- /dev/null +++ b/packages/shaders-react/src/shaders/line-grid.tsx @@ -0,0 +1,145 @@ +import { memo } from 'react'; +import { ShaderMount, type ShaderComponentProps } from '../shader-mount.js'; +import { colorPropsAreEqual } from '../color-props-are-equal.js'; +import { + getShaderColorFromString, + lineGridFragmentShader, + LineGridShapes, + ShaderFitOptions, + type LineGridParams, + type LineGridUniforms, + type ShaderPreset, + defaultPatternSizing, +} from '@paper-design/shaders'; + +export interface LineGridProps extends ShaderComponentProps, LineGridParams {} + +type LineGridPreset = ShaderPreset; + +export const defaultPreset: LineGridPreset = { + name: 'Default', + params: { + ...defaultPatternSizing, + colorBack: '#000000', + colorFill: '#ffffff', + colorStroke: '#ffaa00', + size: 1, + gapX: 32, + gapY: 32, + strokeWidth: 0, + sizeRange: 0.5, + opacityRange: 0, + shape: 'horizontal', + }, +}; + +const barsPreset: LineGridPreset = { + name: 'Bars', + params: { + ...defaultPatternSizing, + colorBack: '#000000', + colorFill: '#ffffff', + colorStroke: '#808080', + size: 6, + gapX: 32, + gapY: 32, + strokeWidth: 1, + sizeRange: 1, + opacityRange: 0, + shape: 'vertical', + }, +}; + +const gridPreset: LineGridPreset = { + name: 'Grid', + params: { + ...defaultPatternSizing, + colorBack: '#000000', + colorFill: '#ffffff', + colorStroke: '#808080', + size: 0.5, + gapX: 60, + gapY: 60, + strokeWidth: 1, + sizeRange: 1, + opacityRange: 0, + shape: 'cross', + }, +}; + +const diagonalPreset: LineGridPreset = { + name: 'Diagonal', + params: { + ...defaultPatternSizing, + colorBack: '#000000', + colorFill: '#ffffff', + colorStroke: '#808080', + size: 5, + gapX: 70, + gapY: 50, + strokeWidth: 0, + sizeRange: 0, + opacityRange: 1, + shape: 'diagonalForward', + }, +}; + +export const lineGridPresets: LineGridPreset[] = [defaultPreset, barsPreset, gridPreset, diagonalPreset]; + +export const LineGrid: React.FC = memo(function LineGridImpl({ + // Own props + colorBack = defaultPreset.params.colorBack, + colorFill = defaultPreset.params.colorFill, + colorStroke = defaultPreset.params.colorStroke, + size = defaultPreset.params.size, + gapX = defaultPreset.params.gapX, + gapY = defaultPreset.params.gapY, + strokeWidth = defaultPreset.params.strokeWidth, + sizeRange = defaultPreset.params.sizeRange, + opacityRange = defaultPreset.params.opacityRange, + shape = defaultPreset.params.shape, + + // 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, + + // Other props + maxPixelCount = 6016 * 3384, // Higher max resolution for this shader + ...props +}: LineGridProps) { + const uniforms = { + // Own uniforms + u_colorBack: getShaderColorFromString(colorBack), + u_colorFill: getShaderColorFromString(colorFill), + u_colorStroke: getShaderColorFromString(colorStroke), + u_size: size, + u_gapX: gapX, + u_gapY: gapY, + u_strokeWidth: strokeWidth, + u_sizeRange: sizeRange, + u_opacityRange: opacityRange, + u_shape: LineGridShapes[shape], + + // 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 LineGridUniforms; + + return ( + + ); +}, colorPropsAreEqual); diff --git a/packages/shaders/src/index.ts b/packages/shaders/src/index.ts index c252f6859..0e7465652 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -53,6 +53,17 @@ export { type DotGridUniforms, } from './shaders/dot-grid.js'; + +// ----- LineGrid Effect ----- // +/** A shader rendering a static line pattern */ +export { + lineGridFragmentShader, + LineGridShapes, + type LineGridShape, + type LineGridParams, + type LineGridUniforms, +} from './shaders/line-grid.js'; + // ----- Simplex noise ----- // /** A shader that calculates a combination of 2 simplex noises with result rendered as a gradient */ export { diff --git a/packages/shaders/src/shaders/line-grid.ts b/packages/shaders/src/shaders/line-grid.ts new file mode 100644 index 000000000..909df1047 --- /dev/null +++ b/packages/shaders/src/shaders/line-grid.ts @@ -0,0 +1,159 @@ +import { sizingVariablesDeclaration, type ShaderSizingParams, type ShaderSizingUniforms } from '../shader-sizing.js'; +import { declarePI, simplexNoise } from '../shader-utils.js'; + +/** + * Static line pattern + * + * Uniforms: + * - u_colorBack, u_colorFill, u_colorStroke (vec4 RGBA) + * - u_size (px): base line thickness + * - u_sizeRange (0..1): randomizes the thickness of lines between 0 and u_size + * - u_strokeWidth (px): the stroke (to be added to u_size) + * - u_gapX, u_gapY (px): pattern spacing + * - u_opacityRange (0..1): variety of line opacity + * - u_shape (float used as integer): + * ---- 0: horizontal line + * ---- 1: vertical line + * ---- 2: diagonal line (/) + * ---- 3: diagonal line (\) + * ---- 4: cross lines (+) + * + */ + +// language=GLSL +export const lineGridFragmentShader: string = `#version 300 es +precision mediump float; + +uniform vec4 u_colorBack; +uniform vec4 u_colorFill; +uniform vec4 u_colorStroke; +uniform float u_size; +uniform float u_gapX; +uniform float u_gapY; +uniform float u_strokeWidth; +uniform float u_sizeRange; +uniform float u_opacityRange; +uniform float u_shape; + +${sizingVariablesDeclaration} + +out vec4 fragColor; + +${declarePI} +${simplexNoise} + +float lineDistance(vec2 p, float lineType) { + float dist = 1e10; // Large initial value + + if (lineType < 0.5) { + // Horizontal line + dist = abs(p.y); + } else if (lineType < 1.5) { + // Vertical line + dist = abs(p.x); + } else if (lineType < 2.5) { + // Diagonal line (/) + dist = abs(p.x + p.y) / sqrt(2.0); + } else if (lineType < 3.5) { + // Diagonal line (\) + dist = abs(p.x - p.y) / sqrt(2.0); + } else { + // Cross lines (+) + float horizontal = abs(p.y); + float vertical = abs(p.x); + dist = min(horizontal, vertical); + } + + return dist; +} + +void main() { + + // x100 is a default multiplier between vertex and fragment shaders + // we use it to avoid UV precision issues + vec2 shape_uv = 100. * v_patternUV; + + vec2 grid = fract(shape_uv / vec2(u_gapX, u_gapY)) + 1e-4; + vec2 grid_idx = floor(shape_uv / vec2(u_gapX, u_gapY)); + float sizeRandomizer = .5 + .8 * snoise(2. * vec2(grid_idx.x * 100., grid_idx.y)); + float opacity_randomizer = .5 + .7 * snoise(2. * vec2(grid_idx.y, grid_idx.x)); + + vec2 center = vec2(0.5) - 1e-3; + vec2 p = (grid - center) * vec2(u_gapX, u_gapY); + + float sizeFactor = clamp(1.0 - sizeRandomizer * u_sizeRange, 0.0, 1.5); + float halfSize = u_size * 0.5 * sizeFactor; + float halfStroke = u_strokeWidth * 0.5 * sizeFactor; + + float dist = lineDistance(p, u_shape); + + // stable anti-alias width (avoid 0) + float edgeWidth = max(fwidth(dist), 1.0e-3); + + // fill mask: 1 inside the line, 0 outside (AA) + float fillMask = 1.0 - smoothstep(halfSize - edgeWidth, halfSize + edgeWidth, dist); + + // outer mask for stroke (line+stroke area) + float outerMask = 1.0 - smoothstep(halfSize + halfStroke - edgeWidth, halfSize + halfStroke + edgeWidth, dist); + + // stroke is the ring between outerMask and fillMask + float stroke = outerMask - fillMask; + + // opacity randomizer & alpha application (keeps your original behaviour) + float lineOpacity = max(0.0, 1.0 - opacity_randomizer * u_opacityRange); + stroke *= lineOpacity; + fillMask *= lineOpacity; + + // premultiply by input alpha channels + stroke *= u_colorStroke.a; + fillMask *= u_colorFill.a; + + vec3 color = vec3(0.0); + color += stroke * u_colorStroke.rgb; + color += fillMask * u_colorFill.rgb; + color += (1.0 - fillMask - stroke) * u_colorBack.rgb * u_colorBack.a; + + float opacity = 0.0; + opacity += stroke; + opacity += fillMask; + opacity += (1.0 - opacity) * u_colorBack.a; + + fragColor = vec4(color, opacity); +} +`; + +export interface LineGridUniforms extends ShaderSizingUniforms { + u_colorBack: [number, number, number, number]; + u_colorFill: [number, number, number, number]; + u_colorStroke: [number, number, number, number]; + u_size: number; + u_gapX: number; + u_gapY: number; + u_strokeWidth: number; + u_sizeRange: number; + u_opacityRange: number; + u_shape: (typeof LineGridShapes)[LineGridShape]; +} + +export interface LineGridParams extends ShaderSizingParams { + colorBack?: string; + colorFill?: string; + colorStroke?: string; + size?: number; + gapX?: number; + gapY?: number; + strokeWidth?: number; + sizeRange?: number; + opacityRange?: number; + shape?: LineGridShape; +} + +export const LineGridShapes = { + horizontal: 0, + vertical: 1, + diagonalForward: 2, // / + diagonalBack: 3, // \ + cross: 4, // + +} as const; + +export type LineGridShape = keyof typeof LineGridShapes; \ No newline at end of file From 9f1066187cd01a044a1e9a32ad3f6df8cf7dc573 Mon Sep 17 00:00:00 2001 From: Dalim Date: Sat, 13 Sep 2025 01:46:49 +0530 Subject: [PATCH 2/2] Update layout.tsx --- docs/src/app/(shaders)/line-grid/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/app/(shaders)/line-grid/layout.tsx b/docs/src/app/(shaders)/line-grid/layout.tsx index d62010cf0..27fb75190 100644 --- a/docs/src/app/(shaders)/line-grid/layout.tsx +++ b/docs/src/app/(shaders)/line-grid/layout.tsx @@ -1,7 +1,7 @@ import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'Ripple • Paper', + title: 'Line Grid • Paper', }; export default function Layout({ children }: { children: React.ReactNode }) {