Mirror: A frag-canvas custom element to apply Shadertoy fragment shaders to a canvas or image/video element

feat: Pause rendering when element isn't visible (#5)

* Add observers and track visibility; pause when not visible

* Add changeset

authored by kitten.sh and committed by GitHub b994c6e7 8306c46c

Changed files
+117 -37
.changeset
src
+5
.changeset/nice-streets-taste.md
··· 1 + --- 2 + 'frag-canvas': patch 3 + --- 4 + 5 + Pause rendering when element isn't visible
+50 -37
src/frag-canvas-element.ts
··· 1 + import { trackVisibility, trackResizes, trackTextUpdates } from './observers'; 2 + 1 3 const VERSION_300 = '#version 300 es'; 2 4 3 5 const VS_SOURCE_100 = ··· 221 223 } 222 224 223 225 class FragCanvas extends HTMLElement implements HTMLCanvasElement { 224 - static observedAttributes = []; 226 + static observedAttributes = ['pause']; 225 227 228 + private subscriptions: (() => void)[] = []; 226 229 private state: ReturnType<typeof createState> | null = null; 227 230 private input: HTMLCanvasElement | HTMLImageElement | HTMLVideoElement; 228 231 private output: HTMLCanvasElement; 229 - 230 - #mutationObserver = new MutationObserver(() => { 231 - if (this.state) { 232 - this.state.updateFragShader(this.source); 233 - } 234 - }); 235 - 236 - #resizeObserver = new ResizeObserver(entries => { 237 - const entry = entries[0]; 238 - if (this.state && entry) { 239 - const width = entry.devicePixelContentBoxSize[0].inlineSize; 240 - const height = entry.devicePixelContentBoxSize[0].blockSize; 241 - if (this.autoresize) { 242 - this.input.width = width; 243 - this.input.height = height; 244 - } 245 - this.state.updateViewport(width, height); 246 - this.state.drawImmediate(); 247 - this.#rescheduleDraw(); 248 - } 249 - }); 232 + public pause: boolean = false; 250 233 251 234 constructor() { 252 235 super(); ··· 375 358 cancelAnimationFrame(this.#frameID); 376 359 this.#frameID = undefined; 377 360 } 378 - this.#frameID = requestAnimationFrame(function draw( 379 - timestamp: DOMHighResTimeStamp 380 - ) { 381 - if (self.state) { 382 - self.state.draw(self.input, timestamp); 383 - self.#frameID = requestAnimationFrame(draw); 384 - } 385 - }); 361 + if (!this.pause) { 362 + this.#frameID = requestAnimationFrame(function draw( 363 + timestamp: DOMHighResTimeStamp 364 + ) { 365 + if (self.state && !self.pause) { 366 + self.state.draw(self.input, timestamp); 367 + self.#frameID = requestAnimationFrame(draw); 368 + } 369 + }); 370 + } 386 371 } 387 372 388 373 connectedCallback() { 374 + this.pause = !!this.getAttribute('pause'); 375 + 389 376 const gl = this.output.getContext('webgl2', { 390 377 alpha: true, 391 378 desynchronized: true, ··· 400 387 401 388 const state = (this.state = gl && createState(gl, init)); 402 389 if (state) { 403 - this.#mutationObserver.observe(this, { 404 - subtree: true, 405 - characterData: true, 406 - }); 407 - this.#resizeObserver.observe(this, { box: 'device-pixel-content-box' }); 390 + this.subscriptions.push( 391 + trackResizes(this, entry => { 392 + const { inlineSize: width, blockSize: height } = entry; 393 + if (this.autoresize) { 394 + this.input.width = width; 395 + this.input.height = height; 396 + } 397 + state.updateViewport(width, height); 398 + state.drawImmediate(); 399 + this.#rescheduleDraw(); 400 + }), 401 + trackTextUpdates(this, () => { 402 + state.updateFragShader(this.source); 403 + }), 404 + trackVisibility(this, isVisible => { 405 + this.pause = !isVisible; 406 + this.#rescheduleDraw(); 407 + }) 408 + ); 409 + this.#rescheduleDraw(); 410 + } 411 + } 412 + 413 + attributeChangedCallback( 414 + name: string, 415 + _oldValue: unknown, 416 + newValue: unknown 417 + ) { 418 + if (name === 'pause') { 419 + this.pause = !!newValue; 408 420 this.#rescheduleDraw(); 409 421 } 410 422 } 411 423 412 424 disconnectedCallback() { 413 - this.#mutationObserver.disconnect(); 414 - this.#resizeObserver.disconnect(); 425 + this.pause = true; 426 + this.subscriptions.forEach(unsubscribe => unsubscribe()); 427 + this.subscriptions.length = 0; 415 428 if (this.#frameID !== undefined) { 416 429 cancelAnimationFrame(this.#frameID); 417 430 this.#frameID = undefined;
+62
src/observers.ts
··· 1 + let intersectionObserver: IntersectionObserver | undefined; 2 + const intersectionListeners = new Map<Element, (isVisible: boolean) => void>(); 3 + const getIntersectionObserver = () => 4 + intersectionObserver || 5 + (intersectionObserver = new IntersectionObserver(entries => { 6 + for (const entry of entries) { 7 + const listener = intersectionListeners.get(entry.target); 8 + if (listener) { 9 + listener(entry.isIntersecting); 10 + } 11 + } 12 + })); 13 + 14 + export function trackVisibility( 15 + element: Element, 16 + onChange: (isVisible: boolean) => void 17 + ): () => void { 18 + const observer = getIntersectionObserver(); 19 + intersectionListeners.set(element, onChange); 20 + observer.observe(element); 21 + return () => { 22 + observer.unobserve(element); 23 + intersectionListeners.delete(element); 24 + }; 25 + } 26 + 27 + let resizeObserver: ResizeObserver | undefined; 28 + const resizeListeners = new Map< 29 + Element, 30 + (box: { inlineSize: number; blockSize: number }) => void 31 + >(); 32 + const getResizeObserver = () => 33 + resizeObserver || 34 + (resizeObserver = new ResizeObserver(entries => { 35 + for (const entry of entries) { 36 + const listener = resizeListeners.get(entry.target); 37 + if (listener) listener(entry.devicePixelContentBoxSize[0]); 38 + } 39 + })); 40 + 41 + export function trackResizes( 42 + element: Element, 43 + onChange: (box: { inlineSize: number; blockSize: number }) => void 44 + ): () => void { 45 + const observer = getResizeObserver(); 46 + resizeListeners.set(element, onChange); 47 + observer.observe(element, { box: 'device-pixel-content-box' }); 48 + return () => { 49 + resizeListeners.delete(element); 50 + }; 51 + } 52 + 53 + export function trackTextUpdates( 54 + element: Element, 55 + onChange: () => void 56 + ): () => void { 57 + const observer = new MutationObserver(onChange); 58 + observer.observe(element, { subtree: true, characterData: true }); 59 + return () => { 60 + observer.disconnect(); 61 + }; 62 + }