Skip to content

@frapx/fx-passes

fx-* — effect runtime Instantiate it directly. WebGL2 only.

@frapx/fx-passes chains fragment shaders into passes. Each pass renders into its own float buffer, and a pass can be drawn many times per frame and averaged. It runs on the same host as @frapx/fx-canvas, so the manual clock, fixed resolution, captureFrame() and the lifecycle work the same way. @frapx/tool-capture exports it without changes.

Stay with fx-canvas for website backgrounds. Reach for fx-passes when a single pass is not enough (bloom, trails, simulations) or when frames for video need many samples.

Terminal window
pnpm add @frapx/fx-passes
import { createPasses } from "@frapx/fx-passes";
const fx = createPasses({
target: "#stage",
resolution: [3840, 2160],
clock: "manual",
accumulation: { samples: 64, shutter: 0.5, fps: 60 },
passes: [
{ name: "scene", fragment: sceneFragment, accumulate: true, format: "rgba32f" },
{ name: "bloom", fragment: bloomFragment, inputs: ["scene"], scale: 0.5 },
{ name: "final", fragment: finalFragment, inputs: ["scene", "bloom"] }
]
});
fx.setTime(2);
const pixels = await fx.captureFrame({ format: "rgba16f" });

Passes are drawn in order every frame, and the last one is shown and captured.

Option Meaning
name GLSL identifier. Readers sample the output as u_<name>, sized u_<name>Size
inputs Passes to sample. An earlier pass gives this frame’s output. The pass itself or a later one gives the previous frame’s (Shadertoy-style; double-buffered automatically, cleared by resetFeedback())
format "rgba16f" (default), "rgba32f" or "rgba8"
scale Output size relative to the canvas. u_resolution is the pass’ own size
filter / wrap How readers sample the output
accumulate Draw the pass accumulation.samples times per frame and pass the mean on

Uniforms and textures are shared: every pass that declares one receives it. Textures take the same forms as @frapx/fx-canvas, including 3D data textures ({ data, size: [nx, ny, nz], format }).

An accumulating pass outputs a single sample’s color. The runtime sums the samples in 32-bit float, so the mean never drifts darker the way a running average stored in half floats does. Later passes run once on the mean, so tone mapping and bloom see the converged image.

Uniform In accumulating passes
u_time The sample’s moment within the open shutter, [time, time + shutter / fps)
u_frameTime The frame’s time
u_jitter Sub-pixel offset in pixels in [-0.5, 0.5); add it to gl_FragCoord.xy
u_sampleIndex, u_sampleCount The sample’s index and the frame’s sample count

A shader that depends only on time gets motion blur without changes. Samples follow a deterministic low-discrepancy sequence, and sample 0 is the plain frame.

For a seamless loop, set accumulation.loop to its length in seconds. Every pass then sees the time wrapped into the loop, with each sample’s shutter offset added in double precision, so the frame one loop later is identical to the bit at any sample count.

  • Preview: an unchanged frame gains one sample per draw until samples, and any input change starts over. A running realtime loop shows one sample per frame. A stopped or manual-clock instance keeps refining the still frame on its own. Progress is in samplesDrawn and onSample.
  • Capture: captureFrame() draws all samples from scratch. It waits for the GPU between batches so long frames never trip the browser’s GPU watchdog, checks signal between samples, then runs the remaining passes and reads the last one. Uniform, accumulation and feedback changes made while it draws apply to the next frame.

Accumulating passes come first and may only read passes listed after them, which gives those passes’ previous-frame output.

fx.bindUniforms(keyframes({ ... })) evaluates the keyframes at every sample’s time in accumulating passes, so a keyframed camera is motion blurred like u_time. See examples/vite-timeline.

fx.setFragment(passName, source) swaps one pass’ shader in place, keeping its buffers, the time and uniforms, and restarting the frame’s samples. A compile error is returned rather than thrown and the previous shader keeps drawing — see @frapx/tool-dev for an on-page error overlay.

Member
canvas, gl, status, supported, ready, clock As in @frapx/fx-canvas
accumulation, samplesDrawn The resolved accumulation settings, and the samples of the current frame drawn so far
start(), stop(), render(), resize(), destroy() Lifecycle. render() at an unchanged time adds the next sample
setTime(seconds), setClock(clock), setTimeSource(source), setResolution(size) Clock and size, as in @frapx/fx-canvas
setAccumulation(options) Changes samples, shutter, fps or loop and restarts the frame’s samples
captureFrame(options) Draws every sample of the current frame and reads the last pass
resetFeedback() Clears the previous-frame outputs
setFragment(name, source) Swaps one pass’ shader (see Hot reloading)
bindUniforms(source) Drives uniforms from a timeline such as keyframes() (see Timelines)
setUniform(), setUniforms(), setTexture(), setTextures() Change user uniforms and textures. Uniforms set during a capture apply to the next frame; a texture applies once it loads

onResize(state) runs whenever the render size actually changes, as in @frapx/fx-canvas: not on setup, but also after setResolution(), and after the capture when requested during captureFrame(). state carries the instance, gl, the canvas and its new width / height. The pass buffers take the new size on the next draw.

onStatusChange(status) runs with the new value each time status changes (for example "paused" in a hidden tab, or "context-lost"), with the same rules as @frapx/fx-canvas: never for the initial "idle", nor before createPasses() returns, and calling destroy() from it is safe.

respectReducedMotion defaults to true, as in @frapx/fx-canvas: while the OS “reduce motion” setting is on, the loop holds a still frame (still refined up to accumulation.samples). Pass respectReducedMotion: false to keep animating.

WebGL2 with EXT_color_buffer_float, otherwise status is "unsupported". Each pass may use GLSL ES 1.00 or 3.00. With EXT_float_blend, samples are summed by additive blending; without it, by an extra pass.