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"));