Skip to main content

Overlay Utils

@oh-just-another/renderer-canvas ships DOM helpers for hi-DPI, multi-layer canvas stacks. The scene is split across three stacked canvases โ€” background, main, and overlay โ€” so drawing onto one layer never invalidates the others.

Hi-DPI setupโ€‹

setupHiDpi configures a canvas for crisp rendering on retina displays: it scales the bitmap by the device pixel ratio, sets the CSS size, and (by default) applies a context transform so your draw calls stay in CSS pixels. The default ratio is cappedDpr() โ€” window.devicePixelRatio clamped to MAX_DEVICE_PIXEL_RATIO; pass an explicit dpr to opt out of the cap. It is idempotent (early-returns when sizes already match, so canvas content is not wiped) and safe to call from a ResizeObserver.

import { setupHiDpi } from "@oh-just-another/renderer-canvas";

const dpr = setupHiDpi(canvas, width, height);
// Pass setupContext=false for a canvas headed for WebGL2 โ€” touching the
// "2d" context would poison the slot for getContext("webgl2"):
setupHiDpi(glCanvas, width, height, window.devicePixelRatio, false);

Layered canvas stackโ€‹

LayeredCanvas manages one <canvas> per layer inside a host element. The background sits at the bottom of the DOM stack and the overlay at the top; each layer is a Canvas2DTarget with its own CSS-pixel coordinate space. The host is made position: relative automatically.

import { LayeredCanvas, type LayeredCanvasOptions } from "@oh-just-another/renderer-canvas";

const options: LayeredCanvasOptions = { layers: ["background", "main", "overlay"] };
const surface = new LayeredCanvas(host, width, height, options);

The layer names and their stacking order come from @oh-just-another/renderer-core:

import { LAYER_ORDER, type LayerName } from "@oh-just-another/renderer-core";
// LAYER_ORDER === ["background", "main", "overlay"]

Backend-selectable surfacesโ€‹

createLayeredSurface builds a LayeredSurface for a chosen RendererBackend โ€” "canvas2d" (default), "webgl2", or "offscreen" โ€” with the same per-layer model. createLayeredSurfaceWithFallback tries the requested backend and falls back down the chain on failure, returning the effectiveBackend so the host can report what was activated. Pick the backend via capability detection (see the performance page).

Each layer's RenderTarget is reached via surface.get(name); call surface.present() once per frame so deferred backends (offscreen) ship their buffered commands.

Drawing editor chrome onto the overlayโ€‹

Selection outlines, resize handles, previews, and peer cursors are drawn by renderOverlay from @oh-just-another/state โ€” a pure draw call targeting the overlay layer:

import { renderOverlay } from "@oh-just-another/state";

renderOverlay(scene, selection, surface.get("overlay"));