HTML-in-Canvas is coming to browsers. Past the cool demo, it gets messy. Prism handles the lifecycle so you don't have to.
Prism is a runtime for managed HTML surfaces inside canvas applications.
Author visual surfaces with HTML/CSS/SVG. Compose them inside Canvas 2D workflows. Ship data visualizations, design tools, generative art, interactive editors and more.
Note: Native mode requires Chrome Canary with
chrome://flags/#canvas-draw-elementenabled. Prism falls back to a compatibility backend in unsupported browsers.
Your app owns the scene, drawing model, animation loop, and state. Prism owns the lifecycle for DOM-authored canvas surfaces — registration, bounds, paint, invalidation, readiness, coordinate helpers, and cleanup.
- Atlantic — North Atlantic tropical cyclone tracker, 2000–2025
- Composer — OG image generator
- Atelier — Generative type art
pnpm add @synthesisengineering/prismUsing an AI coding agent? Install the Prism runtime skill:
npx skills add synthesiseng/prism --skill prism-runtimeThe skill teaches agents the Prism runtime contract:
- import from the package root:
@synthesisengineering/prism - register HTML/CSS/SVG DOM nodes as surfaces
- draw surfaces inside
onPaint() - wait for
document.fonts.readyandruntime.paintOnce()before export - avoid
html2canvas,dom-to-image, rawdrawElementImage(), and deep imports
Source: skills/prism-runtime/SKILL.md.
import { CanvasRuntime } from "@synthesisengineering/prism";
const runtime = new CanvasRuntime(canvas, { backend: "auto" });
runtime.start();Register surfaces when you need them:
const surface = runtime.registerSurface(element, {
bounds: { x: 0, y: 0, width: 1200, height: 630 }
});
runtime.onPaint(({ drawSurface }) => {
drawSurface(surface);
});Use paintOnce() to wait for one complete paint pass before exporting:
await document.fonts.ready;
await runtime.paintOnce();
const blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob(resolve, "image/png");
});Prism prefers native HTML-in-Canvas and falls back automatically:
new CanvasRuntime(canvas, { backend: "auto" }); // default
new CanvasRuntime(canvas, { backend: "native" }); // native only
new CanvasRuntime(canvas, { backend: "fallback" }); // compatibility onlyif (runtime.backendKind !== "native") {
console.warn("Compatibility mode is lower fidelity.");
}The fallback backend is lower fidelity. Native is the target.
Surface bounds and input coordinates use CSS pixels. Direct drawing inside onPaint() uses canvas backing-store pixels.
Use runtime helpers when aligning manual canvas drawing with surface coordinates:
runtime.onPaint(({ ctx, drawSurface }) => {
drawSurface(surface);
const size = runtime.cssLengthToCanvasPixels(24);
ctx.fillRect(0, 0, size, size);
});Helpers: clientToCanvasPoint() · cssLengthToCanvasPixels() · cssPointToCanvasPixels()
// Register
const surface = runtime.registerSurface(element, {
bounds: { x: 0, y: 0, width: 320, height: 180 }
});
// Update bounds
surface.setBounds({ x: 24, y: 32, width: 360, height: 220 });
// Remove
runtime.unregisterSurface(surface);
// or
surface.dispose();
// Destroy runtime
runtime.destroy();import { CanvasRuntime } from "@synthesisengineering/prism";
import type {
CanvasBackendKind,
CanvasBackendPreference,
CanvasPoint,
CanvasRuntimeOptions,
CanvasSurface,
PaintHandler,
SurfaceBoundsInput,
SurfaceOptions,
UpdateHandler
} from "@synthesisengineering/prism";new CanvasRuntime(canvas, options);Options: backend: "auto" | "native" | "fallback"
Properties: canvas · width · height · pixelRatio · backendKind
Methods: registerSurface() · unregisterSurface() · onUpdate() · onPaint() · invalidate() · paintOnce() · start() · stop() · destroy() · clientToCanvasPoint() · cssLengthToCanvasPixels() · cssPointToCanvasPixels()
Returned by registerSurface(). Do not construct directly.
Properties: element · isDisposed
Methods: getBounds() · setBounds() · dispose()
- Native HTML-in-Canvas is experimental.
- Fallback mode is compatibility-only, lower fidelity, and not equivalent to native HTML rendering.
onPaint()andonUpdate()are additive. Each call registers another handler; it does not replace previous handlers.invalidate()schedules another Prism-owned paint pass when app-owned state changes outside Prism APIs.paintOnce()works withoutstart(). It waits for one runtime-owned paint pass and does not export image data itself.- Destroying a runtime disposes its registered surfaces. Disposed surfaces report
isDisposed === true;getBounds()andsetBounds()throw. - Surface bounds and input coordinates use CSS pixels.
- Direct
ctxdrawing uses canvas backing-store pixels. - Undrawn surfaces are inactive for pointer and focus handling until they are drawn again.
- Native mode depends on browser support for HTML-in-Canvas.
- The fallback backend is lower fidelity than native HTML rendering.
- Prism v1 is 2D-first.
- WebGL/WebGPU integrations are future-facing.
pnpm install
pnpm typecheck
pnpm test
pnpm e2e
pnpm lint
pnpm buildPrism is built around the HTML-in-Canvas proposal and related WICG standards work. All credit for the underlying platform capability goes to the proposal authors and the WICG.
MIT © Synthesis
