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); + } } }`;