@fjandin/react-shader
v0.0.30
Published
React component for rendering WebGPU shaders with WGSL
Maintainers
Readme
@fjandin/react-shader
A React component library for rendering WebGPU shaders. Provides <ReactGpuShader> for WGSL with automatic uniform handling, storage buffers, and audio reactivity.
Installation
npm install @fjandin/react-shader
# or
yarn add @fjandin/react-shader
# or
bun add @fjandin/react-shaderQuick Start
import { ReactGpuShader } from "@fjandin/react-shader"
const fragment = /*wgsl*/ `
fn mainImage(uv: vec2f) -> vec4f {
let color = 0.5 + 0.5 * cos(uniforms.iTime + vec3f(uv, 0.0) + vec3f(0.0, 2.0, 4.0));
return vec4f(color, 1.0);
}
`
function App() {
return (
<div style={{ width: "800px", height: "600px" }}>
<ReactGpuShader fragment={fragment} />
</div>
)
}Shaders define a mainImage(uv: vec2f) -> vec4f function. Built-in uniforms are accessed via uniforms.iTime, uniforms.iResolution, etc. The component automatically generates the uniform struct and wrapping @fragment entry point.
Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| fragment | string | Yes | - | WGSL fragment shader source code |
| uniforms | Record<string, GpuUniformValue> | No | {} | Custom uniform values (number, Vec2, Vec3, Vec4) |
| storageBuffers | Record<string, Vec4Array> | No | - | Named storage buffers of Vec4 arrays |
| className | string | No | - | CSS class name for the canvas |
| fullscreen | boolean | No | false | Render as fixed fullscreen overlay |
| timeScale | number | No | 1 | Scale factor for elapsed time |
| onFrame | (info: FrameInfo) => void | No | - | Callback invoked on each frame |
| onClick | (info: FrameInfo) => void | No | - | Callback invoked on canvas click |
| onMouseMove | (info: FrameInfo) => void | No | - | Callback invoked on mouse move |
| onMouseDown | (info: FrameInfo) => void | No | - | Callback invoked on mouse button press |
| onMouseUp | (info: FrameInfo) => void | No | - | Callback invoked on mouse button release |
| onMouseWheel | (info: FrameInfo, wheelDelta: number) => void | No | - | Callback invoked on mouse wheel scroll |
Built-in Uniforms
These uniforms are automatically provided to your shader every frame:
| Uniform | WGSL Type | Description |
|---------|-----------|-------------|
| iTime | f32 | Elapsed time in seconds (scaled by timeScale) |
| iMouse | vec2f | Mouse position in pixels (Y=0 at bottom) |
| iMouseNormalized | vec2f | Mouse position normalized with aspect correction (shorter axis -0.5 to 0.5, center is 0,0) |
| iMouseLeftDown | f32 | 1.0 when left mouse button is pressed, 0.0 otherwise |
| iResolution | vec2f | Canvas resolution in pixels (includes high-DPI scaling) |
Custom Uniforms
Custom uniforms support scalar and vector types:
<ReactGpuShader
fragment={fragment}
uniforms={{
scale: 2.0, // f32
offset: [0.5, 0.5], // vec2f
color: [1.0, 0.5, 0.2], // vec3f
transform: [1, 0, 0, 1], // vec4f
}}
/>Custom uniforms are accessed via uniforms.scale, uniforms.color, etc. in your WGSL code.
Storage Buffers
For large dynamic array data, storage buffers are more efficient than uniforms and support dynamic resizing:
const [particles, setParticles] = useState<Vec4Array>([
[0, 0, 0.5, 1],
[1, 1, 0.3, 0.8],
])
<ReactGpuShader
fragment={fragment}
storageBuffers={{ particles }}
/>In WGSL, storage buffers are declared as array<vec4f> and accessed by name:
fn mainImage(uv: vec2f) -> vec4f {
for (var i: u32 = 0; i < arrayLength(&particles); i++) {
let p = particles[i];
// Use p.xy as position, p.z as radius, p.w as intensity...
}
return vec4f(0.0);
}Storage buffers use over-allocation (1.5x growth) to minimize GPU buffer rebuilds when array sizes change frequently.
Audio Reactivity
The useAudio hook provides real-time audio analysis for audio-reactive shaders:
import { ReactGpuShader, useAudio } from "@fjandin/react-shader"
function AudioVisualizer() {
const audio = useAudio({
source: "microphone", // "microphone" | "element" | "display"
smoothing: 0.9, // 0-1, lerp factor between frames
})
return (
<>
<button onClick={() => audio.isRunning ? audio.stop() : audio.start()}>
{audio.isRunning ? "Stop" : "Start"}
</button>
<ReactGpuShader
fragment={fragment}
uniforms={{
audioLow: audio.levels.low,
audioMid: audio.levels.mid,
audioHigh: audio.levels.high,
}}
/>
</>
)
}useAudio options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| source | "microphone" \| "element" \| "display" | "microphone" | Audio input source |
| mediaElement | HTMLAudioElement \| HTMLVideoElement | - | Media element (when source is "element") |
| fftSize | number | 2048 | FFT size for frequency analysis |
| smoothingTimeConstant | number | 0.8 | AnalyserNode smoothing |
| smoothing | number | 0.9 | Frame-to-frame lerp factor (0 = instant, 0.9 = very smooth) |
useAudio return value:
| Field | Type | Description |
|-------|------|-------------|
| levels | AudioLevels | { low, mid, high, bands } - normalized 0-1 frequency levels |
| frequencyData | Uint8Array \| null | Raw frequency data |
| state | AudioConnectionState | "disconnected" \| "connecting" \| "connected" \| "error" |
| error | Error \| null | Connection error if any |
| start | () => Promise<void> | Start audio capture |
| stop | () => void | Stop audio capture |
| isRunning | boolean | Whether audio is currently capturing |
Shader Helper Functions
Pre-built WGSL shader functions are available:
import {
generateSimplexNoiseFunctionGpu,
generateColorPaletteFunctionGpu,
generateDistortionRippleFunctionGpu,
generateSceneCirclesFunctionGpu,
} from "@fjandin/react-shader"Inject them into your shader source:
const fragment = /*wgsl*/ `
${generateSimplexNoiseFunctionGpu()}
fn mainImage(uv: vec2f) -> vec4f {
let n = SimplexNoise3D(vec3f(uv, uniforms.iTime));
return vec4f(vec3f(n), 1.0);
}
`Frame Callback
Use the onFrame callback to update uniforms based on animation timing:
function App() {
const [customTime, setCustomTime] = useState(0)
return (
<ReactGpuShader
fragment={fragment}
uniforms={{ customTime }}
onFrame={(info) => {
setCustomTime((prev) => prev + info.deltaTime * 0.5)
}}
/>
)
}The FrameInfo object contains:
deltaTime- Time since last frame in secondstime- Total elapsed time in secondsresolution- Canvas resolution as[width, height]mouse- Mouse position as[x, y]mouseNormalized- Aspect-corrected mouse position as[x, y]mouseLeftDown- Whether left mouse button is pressed
TypeScript
All types are exported:
import type {
Vec2,
Vec3,
Vec4,
Vec2Array,
Vec3Array,
Vec4Array,
GpuUniformValue,
GpuStorageBuffers,
DefaultUniforms,
FrameInfo,
ReactGpuShaderProps,
AudioLevels,
AudioConnectionState,
AudioSourceType,
UseAudioOptions,
UseAudioReturn,
} from "@fjandin/react-shader"Features
- WebGPU rendering with WGSL shaders
- Storage buffers for large dynamic arrays
- Audio reactivity via
useAudiohook - Pre-built shader functions (simplex noise, color palettes, distortion ripples, circles)
- High-DPI display support with automatic DPR change detection
- Automatic canvas resizing
- Shader compilation error display
- Mouse tracking with WebGPU coordinate convention
- Optimized render loop with minimal per-frame allocations
Requirements
- React >= 17.0.0
- A browser with WebGPU support
License
MIT
