+5
.changeset/nice-streets-taste.md
+5
.changeset/nice-streets-taste.md
+50
-37
src/frag-canvas-element.ts
+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
+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
+
}