Documentation Index
Fetch the complete documentation index at: https://mintlify.com/open-pencil/open-pencil/llms.txt
Use this file to discover all available pages before exploring further.
SkiaRenderer API
CanvasKit (Skia WASM) renderer for OpenPencil. Renders scene graph to WebGL surface with viewport culling, picture caching, and UI overlays.
Import
import { SkiaRenderer } from '@open-pencil/core'
import type { RenderOverlays } from '@open-pencil/core'
import { getCanvasKit } from '@open-pencil/core'
Constructor
const ck = await getCanvasKit()
const surface = ck.MakeWebGLCanvasSurface(canvas)!
const renderer = new SkiaRenderer(ck, surface)
Creates a new renderer instance. Requires a CanvasKit instance and a WebGL surface.
Viewport Properties
panX, panY
panX: number
panY: number
Pan offset in screen pixels.
zoom
Zoom level (1.0 = 100%).
dpr
Device pixel ratio (for retina displays).
viewportWidth, viewportHeight
viewportWidth: number
viewportHeight: number
Canvas size in CSS pixels.
pageId
Current page to render.
pageColor
Canvas background color.
showRulers
Whether to render rulers (default: true).
Rendering
render()
render(
graph: SceneGraph,
selectedIds: Set<string>,
overlays?: RenderOverlays,
sceneVersion?: number
): void
Renders the scene to the canvas. Uses picture caching when sceneVersion matches.
renderer.panX = 100
renderer.panY = 50
renderer.zoom = 1.5
renderer.pageId = page.id
renderer.render(graph, new Set([rect.id]), {
hoveredNodeId: hoveredId,
snapGuides: [{ axis: 'x', position: 100, from: 0, to: 500 }]
}, sceneVersion)
Overlays:
interface RenderOverlays {
hoveredNodeId?: string | null
editingTextId?: string | null
textEditor?: TextEditor | null
marquee?: Rect | null
snapGuides?: SnapGuide[]
rotationPreview?: { nodeId: string; angle: number } | null
dropTargetId?: string | null
layoutInsertIndicator?: {
x: number
y: number
length: number
direction: 'HORIZONTAL' | 'VERTICAL'
} | null
penState?: PenState | null
remoteCursors?: Array<{
name: string
color: Color
x: number
y: number
selection?: string[]
}>
}
renderSceneToCanvas()
renderSceneToCanvas(canvas: Canvas, graph: SceneGraph, pageId: string): void
Renders scene to a CanvasKit canvas (for export). Disables culling to render everything.
const recorder = new ck.PictureRecorder()
const bounds = ck.LTRBRect(0, 0, 1000, 1000)
const canvas = recorder.beginRecording(bounds)
renderer.renderSceneToCanvas(canvas, graph, pageId)
const picture = recorder.finishRecordingAsPicture()
Font Management
loadFonts()
async loadFonts(): Promise<void>
Loads default font (Inter Regular) and initializes font provider. Call once after creating the renderer.
await renderer.loadFonts()
getFontProvider()
getFontProvider(): TypefaceFontProvider | null
Returns the CanvasKit font provider (for registering custom fonts).
Surface Management
replaceSurface()
replaceSurface(surface: Surface): void
Replaces the WebGL surface (e.g., after canvas resize). Deletes old surface and invalidates picture cache.
const newSurface = ck.MakeWebGLCanvasSurface(canvas)!
renderer.replaceSurface(newSurface)
Picture Caching
Renderer pre-renders nodes with effects/shadows to SkPicture objects for faster repaints.
invalidateScenePicture()
invalidateScenePicture(): void
Invalidates the cached scene picture (forces re-render on next render() call).
invalidateAllPictures()
invalidateAllPictures(): void
Invalidates all cached pictures (scene + per-node).
invalidateNodePicture()
invalidateNodePicture(nodeId: string): void
Invalidates cached picture for a specific node (e.g., after changing effects).
invalidateVectorPath()
invalidateVectorPath(nodeId: string): void
Invalidates cached CanvasKit path for a vector node (call after modifying vectorNetwork).
Hit Testing
hitTestSectionTitle()
hitTestSectionTitle(
graph: SceneGraph,
canvasX: number,
canvasY: number
): SceneNode | null
Tests if canvas coordinates hit a section title pill (for dragging sections).
hitTestComponentLabel()
hitTestComponentLabel(
graph: SceneGraph,
canvasX: number,
canvasY: number
): SceneNode | null
Tests if canvas coordinates hit a component label (for selecting components).
Viewport Culling
Renderer skips rendering nodes outside the visible viewport. Culling logic:
- Leaf nodes: always culled if outside viewport
- Clipped containers: culled if outside viewport
- Unclipped containers: never culled (children may extend beyond bounds)
- Rotated nodes: expand bounds to diagonal for culling
Culling is disabled for renderSceneToCanvas() (export).
Rendering Pipeline
-
Scene layer (world coordinates):
- Apply pan/zoom transform
- Render nodes from page’s children
- Use picture cache if scene hasn’t changed
-
Section titles + component labels (screen coordinates):
- Zoom-independent text rendering
- Ellipsize long names to fit available width
-
UI overlays (screen coordinates):
- Selection bounds with handles
- Rotation handle
- Snap guides
- Marquee selection
- Layout insert indicator
- Remote cursors
- Rulers (horizontal + vertical)
Performance
- Picture caching: Scene is pre-rendered to
SkPicture when sceneVersion is stable. Pan/zoom/hover don’t invalidate cache.
- Per-node caching: Nodes with effects (shadows, blur) are cached individually.
- Viewport culling: Nodes outside the viewport are skipped entirely.
- Shader caching: Gradients and image filters are cached in
Map<string, Shader>.
Example: Full Rendering Setup
import { SceneGraph, SkiaRenderer, getCanvasKit } from '@open-pencil/core'
const canvas = document.getElementById('canvas') as HTMLCanvasElement
const ck = await getCanvasKit()
// Create surface
let surface = ck.MakeWebGLCanvasSurface(canvas)!
const renderer = new SkiaRenderer(ck, surface)
await renderer.loadFonts()
// Setup scene
const graph = new SceneGraph()
const page = graph.getPages()[0]
renderer.pageId = page.id
// Handle resize
const onResize = () => {
const dpr = window.devicePixelRatio
canvas.width = canvas.clientWidth * dpr
canvas.height = canvas.clientHeight * dpr
renderer.dpr = dpr
renderer.viewportWidth = canvas.clientWidth
renderer.viewportHeight = canvas.clientHeight
surface.delete()
surface = ck.MakeWebGLCanvasSurface(canvas)!
renderer.replaceSurface(surface)
}
window.addEventListener('resize', onResize)
onResize()
// Render loop
let sceneVersion = 0
const render = () => {
renderer.render(graph, selectedIds, overlays, sceneVersion)
requestAnimationFrame(render)
}
render()
// On scene mutations
graph.updateNode(nodeId, { width: 200 })
sceneVersion++
Constants
Renderer uses constants from @open-pencil/core/constants:
SELECTION_COLOR, COMPONENT_COLOR, SNAP_COLOR
ROTATION_HANDLE_OFFSET, ROTATION_HANDLE_RADIUS
RULER_SIZE, RULER_BG_COLOR, RULER_TEXT_COLOR
HANDLE_HALF_SIZE (selection handle size)
LABEL_FONT_SIZE, SIZE_FONT_SIZE
SECTION_TITLE_HEIGHT, COMPONENT_LABEL_FONT_SIZE