Skip to content

Commit

Permalink
Fix resize observer for WebKit
Browse files Browse the repository at this point in the history
  • Loading branch information
sockmaster27 committed Jan 5, 2025
1 parent 758370a commit 3130323
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 39 deletions.
57 changes: 18 additions & 39 deletions packages/svader/src/lib/BaseShader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { pixelScale, clamp } from "./utils.js";
import { intersectionObserver } from "./intersectionObserver.js";
import { onDestroy, onMount } from "svelte";
import { devicePixelResizeObserver } from "./devicePixelResizeObserver.js";
/**
* The width of the canvas element.
Expand Down Expand Up @@ -75,36 +76,6 @@
/** @type {HTMLCanvasElement} */
export let canvasElement;
/**
* Size of the containing element in physical device pixels, as reported by `devicePixelContentBoxSize`.
*
* @type {ResizeObserverSize[]}
*/
let containerSize;
/**
* Width of the containing element in physical device pixels.
*/
$: containerWidth = containerSize?.[0]?.inlineSize;
/**
* Height of the containing element in physical device pixels.
*/
$: containerHeight = containerSize?.[0]?.blockSize;
/**
* Size of the canvas in physical device pixelss, as reported by `devicePixelContentBoxSize`.
*
* @type {ResizeObserverSize[]}
*/
let canvasSize;
/**
* Width of the canvas in physical device pixels.
*/
$: canvasWidth = canvasSize?.[0]?.inlineSize;
/**
* Height of the canvas in physical device pixels.
*/
$: canvasHeight = canvasSize?.[0]?.blockSize;
/**
* The handle returned by {@linkcode requestAnimationFrame}.
* `null` if no render pass has been requested.
Expand Down Expand Up @@ -147,12 +118,14 @@
}
/**
* @param {number} canvasWidth
* @param {number} canvasHeight
* @param {import("./devicePixelResizeObserver.js").DevicePixelResizeEvent} event
*/
function updateCanvasSizeInner(canvasWidth, canvasHeight) {
function updateCanvasSizeInner(event) {
// Resizing must happen right before the next render pass.
renderCallbacks.push(() => {
const canvasWidth = event.detail.width;
const canvasHeight = event.detail.height;
canvasElement.width = canvasWidth;
canvasElement.height = canvasHeight;
Expand All @@ -162,11 +135,15 @@
requestRender();
}
$: if (canvasWidth !== undefined && canvasHeight !== undefined)
updateCanvasSizeInner(canvasWidth, canvasHeight);
/**
* @param {import("./devicePixelResizeObserver.js").DevicePixelResizeEvent} event
*/
function updateContainerSizeInner(event) {
const canvasWidth = event.detail.width;
const canvasHeight = event.detail.height;
$: if (containerWidth !== undefined && containerHeight !== undefined)
updateContainerSize(containerWidth, containerHeight);
updateContainerSize(canvasWidth, canvasHeight);
}
$: if (offsetX !== undefined && offsetY !== undefined)
updateOffset(offsetX, offsetY);
Expand Down Expand Up @@ -209,15 +186,17 @@
{#if canRender}
<div
bind:this={containerElement}
bind:devicePixelContentBoxSize={containerSize}
use:devicePixelResizeObserver
on:devicepixelresize={updateContainerSizeInner}
use:intersectionObserver
on:intersectionchanged={updateCanvasCutout}
style:--width={width}
style:--height={height}
>
<canvas
bind:this={canvasElement}
bind:devicePixelContentBoxSize={canvasSize}
use:devicePixelResizeObserver
on:devicepixelresize={updateCanvasSizeInner}
use:intersectionObserver={{ rootMargin: "100px" }}
on:intersectionchanged={updateCanvasCutout}
class:offset-from-bottom={offsetFromBottom}
Expand Down
76 changes: 76 additions & 0 deletions packages/svader/src/lib/devicePixelResizeObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* @template T
* @template {Record<string, any>} A
* @typedef {import("svelte/action").ActionReturn<T, A>} ActionReturn<T, A>
*/

/**
* @typedef {CustomEvent<{ width: number, height: number }>} DevicePixelResizeEvent
* @typedef {{ "on:devicepixelresize"?: (e: DevicePixelResizeEvent) => void }} Attributes
*/

/**
* Check if "devicePixelContentBoxSize" is supported (it's not in Safari).
*
* @returns {Promise<boolean>}
*/
async function checkDevicePixelContentBox() {
return new Promise(resolve => {
const observer = new ResizeObserver(entries => {
resolve(
entries.every(entry => "devicePixelContentBoxSize" in entry),
);
observer.disconnect();
});
observer.observe(document.body, { box: "device-pixel-content-box" });
}).catch(() => false);
}
const hasDevicePixelContentBoxPromise = checkDevicePixelContentBox();
let hasDevicePixelContentBox = false;

/**
* Observe all changes to the device-pixel-content-box of a node.
*
* @param {Element} node
* @returns {ActionReturn<void, Attributes>}
*/
export function devicePixelResizeObserver(node) {
let observer = new ResizeObserver(callback);

/** @param {ResizeObserverEntry[]} entries */
function callback(entries) {
const entry = entries[0];

const detail = hasDevicePixelContentBox
? {
width: entry.devicePixelContentBoxSize[0].inlineSize,
height: entry.devicePixelContentBoxSize[0].blockSize,
}
: {
// Not perfect, but it's the best we can do in this case.
width:
entry.contentBoxSize[0].inlineSize *
window.devicePixelRatio,
height:
entry.contentBoxSize[0].blockSize *
window.devicePixelRatio,
};

node.dispatchEvent(new CustomEvent("devicepixelresize", { detail }));
}

hasDevicePixelContentBoxPromise.then(r => {
hasDevicePixelContentBox = r;
observer.observe(node, {
box: hasDevicePixelContentBox
? "device-pixel-content-box"
: "content-box",
});
});

return {
destroy() {
observer.disconnect();
},
};
}

0 comments on commit 3130323

Please sign in to comment.