Browse by type
A 2D canvas rendering library for Deno that brings the HTML5 Canvas API to native applications. Powered by NanoVG for vector graphics, SDL3 for window management, and OpenGL for rendering.
Supported platforms: macOS (arm64), Linux (x64, arm64), Windows (x64, arm64).
Deno — no install step; the examples below work as-is.
Bun — Bun doesn't resolve jsr: specifiers natively, so install from JSR's npm proxy first:
bunx jsr add @diyorbek/canvas-native
Then drop the jsr: prefix from imports throughout the examples below — e.g. 'jsr:@diyorbek/canvas-native' becomes '@diyorbek/canvas-native'.

Everything lives in one script — the library handles the window, event loop, and worker setup internally.
// demo.ts
import {
createCanvas,
requestAnimationFrame,
} from 'jsr:@diyorbek/canvas-native';
const { ctx, width, height } = await createCanvas(400, 300, 'Hello');
function draw(t: number) {
ctx.clearRect(0, 0, width, height);
const r = 50 + Math.sin(t / 300) * 30;
ctx.beginPath();
ctx.arc(width / 2, height / 2, r, 0, Math.PI * 2);
ctx.fillStyle = 'tomato';
ctx.fill();
ctx.fillStyle = 'black';
ctx.font = '16px sans-serif';
ctx.fillText('Hello, Canvas Native', 20, 30);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
# Bun
bun demo.ts
# Deno
deno --allow-ffi --allow-env --allow-read demo.ts
⚠️ Heads-up: your script runs twice.
Internally,
createCanvas()re-launches your script as a Web Worker — the main thread runs the native window loop, and the worker runs your drawing code. That means any code at the top level of your file executes on both threads, once on each.
typescript // ✅ Safe — only imports and createCanvas() before the await import { createCanvas, requestAnimationFrame, } from 'jsr:@diyorbek/canvas-native'; const { ctx, width, height } = await createCanvas(800, 500, 'Demo'); // All your setup and drawing code goes here. // It only runs in the worker.
typescript // ⚠️ Runs twice — avoid import { createCanvas } from 'jsr:@diyorbek/canvas-native'; console.log('starting up'); // prints twice await fetch('https://example.com'); // hits the network twice const { ctx } = await createCanvas(800, 500, 'Demo');If you need full control and want to guarantee single-execution of setup code, use the two-file API below.
Use this when you need finer control over the main/worker split, or want to guarantee that setup code only runs once.
app.ts — main thread (creates the window):
import { createWindow } from 'jsr:@diyorbek/canvas-native/app';
const { mainLoop } = await createWindow(800, 500, 'My App', './worker.ts');
mainLoop();
worker.ts — drawing thread:
import {
initCanvas,
requestAnimationFrame,
} from 'jsr:@diyorbek/canvas-native/worker';
const ctx = await initCanvas();
const W = 800;
const H = 500;
function draw(t: number) {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#f00';
ctx.fillRect(100, 100, 200, 150);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
# Bun
bun app.ts
# Deno
deno --allow-ffi --allow-env --allow-read app.ts
Prerequisites: Deno, CMake 3.24+, a C++23 compiler.
git clone https://github.com/diyorbek/canvas-native
cd canvas-native
cmake -B build && cmake --build build --target canvasnative
The build output at ./build/libcanvasnative.* is picked up automatically.
canvas-native looks for the native library in this order:
CANVAS_NATIVE_LIB env var (absolute path to your own build)./build/libcanvasnative.{dylib,so,dll} in the current working directory (local dev builds)~/Library/Caches/canvas-native/v<version>/ on macOS$XDG_CACHE_HOME/canvas-native/v<version>/ on Linux%LOCALAPPDATA%\canvas-native\v<version>\ on WindowsContributions are welcome! Areas for enhancement:
requestAnimationFrame)$ claude mcp add canvas-native \
-- python -m otcore.mcp_server <graph>