ing compilation overhead during animation loops.
2. Automatic FBO Management: Multi-pass effects require intermediate textures. The engine automatically allocates ping-pong framebuffers based on the effect's pass count. For HDR effects, it requests RGBA16F internal formats, ensuring that bright values are preserved without clamping to the [0.0, 1.0] range.
3. Source Agnosticism: The input interface accepts multiple DOM types, including HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, ImageBitmap, OffscreenCanvas, and ImageData. The engine normalizes these sources into WebGL textures, handling format conversion and upload strategies internally.
4. Type-Safe Parameters: Effect definitions include default configurations that drive TypeScript type inference. This ensures that parameter objects are fully typed, preventing runtime errors from misspelled uniform names or incorrect value types.
Implementation Example
The following example demonstrates initializing the engine and applying a multi-pass effect with custom parameters. Note the use of await image.decode() to ensure the image data is ready before GPU upload, a critical best practice for avoiding flicker or blank renders.
import { RenderEngine, LuminousGlow } from "@vysmo/effects";
// Initialize the engine with a target canvas
const canvasElement = document.getElementById("visual-output") as HTMLCanvasElement;
const engine = new RenderEngine({
target: canvasElement,
preserveDrawingBuffer: false, // Optimize for performance
});
// Load and decode source image
const sourceImage = document.querySelector<HTMLImageElement>("#hero-image")!;
await sourceImage.decode();
// Execute a multi-pass HDR effect
// The engine handles bright-pass, blur passes, and composite automatically
engine.execute(LuminousGlow, {
inputSource: sourceImage,
settings: {
glowIntensity: 1.4,
luminanceThreshold: 0.75,
blurRadius: 8.0,
},
});
Custom Effect Authoring
Developers can define custom effects using a declarative API. The engine injects utility functions for texture sampling and manages uniform binding. This allows focus on the mathematical logic of the effect rather than WebGL boilerplate.
import { createFilter, RenderEngine } from "@vysmo/effects";
// Define a vignette filter with falloff and darkness controls
const VignetteFilter = createFilter({
identifier: "vignette-darken",
defaultConfig: { falloff: 0.6, darkness: 0.4 },
shaderSource: `
uniform float uFalloff;
uniform float uDarkness;
vec4 compute(vec2 coord) {
vec4 pixel = fetchInput(coord);
float dist = distance(coord, vec2(0.5));
float vignette = smoothstep(0.8, uFalloff, dist);
vec3 result = mix(pixel.rgb, pixel.rgb * (1.0 - uDarkness), vignette);
return vec4(result, pixel.a);
}
`,
});
// Apply custom filter
engine.execute(VignetteFilter, {
inputSource: sourceImage,
settings: { falloff: 0.5, darkness: 0.6 },
});
Video and Interaction Patterns
The engine is designed for real-time updates. When processing video, the engine re-uploads the current video frame texture on each execution. Shader caching ensures that parameter changes do not trigger recompilation, enabling smooth interaction.
const videoPlayer = document.querySelector<HTMLVideoElement>("#media-stream")!;
videoPlayer.play();
const renderLoop = () => {
engine.execute(LuminousGlow, {
inputSource: videoPlayer,
settings: { glowIntensity: 0.9, luminanceThreshold: 0.6 },
});
requestAnimationFrame(renderLoop);
};
renderLoop();
// Interactive parameter update
const intensitySlider = document.getElementById("glow-slider") as HTMLInputElement;
intensitySlider.addEventListener("input", (event) => {
const value = Number((event.target as HTMLInputElement).value);
engine.execute(LuminousGlow, {
inputSource: videoPlayer,
settings: { glowIntensity: value, luminanceThreshold: 0.6 },
});
});
Pitfall Guide
Production WebGL2 implementations require careful attention to resource management and browser quirks. The following pitfalls are common when building or integrating image processing pipelines.
-
Shader Recompilation in Animation Loops
- Explanation: Creating new effect instances or modifying shader source strings inside a
requestAnimationFrame loop forces the GPU driver to recompile shaders every frame. This causes severe frame drops.
- Fix: Define effects once outside the render loop. Reuse the same effect instance and only update parameter values. The engine caches compiled programs, so parameter updates are nearly free.
-
HDR Clamping in Multi-Pass Effects
- Explanation: Using standard 8-bit per channel textures for intermediate passes in effects like bloom causes bright highlights to clamp to
1.0. This results in flat, washed-out visuals where the glow lacks intensity.
- Fix: Ensure the pipeline uses
RGBA16F (16-bit float) internal formats for intermediate framebuffers. @vysmo/effects handles this automatically for HDR-aware effects.
-
Memory Leaks via Framebuffer Objects
- Explanation: FBOs and associated textures consume GPU memory. Failing to release these resources when the component unmounts or the engine is no longer needed leads to memory leaks, eventually causing context loss or browser crashes.
- Fix: Always call the engine's disposal method (e.g.,
engine.dispose()) during cleanup. This releases all allocated FBOs, textures, and shader programs.
-
Ignoring WebGL Context Loss
- Explanation: WebGL contexts can be lost due to GPU driver resets, tab suspension, or resource exhaustion. If the application does not handle context loss, the canvas will stop rendering without warning.
- Fix: Listen for
webglcontextlost and webglcontextrestored events. Reinitialize the engine and reload resources when the context is restored.
-
Blocking Main Thread with Synchronous Operations
- Explanation: Uploading large textures or reading pixels back to the CPU (
readPixels) can block the main thread, causing jank.
- Fix: Use
await image.decode() before rendering to ensure textures are ready. Avoid readPixels in animation loops; if pixel data is required, use it sparingly or offload to a Web Worker.
-
High-DPI Scaling Mismatches
- Explanation: Setting CSS dimensions on a canvas without adjusting the internal
width and height attributes results in blurry rendering on high-DPI displays.
- Fix: Scale the canvas internal dimensions by
window.devicePixelRatio while maintaining the CSS size. The engine should be configured to respect the backing store size.
-
Source Format Incompatibility
- Explanation: Attempting to render a source that hasn't finished loading or decoding results in errors or blank outputs.
- Fix: Always await
decode() for images and ensure video elements have sufficient data buffered before the first render call.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple blur or grayscale | CSS filter | No JS overhead, native GPU acceleration | Zero bundle size |
| Complex multi-pass (bloom, glow) | @vysmo/effects | Automatic HDR, ping-pong buffering, small footprint | ~9 KB (full library) |
| Custom shader logic | @vysmo/effects + createFilter | Type safety, resource management, shader caching | Minimal overhead |
| Video processing with effects | @vysmo/effects | Efficient frame upload, 60 FPS support | Low CPU/GPU usage |
| Legacy browser support | Fallback to CSS | WebGL2 not available | Graceful degradation |
Configuration Template
The following TypeScript template demonstrates a robust setup with error handling, context loss recovery, and DPI scaling.
import { RenderEngine, GaussianBlur, createFilter } from "@vysmo/effects";
class VisualProcessor {
private engine: RenderEngine | null = null;
private canvas: HTMLCanvasElement;
constructor(canvasId: string) {
this.canvas = document.getElementById(canvasId) as HTMLCanvasElement;
this.initEngine();
this.setupContextListeners();
}
private initEngine() {
const dpr = window.devicePixelRatio || 1;
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * dpr;
this.canvas.height = rect.height * dpr;
this.engine = new RenderEngine({
target: this.canvas,
preserveDrawingBuffer: false,
});
}
private setupContextListeners() {
this.canvas.addEventListener("webglcontextlost", (e) => {
e.preventDefault();
console.warn("WebGL context lost. Reinitializing...");
this.engine?.dispose();
this.engine = null;
setTimeout(() => this.initEngine(), 1000);
});
}
public applyBlur(image: HTMLImageElement, radius: number) {
if (!this.engine) return;
this.engine.execute(GaussianBlur, {
inputSource: image,
settings: { spreadRadius: radius },
});
}
public dispose() {
this.engine?.dispose();
this.engine = null;
}
}
Quick Start Guide
- Install: Run
npm install @vysmo/effects in your project directory.
- Import: Add
import { RenderEngine, blur } from "@vysmo/effects"; to your module.
- Initialize: Create a
RenderEngine instance bound to your canvas element.
- Render: Call
engine.execute(blur, { inputSource: image, settings: { spreadRadius: 8 } }); after decoding the source.
- Cleanup: Invoke
engine.dispose() when the component is destroyed.
@vysmo/effects is MIT licensed, zero-dependency, and free for commercial use. It provides a production-ready abstraction for WebGL2 image processing, enabling high-performance visual effects with minimal engineering overhead.