Skip to content
synthesisengPublic

About

Native-first HTML-in-Canvas runtime for managed DOM surfaces in canvas applications.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

94 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Prism

Prism

npm alpha version npm downloads node version CI status license

Docs

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 qualification covers Chrome for Testing 158.0.8115.0 on macOS ARM64 with chrome://flags/#canvas-draw-element enabled and Chromium sandboxing enabled. Other browser configurations are not qualified. Fallback draws placeholders, not HTML snapshots.


How It Works

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.

Built With Prism

  • Atlantic — North Atlantic tropical cyclone tracker, 2000–2025
  • Composer — OG image generator
  • Atelier — Generative type art

Installation

Install this package version:

pnpm add @synthesisengineering/prism@0.1.0-alpha.10

Agent Skill

Using an AI coding agent? Install the Prism runtime skill:

npx skills add synthesiseng/prism --skill prism-runtime

The 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.ready and runtime.paintOnce() before export
  • avoid html2canvas, dom-to-image, raw drawElementImage(), and deep imports

Source: skills/prism-runtime/SKILL.md.

Quickstart

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

Export Readiness

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

Backend Modes

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 only
if (runtime.backendKind !== "native") {
  console.warn("Compatibility mode draws placeholders, not HTML.");
}

The fallback backend draws placeholders, not HTML snapshots. Native is the target.

Coordinate Spaces

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()

Surface Lifecycle

// 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();

API

import { CanvasRuntime } from "@synthesisengineering/prism";
import type {
  CanvasBackendKind,
  CanvasBackendPreference,
  CanvasPoint,
  CanvasRuntimeOptions,
  CanvasSurface,
  PaintHandler,
  SurfaceBoundsInput,
  SurfaceOptions,
  UpdateHandler
} from "@synthesisengineering/prism";

CanvasRuntime

new CanvasRuntime(canvas, options);

Options: backend: "auto" | "native" | "fallback", nativePaintTimeoutMs

Properties: canvas · width · height · pixelRatio · backendKind · diagnostics

Methods: registerSurface() · unregisterSurface() · onUpdate() · onPaint() · invalidate() · paintOnce() · start() · stop() · destroy() · clientToCanvasPoint() · cssLengthToCanvasPixels() · cssPointToCanvasPixels()

CanvasSurface

Returned by registerSurface(). Do not construct directly.

Properties: element · isDisposed

Methods: getBounds() · setBounds() · dispose()

Native lifecycle

This section describes alpha.10. See the migration note for the changes from alpha.8 and alpha.9.

The current native path uses canvas content="drawable" and surface drawable. Surface bounds describe the border box in CSS pixels. Prism writes box-sizing: border-box, width and height as well as the drawing rectangle. The browser synchronizes native input geometry; application CSS transforms are preserved.

Prism applies layout sizing before the browser snapshot. A size change made inside onPaint() defers drawing until a subsequent paint has the new layout; paintOnce() remains pending until that pass completes. Function-based bounds are still evaluated when drawSurface() is called, so their first attempted paint can also be deferred. Retries share the original visible-time timeout budget. Four consecutive incomplete layout passes stop the episode with LayoutConvergenceError, not PrismNativeError. Pending callers reject; native health keeps its last platform outcome. Fix the layout, then call setBounds() or invalidate() outside paint to recover. Calls inside paint do not reset the limit. Until recovery, paintOnce() rejects with the same error. A timeout during layout retry also stops further retries. Keep paint handlers repeatable: a deferred pass can invoke them again, and drawing already performed in an incomplete pass is not rolled back. Application CSS transforms are preserved. Prism never compensates for a browser hit-testing failure by rewriting transforms or pointer coordinates.

Prism owns the outer size, so measure an auto-height inner wrapper instead of the surface's offsetHeight. Add the surface's padding and borders to the measured content height. Unchanged literal bounds do not request a repaint, except when an outside-paint call restarts a stopped layout episode. A position change made after drawing requests the next pass. See the alpha.8 → alpha.10 migration note.

A canvas has one live runtime, and an element has one active surface within this module instance. Duplicate registration throws, including within the same runtime. Dispose or unregister before registering an element again. On release, Prism restores an initial attribute or style property only if its current value still matches Prism's last write. Application changes to other values survive cleanup, including CSS transforms and box-sizing. This also applies to each per-frame pointer-events and inert write. If an application writes exactly Prism's value, that write is indistinguishable and cleanup restores the initial value. DOM parent placement is restored as before.

stop() stops animation, leaving explicit paintOnce() requests active. Concurrent callers share the next pass; requests made inside a paint handler wait for a later pass. destroy() rejects outstanding requests and releases resources.

Pending native paintOnce() callers own a visible-time diagnostic deadline (nativePaintTimeoutMs). Invalidation without waiting callers does not arm a deadline. A caller joining an outstanding invalidation starts its budget then; repeated invalidation and concurrent callers do not reset it. Hidden time pauses the remaining budget without resetting it; a paint completed while hidden still succeeds. The default is provisionally 2000 ms and is not a browser scheduling guarantee or a measured performance target.

diagnostics reports backend, capability and native health (idle, pending, healthy, failed). Capability describes API presence, not verified rendering. PrismNativeError reports paint-timeout or draw-failed; draw failures preserve the original exception as cause. Catching a failed drawSurface() inside an application handler does not make that pass successful: its waiting callers still reject and diagnostics retain the native failure, even if a later draw in the same pass succeeds. A later successful paint can restore health. Application paint-handler exceptions reject the callers waiting for that pass with the original thrown value. When none are waiting, Prism calls reportError(). Application exceptions do not become native errors, enter lastError, or mark platform health failed; a delivered native pass can be healthy even when app code throws. A request for a later pass remains pending. The backend never changes silently after construction. Legacy or partial native API shapes are rejected in forced native mode; legacy users can pin alpha.8.

Hosted native qualification passed on main 06b377d3 for Chrome for Testing 158.0.8115.0 on macOS ARM64 with CanvasDrawElement and Chromium sandboxing enabled. It covers ten geometry/pixel/click scenarios, eleven lifecycle/layout tests and seven Atlantic cases against the packed candidate, with five intentional cross-project skips. Ordinary CI also passed, including Node 22 package consumers. This bounded experimental configuration does not establish Linux native qualification or general browser support. The fallback draws placeholders, not HTML. See the release notes for qualification limits and release status. Native drawable also creates isolation; synthetic tests show that some remote image content is omitted, so image/export fidelity must be checked per use case.

Considerations

  • Native HTML-in-Canvas is experimental.
  • Fallback mode is compatibility-only and draws placeholders, not HTML snapshots.
  • onPaint() and onUpdate() 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 without start(). 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() and setBounds() throw.
  • Surface bounds and input coordinates use CSS pixels.
  • Direct ctx drawing uses canvas backing-store pixels.
  • Omitted surfaces are inactive for pointer and focus handling. Surfaces deferred for native layout retain their prior interaction state until the retry completes or fails.

Limitations

  • Native mode depends on browser support for HTML-in-Canvas.
  • The fallback backend draws placeholders rather than native HTML content.
  • Prism v1 is 2D-first.
  • WebGL/WebGPU integrations are future-facing.

Development

pnpm install
pnpm typecheck
pnpm test
pnpm e2e
pnpm lint
pnpm build

Platform Credit

Prism 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.

License

MIT © Synthesis

About

Native-first HTML-in-Canvas runtime for managed DOM surfaces in canvas applications.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages