@frapx/shader
v0.6.0
Published
Lightweight WebGL shader background runtime for websites.
Maintainers
Readme
@frapx/shader
Lightweight WebGL shader background runtime for websites.
This is not a scene, camera, mesh, or full rendering-engine abstraction. It creates and manages a WebGL canvas for an existing DOM region, then lets you drive fragment shaders with built-in uniforms, custom uniforms, textures, and optional previous-frame feedback.
Install
pnpm add @frapx/shaderBasic Usage
import { createShaderBackground, glsl } from "@frapx/shader";
const fx = createShaderBackground({
target: ".hero",
fragment: glsl`
precision highp float;
uniform vec2 u_resolution;
uniform vec2 u_pointerUv;
uniform float u_time;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
float glow = 0.5 + 0.5 * sin(u_time + uv.x * 8.0);
gl_FragColor = vec4(uv.x, u_pointerUv.y, glow, 1.0);
}
`
});JS uniform names omit u_; GLSL uniforms use u_.
fx.setUniform("progress", 0.4);
// GLSL: uniform float u_progress;This naming rule also applies to initial custom uniforms:
createShaderBackground({
target: ".hero",
fragment,
uniforms: {
progress: 0
}
});The shader must declare uniform float u_progress;, not uniform float progress;.
Uniforms set before ready are cached and applied on the first render.
Examples
examples/vite-basic- minimal Vite usage and integration checks.examples/vite-feedback- interactive previous-frame feedback demo.examples/vite-react- React binding example.
Color Uniforms
Use hexToRgb() and hexToRgba() to convert hex colors into vec3 and vec4 uniform values.
import { createShaderBackground, glsl, hexToRgb, hexToRgba } from "@frapx/shader";
const fx = createShaderBackground({
target: ".hero",
fragment,
uniforms: {
baseColor: hexToRgb("#7dd3fc"),
overlayColor: hexToRgba("#0f172acc")
}
});uniform vec3 u_baseColor;
uniform vec4 u_overlayColor;The helpers support #rgb, #rgba, #rrggbb, #rrggbbaa, and the same forms without #.
The returned values are sRGB channels normalized to 0..1. Invalid hex values throw an Error.
Textures
const fx = createShaderBackground({
target: ".hero",
fragment,
textures: {
image: "/hero.webp",
mask: {
source: "/mask.webp",
wrap: "clamp",
filter: "linear",
flipY: true
}
}
});Each texture creates a sampler and size uniform:
uniform sampler2D u_image;
uniform vec2 u_imageSize;Supported v1 sources are image URL, HTMLImageElement, and HTMLCanvasElement.
Textures can be updated at runtime. Updates are async because URL sources must load before they can be uploaded to WebGL.
await fx.setTexture("image", "/next-hero.webp");
await fx.setTextures({
image: "/next-hero.webp",
mask: nextMaskCanvas
});setTextures() is a partial update: omitted texture names are left unchanged.
If a runtime texture update fails, the previous texture remains active and the
returned promise rejects. In "demand" render mode, successful texture updates
request a render.
Feedback
Enable feedback when a shader needs to read the previous rendered frame:
createShaderBackground({
target: ".hero",
fragment,
feedback: true
});This creates a managed ping-pong framebuffer pair and exposes the previous rendered frame as a sampler:
uniform vec2 u_resolution;
uniform sampler2D u_previousFrame;
uniform vec2 u_previousFrameSize;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec4 history = texture2D(u_previousFrame, uv);
gl_FragColor = mix(vec4(0.0), history, 0.96);
}For GLSL ES 3.00 shaders, use the same uniform names with texture():
#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 u_resolution;
uniform sampler2D u_previousFrame;
uniform vec2 u_previousFrameSize;
void main() {
fragColor = texture(u_previousFrame, gl_FragCoord.xy / u_resolution);
}feedback: true uses u_previousFrame and u_previousFrameSize. To customize the generated suffix name:
feedback: {
uniform: "history",
filter: "linear",
wrap: "clamp",
clearColor: [0, 0, 0, 0]
}The suffix must be a valid GLSL identifier without the u_ prefix. For example, uniform: "history" creates u_history and u_historySize. A user texture with the same name is rejected.
The feedback texture always follows the canvas drawing-buffer size, and u_previousFrameSize reports that size in pixels. The first frame and any resize reset the texture to clearColor (transparent black by default). Alpha is preserved from the fragment output; the texture represents shader output before DOM compositing.
In renderMode: "demand", u_previousFrame is the result of the previous demand render. Enabling feedback does not start a continuous render loop by itself.
When feedback is enabled, onBeforeRender and onAfterRender wrap the main shader draw into the internal framebuffer. The copy to the visible canvas happens after onAfterRender.
See examples/vite-feedback for a small interactive feedback demo.
External Uniforms
Scroll is intentionally not built in. Use any scroll or animation library and push values into uniforms.
const fx = createShaderBackground({
target: ".hero",
fragment,
uniforms: {
progress: 0,
velocity: 0
},
renderMode: "demand"
});
window.addEventListener("scroll", () => {
const max = document.documentElement.scrollHeight - innerHeight;
fx.setUniform("progress", max > 0 ? scrollY / max : 0);
});Render Modes
createShaderBackground({
target: ".hero",
fragment,
renderMode: "always" // default
});"demand" renders when uniforms, pointer state, texture load, or resize changes. fx.render() is also available.
Built-In Uniforms
uniform vec2 u_resolution; // drawing buffer px
uniform vec2 u_viewportSize; // CSS px
uniform float u_pixelRatio;
uniform float u_time; // seconds, paused offscreen/hidden
uniform float u_delta; // seconds, clamped to 0.1
uniform vec2 u_pointer; // drawing buffer px, bottom-left origin
uniform vec2 u_pointerUv; // 0..1, bottom-left origin
uniform float u_pointerActive; // 0 or 1
uniform float u_reducedMotion; // 1 when the OS prefers reduced motion, else 0u_reducedMotion is always supplied. Use it to soften or stop motion yourself when you do not want the library to pause the loop (see respectReducedMotion below).
Options
createShaderBackground({
target: ".hero",
canvas: existingCanvas,
fragment,
vertex,
uniforms,
textures,
feedback: false,
layer: "background",
autoStart: true,
pauseWhenOffscreen: true,
pauseWhenHidden: true,
respectReducedMotion: false,
renderMode: "always",
dpr: "auto",
maxDpr: 2,
autoResize: true,
debug: false,
canvasClass: "hero-fx",
canvasStyle: {
opacity: "0.8",
mixBlendMode: "screen"
},
onReady(instance) {},
onError(error) {},
onBeforeRender(state) {},
onAfterRender(state) {}
});layer: "background" inserts the canvas as the first child with z-index: 0. layer: "overlay" inserts it as the last child with z-index: 1. Existing child styles are not changed.
Lifecycle & accessibility
The render loop is paused whenever it is not worth running, and resumes automatically:
pauseWhenOffscreen(defaulttrue) — pause while the target scrolls out of view.pauseWhenHidden(defaulttrue) — pause while the document is hidden (e.g. a background tab).respectReducedMotion(defaultfalse) — when enabled, hold a single static frame while the OS "prefers reduced motion" setting is on, and resume if the user turns it off. Theu_reducedMotionuniform andstate.reducedMotionare supplied regardless of this flag, so you can also handle reduced motion inside the shader. InrenderMode: "demand"the motion gate does not apply (there is no loop to throttle).
WebGL2 / GLSL ES 3.00
Start your fragment shader with #version 300 es and the library automatically requests a WebGL2 context:
#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 fragColor;
uniform float u_time;
void main() {
fragColor = vec4(v_uv, abs(sin(u_time)), 1.0);
}The internal vertex shader switches to a #version 300 es / in/out variant automatically. The v_uv varying name is the same as in WebGL1. You are responsible for writing precision, in vec2 v_uv;, and out vec4 — the library does not inject any preamble.
If WebGL2 is unavailable on the device, the instance transitions to status: "unsupported" and onError receives an UnsupportedError. No automatic downgrade is attempted.
Shaders without #version 300 es continue to use WebGL1 exactly as before.
Instance API
fx.ready;
fx.start();
fx.stop();
fx.render();
fx.resize();
fx.destroy();
await fx.setTexture("image", "/next-hero.webp");
await fx.setTextures({ image: "/next-hero.webp" });
fx.setUniform("progress", 0.5);
fx.setUniforms({ progress: 0.5, color: [1, 0, 0] });Unsupported environments return a no-op instance. ready rejects and debug: true prints warnings.
GLSL Helpers
import { glsl, glslUtils } from "@frapx/shader/glsl";
const fragment = glsl`
precision highp float;
${glslUtils.coverUv}
uniform vec2 u_resolution;
uniform vec2 u_imageSize;
uniform sampler2D u_image;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
gl_FragColor = texture2D(u_image, coverUv(uv, u_resolution, u_imageSize));
}
`;SSR Notes
The package is safe to import during SSR. Calling createShaderBackground() without a browser returns a no-op instance whose ready promise rejects.
Browser Support
Shaders without #version 300 es use WebGL1 / GLSL ES 1.00. Shaders that start with #version 300 es request WebGL2 / GLSL ES 3.00 automatically. WebGL2 support is additive and does not change the WebGL1 default path.
