* fix(canvas): bound image decoding for large Figma documents * test(canvas): update lifecycle fixture for image caches * test(canvas): align image regressions with contribution guides Use native wheel zoom in the browser regression instead of inline store access. Keep export state restoration in the existing raster suite and explicitly format the new image tests omitted by the root format script. * fix(harness): pin patched proxy address dependency Override the transitive proxy-addr dependency with 2.0.8 to fix IPv4-mapped IPv6 trust subnet checks and pass the critical dependency audit (GHSA-jqcg-44mw-7w3h). * build: drop the proxy-addr override master already resolves Master's lockfile has proxy-addr 2.0.8 since the advisory was fixed upstream, so the override this branch added is redundant. * perf(canvas): encode image previews as WebP PNG previews of photos ran to several megabytes each, so a page of them filled the 64 MiB preview cache and evicted itself: page 2 of the #924 reproduction held 48 MB of PNG previews against 7.3 MB as WebP. Browsers that cannot encode WebP hand back PNG. Co-authored-by: sableangle <sableangle@gmail.com> * perf(canvas): decode four image previews at once The browser decodes previews off the main thread, so one at a time left a page of photos filling in for seconds: page 2 of the #924 reproduction took 4.6 s to finish and now takes 2.0 s. Co-authored-by: sableangle <sableangle@gmail.com> * refactor(canvas): decide on image previews once per document size needsImagePreviews replaces useViewportImageRendering, which was not a Vue composable, names its two thresholds, and keeps its answer per document and image count instead of summing every image on each frame. Choosing a preview by zoom carries a lint exception: preview mode draws the scene uncached. Co-authored-by: sableangle <sableangle@gmail.com> * docs: describe the image memory fix in the changelog Co-authored-by: sableangle <sableangle@gmail.com> * fix(canvas): let go of a document's image previews when the document changes Switching to a document that needs no previews left the preview cache holding the previous graph and its encoded images, often hundreds of megabytes, until previews were used again. The renderer now releases them when its document changes, keeping the decoder for the next one. --------- Co-authored-by: sableangle <sableangle@gmail.com> |
||
|---|---|---|
| .. | ||
| example | ||
| src | ||
| tests | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsdown.config.ts | ||
@open-pencil/vue
Headless Vue 3 SDK for building OpenPencil-powered editors.
@open-pencil/vue sits on top of @open-pencil/core and provides:
- Vue editor injection via
provideEditor()/useEditor() - canvas integration via
useCanvas(),useCanvasInput(), anduseTextEdit() - selection, command, panel, variables, and i18n composables
- headless structural primitives like
CanvasRoot,LayerTreeRoot,PageListRoot, andToolbarRoot
The SDK is headless by design: it provides logic and structure, while your app owns styling and product-specific UI.
Install
bun add @open-pencil/vue @open-pencil/core @open-pencil/scene-graph canvaskit-wasm
The current development version requires Vue ^3.5.41 and, when supplying the optional CanvasKit peer, canvaskit-wasm >=0.41.1. See SDK Getting Started for migration guidance; older releases may have different peer requirements.
Quick start
<script setup lang="ts">
import { reactive } from 'vue'
import { createDefaultEditorState, createEditor } from '@open-pencil/core/editor'
import { SceneGraph } from '@open-pencil/scene-graph'
import { CanvasRoot, CanvasSurface, provideEditor } from '@open-pencil/vue'
const graph = new SceneGraph()
const page = graph.getPages()[0]
if (!page) throw new Error('Expected an initial page')
const editor = createEditor({
graph,
state: reactive(createDefaultEditorState(page.id)),
getViewportSize: () => ({ width: 1200, height: 800 }),
})
editor.createShape('RECTANGLE', 100, 100, 200, 150)
editor.zoomToFit()
provideEditor(editor)
</script>
<template>
<div class="h-screen">
<CanvasRoot>
<CanvasSurface class="size-full" />
</CanvasRoot>
</div>
</template>
The fixed viewport size above is illustrative; a resizable shell should return its actual canvas container dimensions from getViewportSize. Pass reactive state for Vue controls, while keeping the Scene Graph itself framework-neutral.
Core concepts
Editor context
Use provideEditor(editor) once near the top of your subtree.
import { provideEditor } from '@open-pencil/vue'
provideEditor(editor)
Read it anywhere below with useEditor().
import { useEditor } from '@open-pencil/vue'
const editor = useEditor()
Canvas wiring
At the composable level, the main canvas APIs are:
useCanvas()useCanvasInput()useTextEdit()PlayIslands
If you want SDK-provided structure, use headless primitives like CanvasRoot and CanvasSurface.
PlayIslands previews a canvas pane: place it over the canvas with the pane's view state, and while the pane's play state is set, each top-level layer holding components with behaviours runs as live Reka UI components in its own shadow root.
Headless primitives
Main structural primitives include:
CanvasRootLayerTreeRootPageListRootPropertyListRootPropertySectionRootSegmentedControlRootToolbarRootColorPickerRootFontPickerRootNumberFieldRoot/NumberFieldInput/NumberFieldValueBindableValueRoot/BindableValueTrigger/BindableValuePickerLayoutControlsRootConstraintsControlRoot
These components coordinate structure and state, but do not impose app styling. NumberField
adds pointer scrubbing, Arrow-key stepping, mixed/bound state attributes, and safe arithmetic
expressions such as +10, *2, 50%, and 12*8+4. BindableValue composes fields with a
generic BindingProvider and supports detach-on-edit, read-only, and edit-variable policies.
Focusing a bound NumberField is non-destructive; the configured policy begins only on the first
value mutation. LayoutControlsRoot exposes axis-oriented sizing actions; editing a Hug or Fill
dimension can switch that axis to Fixed inside the same provider transaction.
ConstraintsControlRoot exposes eligible frame-child constraints, mixed axis values, pin actions,
and undo-batched multi-selection updates. AppearanceControlsRoot
exposes selection-derived independent-corner presentation state so consumers do not need parallel
expansion heuristics. PropertyListRoot is controlled and
editor-agnostic; OpenPencil panels connect it to selection and undo through
useEditorPropertyList(). useColorModel() provides precise scene-color/Reka bridges, reactive
RGB/HSL/HSB/OkHCL channels, extensible format state, and shared slider presentation data.
FillRoot and FillSwatch separate fill behavior and binding-aware previews from popover
composition; ChannelSlider provides accessible scalar OkHCL controls until Reka supports them.
Public API tiers
Core API
These are the main APIs most SDK consumers should start with.
Context and canvas
provideEditor()useEditor()useCanvas()useCanvasInput()useTextEdit()
Selection and commands
useSelectionState()useSelectionCapabilities()useEditorCommands()useMenuModel()
Property panels
usePosition()useLayout()useConstraints()useComponentProperties()useAppearance()useSharedStyleBinding()useColorModel()useMask()useTypography()useExport()useFillControls()useStrokeControls()useEffectsControls()
Variables, navigation, and localization
usePageList()useI18n()
Headless primitives
CanvasRootLayerTreeRootPageListRootPropertyListRootPropertyListItemPropertyListAdd/PropertyListRemove/PropertyListVisibilityPropertySectionRoot/PropertySectionHeader/PropertySectionTitlePropertySectionActions/PropertySectionContent/PropertySectionEmptyActionSegmentedControlRoot/SegmentedControlItemToolbarRootNumberFieldRootNumberFieldInputNumberFieldValueNumberFieldLeadingNumberFieldUnitNumberFieldTrailingNumberFieldMenuBindableValueRootBindableValueTriggerBindableValuePickerFillRoot/FillSwatchChannelSliderRoot/ChannelSliderTrack/ChannelSliderThumb
Advanced API
These exports are intentionally public, but they are lower-level or more specialized.
useNodeProps()useEditorPropertyList()useSceneComputed()useColorBindingProvider()useColorVariableBinding()provideBindingProvider()useBindingProvider()useNumberBindingProvider()useFill()useGradientStops()useFontPicker()useOkHCL()useVariables()usePropScrub()useLayerDrag()useInlineRename()useToolbarState()useNodeFontStatus()useCanvasDrop()extractImageFilesFromClipboard()useViewportKind()toolCursor()
Primitive context helpers and low-level stores
These are mostly useful when extending SDK primitives rather than building from top-level composables.
useCanvasContext()useLayerTree()useToolbar()usePropertyList()useNumberField()localelocaleSettingsetLocale()AVAILABLE_LOCALESLOCALE_LABELS
Example patterns
Minimal provider component
<script setup lang="ts">
import { provideEditor } from '@open-pencil/vue'
import type { Editor } from '@open-pencil/core/editor'
const props = defineProps<{
editor: Editor
}>()
provideEditor(props.editor)
</script>
<template>
<slot />
</template>
Read selection state
import { useSelectionState } from '@open-pencil/vue'
const { hasSelection, selectedCount, selectedNode } = useSelectionState()
Build a menu
import { useMenuModel } from '@open-pencil/vue'
const { appMenu, canvasMenu } = useMenuModel()
Build a page list
<PageListRoot v-slot="{ pages, currentPageId, switchPage }">
<ul>
<li v-for="page in pages" :key="page.id">
<button :data-active="page.id === currentPageId" @click="switchPage(page.id)">
{{ page.name }}
</button>
</li>
</ul>
</PageListRoot>
Documentation
For fuller guides and API docs, see the documentation site:
packages/docs/programmable/sdk/
Example app
Run the included example:
cd packages/vue/example
bun install
bun run dev