@obinexusltd/css-canvas
v0.1.0
Published
CSS Canvas Engine - render native HTML5 CSS3 components directly onto an HTML5 Canvas
Maintainers
Readme
@obinexusltd/css-canvas
CSS Canvas Engine — render native HTML5 / CSS3 components directly onto an HTML5 <canvas> element. Write your UI with real CSS (custom properties, flexbox, transitions, animations) and let the engine rasterize it to the canvas every frame — enabling canvas-level effects, pixel manipulation, and WebGL compositing on top of ordinary DOM markup.
npm i @obinexusltd/css-canvasTable of Contents
- How it works
- Quick start
- Installation
- API reference
- Examples
- Building from source
- Known limitations
- License
How it works
┌──────────────────────────────────────┐
│ <canvas id="displayCanvas"> │ ← visible render surface
└──────────────────────────────────────┘
↑ ctx.drawImage each rAF
┌──────────────────────────────────────┐
│ .css-canvas-overlay (div) │ ← invisible DOM host
│ ├─ CSSComponent host (div) │ opacity: 0 / pointer-events: none
│ │ └─ your HTML + CSS + JS │ position: absolute
│ └─ … │ matches canvas rect
└──────────────────────────────────────┘- An invisible
<div>overlay is inserted next to the canvas, matching its dimensions. - Your CSS/HTML components live inside the overlay — the browser handles layout, custom properties, transitions, and animations natively.
- On every
requestAnimationFrametick the engine serialises the overlay to an SVG<foreignObject>blob and callsctx.drawImage, painting the CSS output onto the canvas. - You can then apply WebGL effects, pixel transforms, or draw additional canvas primitives on top using
engine.ctx.
Quick start
<!DOCTYPE html>
<html lang="en">
<head>
<style>
html, body { margin: 0; height: 100%; background: #0d0d1a; }
#displayCanvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="displayCanvas"></canvas>
<!-- Components live inside <template> so scripts don't execute on page load -->
<template id="my-template">
<div id="root">
<style>
.box {
--color: #33b658;
width: 200px; height: 200px;
background: var(--color);
border-radius: 12px;
margin: auto;
animation: spin 3s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<div class="box"></div>
</div>
</template>
<script type="module">
import { CSSCanvasEngine } from 'https://cdn.jsdelivr.net/npm/@obinexusltd/css-canvas/dist/css-canvas.esm.min.js';
const canvas = document.getElementById('displayCanvas');
const engine = new CSSCanvasEngine(canvas, {
width: window.innerWidth,
height: window.innerHeight,
background: '#0d0d1a',
});
window.addEventListener('resize', () =>
engine.resize(window.innerWidth, window.innerHeight)
);
const source = document.getElementById('my-template').content.querySelector('#root');
await engine.mount(source, 0, 0, window.innerWidth, window.innerHeight);
engine.start();
</script>
</body>
</html>Installation
npm / pnpm / yarn
npm i @obinexusltd/css-canvasCDN (UMD, browser global CSSCanvas)
<script src="https://cdn.jsdelivr.net/npm/@obinexusltd/css-canvas/dist/css-canvas.umd.min.js"></script>
<script>
const { CSSCanvasEngine } = CSSCanvas;
</script>CDN (ESM)
import { CSSCanvasEngine } from 'https://cdn.jsdelivr.net/npm/@obinexusltd/css-canvas/dist/css-canvas.esm.min.js';API reference
CSSCanvasEngine
The main entry point. Manages the canvas, the overlay, mounted components, and the render loop.
new CSSCanvasEngine(canvas, opts?)
| Parameter | Type | Default | Description |
|-------------------|----------------------|------------------|-------------|
| canvas | HTMLCanvasElement | required | Target render surface |
| opts.width | number | canvas.width | Initial canvas width in px |
| opts.height | number | canvas.height | Initial canvas height in px |
| opts.background | string | 'transparent' | CSS colour painted behind every frame |
| opts.fps | number | 60 | Target frames per second |
| opts.debug | boolean | false | Log renderer warnings to the console |
const engine = new CSSCanvasEngine(canvas, {
width: 800,
height: 600,
background: '#111',
fps: 30,
debug: true,
});engine.mount(source, x?, y?, width?, height?) → Promise<CSSComponent>
Mount an HTML element as a component inside the engine overlay.
| Parameter | Type | Default | Description |
|-----------|---------------------------------|-----------------|-------------|
| source | Element \| DocumentFragment | required | Root element or <template>.content fragment to mount |
| x | number | 0 | Left offset in canvas pixels |
| y | number | 0 | Top offset in canvas pixels |
| width | number | canvas.width | Component width in canvas pixels |
| height | number | canvas.height | Component height in canvas pixels |
The engine clones the element's innerHTML, strips <script> tags, inserts the markup into the live overlay, then executes the scripts so that DOM APIs such as getBoundingClientRect and getComputedStyle return real values.
const template = document.getElementById('eye-template');
const source = template.content.querySelector('#eye');
const comp = await engine.mount(source, 0, 0, window.innerWidth, window.innerHeight);
console.log('Mounted:', comp.id);engine.unmount(id)
Remove a mounted component by its ID.
engine.unmount(comp.id);engine.start() → this
Start the rAF render loop. Safe to call multiple times.
engine.start();engine.stop() → this
Pause the render loop without destroying the engine or its components.
engine.stop();engine.resize(width, height) → this
Resize the canvas and overlay. Call this inside a window 'resize' listener.
window.addEventListener('resize', () =>
engine.resize(window.innerWidth, window.innerHeight)
);engine.destroy()
Stop the render loop, remove the overlay from the DOM, and clear all mounted components.
engine.destroy();Properties
| Property | Type | Description |
|---------------------|---------------------------------|-------------|
| engine.canvas | HTMLCanvasElement | The render surface |
| engine.ctx | CanvasRenderingContext2D | The 2D drawing context |
| engine.overlay | HTMLElement | The invisible overlay div (read-only) |
| engine.components | Map<string, CSSComponent> | All currently mounted components |
| engine.running | boolean | Whether the render loop is active |
| engine.background | string | Background fill colour |
CSSComponent
Returned by engine.mount(). Provides a handle for controlling a mounted component at runtime.
comp.show() → this
Make the component visible in the next frame.
comp.hide() → this
Hide the component without unmounting it.
comp.moveTo(x, y) → this
Reposition the component within canvas space.
comp.moveTo(100, 200);comp.resize(width, height) → this
Resize the component's host element.
comp.resize(400, 300);comp.setVar(name, value) → this
Set a CSS custom property on the component host, cascading to all children.
comp.setVar('--eye-size', '160px');
comp.setVar('--eye-color', '#ff6b6b');comp.getVar(name) → string
Read a CSS custom property from the component's computed style.
const size = comp.getVar('--eye-size'); // '160px'comp.query(selector) → Element | null
Query for a descendant inside this component's host.
const pupil = comp.query('.pupil');comp.queryAll(selector) → NodeList
Query for all matching descendants.
const lids = comp.queryAll('.eye-lid');comp.destroy()
Remove the component from the overlay and clean up its DOM node.
comp.destroy();Properties
| Property | Type | Description |
|---------------|---------------|-------------|
| comp.id | string | Auto-generated unique ID |
| comp.host | HTMLElement | Wrapper div inside the overlay |
| comp.x | number | Current left offset in canvas pixels |
| comp.y | number | Current top offset in canvas pixels |
| comp.width | number | Current width in canvas pixels |
| comp.height | number | Current height in canvas pixels |
| comp.visible| boolean | Whether the component is currently visible |
CSSCanvasRenderer
Low-level rasterizer used internally by the engine. Exposed for advanced use cases — for example if you want to rasterize arbitrary DOM nodes outside the engine's render loop.
CSSCanvasRenderer.elementToImage(element, width, height) → Promise<HTMLImageElement>
Static helper. Serialises a live DOM element to an <img> via an SVG <foreignObject> blob.
import { CSSCanvasRenderer } from '@obinexusltd/css-canvas';
const img = await CSSCanvasRenderer.elementToImage(
document.querySelector('.my-component'),
800,
600
);
ctx.drawImage(img, 0, 0);renderer.renderFrame(overlay, background?) → Promise<void>
Clears the canvas, fills the background, and paints the overlay for one frame.
Examples
Interactive eye demo
The examples/ folder in the repository contains a full eye-tracking component demo that demonstrates:
- CSS custom properties (
--eye-size,--eye-color) as design tokens mousemoveinteraction inside a<template>component- Dynamic eyelid animation via CSS transitions
- Responsive resize handling
Run it locally:
git clone https://github.com/obinexusmk2/css-canvas.git
cd css-canvas
npx serve examples/
# open http://localhost:3000Post-processing with engine.ctx
You can draw on top of the rasterized CSS frame each tick by listening for
the next rAF and accessing engine.ctx directly:
engine.start();
function postProcess() {
requestAnimationFrame(postProcess);
const { ctx, canvas } = engine;
// Invert colours over a 200×200 region
const imageData = ctx.getImageData(0, 0, 200, 200);
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i] = 255 - imageData.data[i];
imageData.data[i + 1] = 255 - imageData.data[i + 1];
imageData.data[i + 2] = 255 - imageData.data[i + 2];
}
ctx.putImageData(imageData, 0, 0);
}
postProcess();Building from source
git clone https://github.com/obinexusmk2/css-canvas.git
cd css-canvas
npm install
npm run buildOutput files are written to dist/:
| File | Format | Use case |
|-----------------------------|--------|----------|
| css-canvas.esm.js | ESM | Bundlers (Vite, Webpack, Rollup) |
| css-canvas.esm.min.js | ESM | CDN ESM import, minified |
| css-canvas.cjs.js | CJS | Node.js, Jest, older bundlers |
| css-canvas.umd.js | UMD | Browser <script> tag (development) |
| css-canvas.umd.min.js | UMD | Browser <script> tag (production) |
Watch mode:
npm run devKnown limitations
- External images inside components must be CORS-accessible or inlined as
data:URIs. The SVG<foreignObject>serialiser cannot embed cross-origin resources. - Custom fonts must be declared with
@font-face { src: url('data:…') }. Web font URLs are blocked by the SVG blob context. - CSS filters and
mix-blend-modehave partial support inside<foreignObject>on some browsers — test on your target. <canvas>elements inside mounted components will appear tainted in Firefox due to the SVG rasterisation path.- Performance — each frame serialises the full overlay to a blob and decodes it as an image. For high-frequency animations (60 fps, large canvases) consider lowering
opts.fpsor splitting large components into smaller ones so unchanged regions can be culled.
License
ISC © Nnamdi Michael Okpala / OBINexus
Part of the OBINexus open-source ecosystem.
