openpencil/packages/pen-renderer
Kayshen-X a4b7f62e9a Merge feat/rust-ification into v0.8.0 (Step 0 Rust workspace bootstrap)
Step 0 of OP Rust-ification (per kickoff spec v7 FROZEN):
- Cargo workspace at root (members = ["crates/*"], glob)
- 9 skeleton crates: openpencil-app, openpencil-shell-{core,web,native},
  pen-{types,core,engine,codegen,figma}
- rust-toolchain.toml pinned 1.85 (forced from 1.80 → 1.82 → 1.85
  due to crates.io ecosystem edition2024 requirements)
- deny.toml with kickoff §1.2 wasm32 ban invariant
- 2 GitHub Actions: rust-check.yml (3-platform native + cargo-deny)
  and wasm-bundle-check.yml (wasm32 forward + reverse cargo-deny bans)
- vendor/agent submodule → github.com/ZSeven-W/agent-rs
- Bun script wrappers (cargo:check / :test / :wasm-check / :deny)
- README "Rust subsystem" section + Phase boundary note

§1.2 invariants live:
- Forward wasm32 check: shell-web + 5 bucket A crates compile
- Reverse cargo-deny check bans: native + wasm32 both clean
- compile_error guard: shell-native fails wasm32 build with explicit
  message, validated by canary

Step 1+ owns real implementation; Phase 0 docs (snapshot / plan
patches / IPC inventory / parley-taffy matrix / cargo-deny validation)
in openpencil-docs.
2026-05-04 21:00:00 +08:00
..
src Merge feat/rust-ification into v0.8.0 (Step 0 Rust workspace bootstrap) 2026-05-04 21:00:00 +08:00
CLAUDE.md V0.7.1 (#102) 2026-04-13 21:30:23 +08:00
LICENSE V0.7.1 (#102) 2026-04-13 21:30:23 +08:00
package.json Merge origin/v0.8.0 into feat/rust-ification 2026-05-03 21:00:00 +08:00
README.md V0.7.1 (#102) 2026-04-13 21:30:23 +08:00
tsconfig.json V0.5.0 (#67) 2026-03-22 09:44:04 +08:00

@zseven-w/pen-renderer

Standalone CanvasKit/Skia renderer for OpenPencil design files. Render .op documents to a GPU-accelerated canvas — works in browsers, Node.js, and headless environments.

Install

npm install @zseven-w/pen-renderer canvaskit-wasm
# or
bun add @zseven-w/pen-renderer canvaskit-wasm

canvaskit-wasm is a peer dependency — you provide the WASM binary.

Overview

pen-renderer is a pure TypeScript + CanvasKit rendering pipeline with no React or framework dependency. It takes a PenDocument and renders it to a WebGL surface with GPU acceleration. The pipeline:

PenDocument → flattenToRenderNodes() → absolute positions → SkiaNodeRenderer → GPU canvas
                                           ↓
                                    SpatialIndex (R-tree) → hitTest / searchRect

Quick Start

import { loadCanvasKit, PenRenderer } from '@zseven-w/pen-renderer';

// 1. Initialize CanvasKit WASM (once, globally)
await loadCanvasKit();

// 2. Create renderer on a canvas element
const renderer = new PenRenderer(canvas, document, {
  width: 1920,
  height: 1080,
});

// 3. Render
renderer.render();

// 4. Interact
renderer.zoomToFit();
renderer.zoomTo(1.5, centerX, centerY);
renderer.pan(deltaX, deltaY);
const node = renderer.hitTest(mouseX, mouseY);

// 5. Cleanup
renderer.dispose();

Features

High-Level Renderer

PenRenderer provides a complete rendering solution with viewport, selection, and interaction:

const renderer = new PenRenderer(canvas, document, options);

renderer.setDocument(newDoc); // Update document
renderer.render(); // Trigger re-render
renderer.zoomToFit(); // Fit content to viewport
renderer.zoomTo(zoom, cx, cy); // Zoom to point
renderer.pan(dx, dy); // Pan viewport
renderer.hitTest(x, y); // Hit test at screen coords
renderer.dispose(); // Free resources

Document Flattening

Pre-process the document tree into flat render nodes with absolute positions:

import {
  flattenToRenderNodes,
  resolveRefs,
  premeasureTextHeights,
  remapIds,
} from '@zseven-w/pen-renderer';

// Flatten tree → absolute positions
const renderNodes = flattenToRenderNodes(children, viewport);

// Resolve $ref nodes to their source
const resolved = resolveRefs(renderNodes, document);

// Pre-measure text heights using Canvas 2D (for accurate layout)
premeasureTextHeights(renderNodes, canvasContext);

Viewport Math

Camera transforms for pan, zoom, and coordinate conversion:

import {
  viewportMatrix,
  screenToScene,
  sceneToScreen,
  zoomToPoint,
  getViewportBounds,
  isRectInViewport,
} from '@zseven-w/pen-renderer';

const matrix = viewportMatrix(zoom, panX, panY); // 3x3 CanvasKit matrix
const scene = screenToScene(mouseX, mouseY, viewport);
const screen = sceneToScreen(nodeX, nodeY, viewport);
const newVp = zoomToPoint(viewport, 2.0, centerX, centerY);

Spatial Index

R-tree backed spatial queries for click hit testing and marquee selection:

import { SpatialIndex } from '@zseven-w/pen-renderer';

const index = new SpatialIndex();
index.rebuild(renderNodes);

const clicked = index.hitTest(x, y); // topmost node at point
const selected = index.searchRect(x, y, w, h); // all nodes in rect
const node = index.get(nodeId); // lookup by ID

Low-Level Renderers

For custom rendering pipelines:

import {
  SkiaNodeRenderer,
  SkiaTextRenderer,
  SkiaFontManager,
  SkiaImageLoader,
} from '@zseven-w/pen-renderer';
Class Handles
SkiaNodeRenderer All node types — rectangles, ellipses, paths, images, icons, lines, polygons. Fills (solid, gradient, image), strokes, effects (shadow, blur), corner radius, clip, opacity, blend mode
SkiaTextRenderer Text layout and rendering via Paragraph API with bitmap fallback. FIFO caches (256 MB text, 64 MB paragraph)
SkiaFontManager Font loading — bundled fonts (Inter, Poppins, Roboto, etc.) + Google Fonts CSS fetching
SkiaImageLoader Async image loading with caching and custom source resolvers

Thumbnail Generation

Render individual nodes to offscreen thumbnails (used for git conflict UI, exports):

import { renderNodeThumbnail } from '@zseven-w/pen-renderer';

const dataUrl = renderNodeThumbnail(node, { width: 200, height: 200 });

Paint Utilities

import {
  parseColor,
  resolveFillColor,
  resolveStrokeColor,
  wrapLine,
  cssFontFamily,
  sanitizeSvgPath,
} from '@zseven-w/pen-renderer';

const color = parseColor('#2563EB'); // CanvasKit Color4f

API Reference

Category Exports
Init loadCanvasKit(options?), getCanvasKit()
Renderer PenRenderer
Flatten flattenToRenderNodes, resolveRefs, premeasureTextHeights, remapIds
Viewport viewportMatrix, screenToScene, sceneToScreen, zoomToPoint, getViewportBounds
Spatial SpatialIndex — rebuild, hitTest, searchRect, get
Node SkiaNodeRenderer
Text SkiaTextRenderer
Font SkiaFontManager, BUNDLED_FONT_FAMILIES
Image SkiaImageLoader
Paint parseColor, sanitizeSvgPath, cssFontFamily
Thumbnail renderNodeThumbnail

License

MIT