Render artwork at correct resolution when using vello on wasm (#3416)

* Work on fixing rendering for wasm+vello

* Render vello canvas in wasm at the correct resolution

* Cleanup unused surface rendering code

* Remove vector to raster conversion

* Remove desktop changes

* Revert window.rs changes

* Don't round logical coordinates

* Fix desktop compilation + don't round logical coordinates for svg rendering

* Further cleanup

* Compute logical size from acutal physical sizes
This commit is contained in:
Dennis Kobert
2025-11-24 15:23:27 +01:00
committed by GitHub
parent 6e66c79392
commit a932eaedcf
13 changed files with 197 additions and 216 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { getContext, onMount, tick } from "svelte";
import { getContext, onMount, onDestroy, tick } from "svelte";
import type { Editor } from "@graphite/editor";
import {
@@ -20,7 +20,7 @@
import type { DocumentState } from "@graphite/state-providers/document";
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
import { updateBoundsOfViewports as updateViewport } from "@graphite/utility-functions/viewports";
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -203,9 +203,18 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let canvas = (window as any).imageCanvases[canvasName];
if (canvasName !== "0" && canvas.parentElement) {
var newCanvas = window.document.createElement("canvas");
var context = newCanvas.getContext("2d");
// Get logical dimensions from foreignObject parent (set by backend)
const foreignObject = placeholder.parentElement;
if (!foreignObject) return;
const logicalWidth = parseFloat(foreignObject.getAttribute("width") || "0");
const logicalHeight = parseFloat(foreignObject.getAttribute("height") || "0");
// Clone canvas for repeated instances (layers that appear multiple times)
// Viewport canvas is marked with data-is-viewport and should never be cloned
const isViewport = placeholder.hasAttribute("data-is-viewport");
if (!isViewport && canvas.parentElement) {
const newCanvas = window.document.createElement("canvas");
const context = newCanvas.getContext("2d");
newCanvas.width = canvas.width;
newCanvas.height = canvas.height;
@@ -215,6 +224,10 @@
canvas = newCanvas;
}
// Set CSS size to logical resolution (for correct display size)
canvas.style.width = `${logicalWidth}px`;
canvas.style.height = `${logicalHeight}px`;
placeholder.replaceWith(canvas);
});
}
@@ -393,8 +406,8 @@
rulerHorizontal?.resize();
rulerVertical?.resize();
// Send the new bounds of the viewports to the backend
if (viewport.parentElement) updateViewport(editor);
// Note: Viewport bounds are now sent to the backend by the ResizeObserver in viewports.ts
// which provides pixel-perfect physical dimensions via devicePixelContentBoxSize
}
onMount(() => {
@@ -473,14 +486,21 @@
displayRemoveEditableTextbox();
});
// Once this component is mounted, we want to resend the document bounds to the backend via the resize event handler which does that
window.dispatchEvent(new Event("resize"));
// Setup ResizeObserver for pixel-perfect viewport tracking with physical dimensions
// This must happen in onMount to ensure the viewport container element exists
setupViewportResizeObserver(editor);
// Also observe the inner viewport for canvas sizing and ruler updates
const viewportResizeObserver = new ResizeObserver(() => {
updateViewportInfo();
});
if (viewport) viewportResizeObserver.observe(viewport);
});
onDestroy(() => {
// Cleanup the viewport resize observer
cleanupViewportResizeObserver();
});
</script>
<LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>

View File

@@ -10,7 +10,6 @@ import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode }
import { operatingSystem } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
import { stripIndents } from "@graphite/utility-functions/strip-indents";
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";
const BUTTON_LEFT = 0;
const BUTTON_MIDDLE = 1;
@@ -43,7 +42,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: AddEventListenerOptions }[] = [
{ target: window, eventName: "resize", action: () => updateBoundsOfViewports(editor) },
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => onBeforeUnload(e) },
{ target: window, eventName: "keyup", action: (e: KeyboardEvent) => onKeyUp(e) },
{ target: window, eventName: "keydown", action: (e: KeyboardEvent) => onKeyDown(e) },
@@ -529,8 +527,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
// Bind the event listeners
bindListeners();
// Resize on creation
updateBoundsOfViewports(editor);
// Return the destructor
return unbindListeners;

View File

@@ -1,12 +1,57 @@
import { type Editor } from "@graphite/editor";
export function updateBoundsOfViewports(editor: Editor) {
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
let resizeObserver: ResizeObserver | undefined;
export function setupViewportResizeObserver(editor: Editor) {
// Clean up existing observer if any
if (resizeObserver) {
resizeObserver.disconnect();
}
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
if (viewports.length <= 0) return;
const bounds = viewports[0].getBoundingClientRect();
const scale = window.devicePixelRatio || 1;
const viewport = viewports[0] as HTMLElement;
editor.handle.updateViewport(bounds.x, bounds.y, bounds.width, bounds.height, scale);
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const devicePixelRatio = window.devicePixelRatio || 1;
// Get exact device pixel dimensions from the browser
// Use devicePixelContentBoxSize for pixel-perfect rendering with fallback for Safari
let physicalWidth: number;
let physicalHeight: number;
if (entry.devicePixelContentBoxSize && entry.devicePixelContentBoxSize.length > 0) {
// Modern browsers (Chrome, Firefox): get exact device pixels from the browser
physicalWidth = entry.devicePixelContentBoxSize[0].inlineSize;
physicalHeight = entry.devicePixelContentBoxSize[0].blockSize;
} else {
// Fallback for Safari: calculate from contentBoxSize and devicePixelRatio
physicalWidth = entry.contentBoxSize[0].inlineSize * devicePixelRatio;
physicalHeight = entry.contentBoxSize[0].blockSize * devicePixelRatio;
}
// Compute the logical size which corresponds to the physical size
const logicalWidth = physicalWidth / devicePixelRatio;
const logicalHeight = physicalHeight / devicePixelRatio;
// Get viewport position
const bounds = entry.target.getBoundingClientRect();
// TODO: Consider passing physical sizes as well to eliminate pixel inaccuracies since width and height could be rounded differently
const scale = physicalWidth / logicalWidth;
editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale);
}
});
resizeObserver.observe(viewport);
}
export function cleanupViewportResizeObserver() {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = undefined;
}
}