diff --git a/docs/public/shaders/line-grid.webp b/docs/public/shaders/line-grid.webp new file mode 100644 index 00000000..4a3f2d82 Binary files /dev/null and b/docs/public/shaders/line-grid.webp differ diff --git a/docs/registry.json b/docs/registry.json index 27a75da8..c852604c 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 00000000..bdbc298a --- /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 00000000..27fb7519 --- /dev/null +++ b/docs/src/app/(shaders)/line-grid/layout.tsx @@ -0,0 +1,9 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Line Grid • 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 00000000..cb66028e --- /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 5e6f9ba1..f462a733 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, speed: 0 }, }, + { + 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 00000000..8aebd1b1 --- /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 0a6b6d22..ff6561eb 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 00000000..cc699767 --- /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 bf43915b..2f7f3bbe 100644 --- a/packages/shaders/src/index.ts +++ b/packages/shaders/src/index.ts @@ -59,6 +59,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 00000000..909df104 --- /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