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