diff --git a/docs/src/app/test/page.tsx b/docs/src/app/test/page.tsx
index d6dbe70d..395f52d3 100644
--- a/docs/src/app/test/page.tsx
+++ b/docs/src/app/test/page.tsx
@@ -1,13 +1,103 @@
'use client';
-import { PaperTexture } from '@paper-design/shaders-react';
+import { useState, useCallback } from 'react';
+import {
+ ColorPanels,
+ Dithering,
+ DotGrid,
+ DotOrbit,
+ FlutedGlass,
+ GodRays,
+ GrainGradient,
+ HalftoneCmyk,
+ HalftoneDots,
+ Heatmap,
+ ImageDithering,
+ LiquidMetal,
+ MeshGradient,
+ Metaballs,
+ NeuroNoise,
+ PaperTexture,
+ PerlinNoise,
+ PulsingBorder,
+ SimplexNoise,
+ SmokeRing,
+ Spiral,
+ StaticMeshGradient,
+ StaticRadialGradient,
+ Swirl,
+ Voronoi,
+ Warp,
+ Water,
+ Waves,
+} from '@paper-design/shaders-react';
+import type { ReactElement } from 'react';
+
+const sampleImage = 'https://shaders.paper.design/images/image-filters/0018.webp';
+const sampleSvg = 'https://shaders.paper.design/images/logos/diamond.svg';
+
+const shaderFactories: Array<(key: string) => ReactElement> = [
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+ (key) => ,
+];
+
+let nextId = 0;
export default function TestPage() {
- return (
-
- );
+ const [shaders, setShaders] = useState([]);
+
+ const addShader = useCallback(() => {
+ const factory = shaderFactories[Math.floor(Math.random() * shaderFactories.length)]!;
+ const id = `shader-${nextId++}`;
+ setShaders((prev) => [...prev, factory(id)]);
+ }, []);
+
+ const removeShader = useCallback(() => {
+ setShaders((prev) => prev.slice(0, -1));
+ }, []);
+
+ return (
+
+
+
+
+ {shaders.length} shaders
+
+
{shaders}
+
+ );
}
diff --git a/packages/shaders/src/shader-mount.ts b/packages/shaders/src/shader-mount.ts
index b327d682..4ca28e0a 100644
--- a/packages/shaders/src/shader-mount.ts
+++ b/packages/shaders/src/shader-mount.ts
@@ -115,6 +115,10 @@ export class ShaderMount {
// Listen for document visibility changes to pause the shader when the tab is hidden
document.addEventListener('visibilitychange', this.handleDocumentVisibilityChange);
+
+ // Handle WebGL context loss (browsers silently evict contexts when too many are active)
+ this.canvasElement.addEventListener('webglcontextlost', this.handleContextLost);
+ this.canvasElement.addEventListener('webglcontextrestored', this.handleContextRestored);
}
private initProgram = () => {
@@ -317,6 +321,39 @@ export class ShaderMount {
this.rafId = requestAnimationFrame(this.render);
};
+ private handleContextLost = (e: Event): void => {
+ e.preventDefault();
+
+ if (this.rafId !== null) {
+ cancelAnimationFrame(this.rafId);
+ this.rafId = null;
+ }
+
+ this.canvasElement.style.visibility = 'hidden';
+ this.parentElement.setAttribute('data-paper-shader-placeholder', '');
+ };
+
+ private handleContextRestored = (): void => {
+ this.canvasElement.style.visibility = '';
+ this.parentElement.removeAttribute('data-paper-shader-placeholder');
+
+ this.initProgram();
+ this.setupPositionAttribute();
+ this.setupUniforms();
+ this.uniformCache = {};
+ this.textureUnitMap.clear();
+ this.textures.clear();
+ this.setUniformValues(this.providedUniforms);
+ this.resolutionChanged = true;
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
+
+ this.lastRenderTime = performance.now();
+ this.render(performance.now());
+ if (this.currentSpeed !== 0) {
+ this.requestRender();
+ }
+ };
+
/** Creates a texture from an image and sets it into a uniform value */
private setTextureUniform = (uniformName: string, image: HTMLImageElement): void => {
if (!image.complete || image.naturalWidth === 0) {
@@ -569,8 +606,11 @@ export class ShaderMount {
visualViewport?.removeEventListener('resize', this.handleVisualViewportChange);
document.removeEventListener('visibilitychange', this.handleDocumentVisibilityChange);
+ this.canvasElement.removeEventListener('webglcontextlost', this.handleContextLost);
+ this.canvasElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
this.uniformLocations = {};
+ this.parentElement.removeAttribute('data-paper-shader-placeholder');
// Remove the shader from the div wrapper element
this.canvasElement.remove();
@@ -656,6 +696,20 @@ const defaultStyle = `@layer paper-shaders {
border-radius: inherit;
corner-shape: inherit;
}
+
+ &[data-paper-shader-placeholder]::after {
+ content: 'WebGL context limit reached';
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ border-radius: inherit;
+ corner-shape: inherit;
+ background: rgba(0, 0, 0, 0.3);
+ color: rgba(255, 255, 255, 0.8);
+ }
}
}`;