ModernCanvasEditor
An infinite canvas editor framework (Vue 3 + TypeScript) with real-time collaboration, a timeline, components and design tokens — built on WebGL rendering. Bring your own UI. ESM only.
Documentation · Try in CodeSandbox
Features
Canvas & editing
- Infinite canvas with pan / zoom, rulers, scrollbars, pixel grid and checkerboard
- Smart guides & snapping, alignment / distribution, z-order arrange, tidy-up
- Multi-select & marquee, transform (move / resize / rotate / flip), foreground crop
- Frames (artboards) with auto-nesting, and Flex auto-layout (drag-to-reorder)
Content
- Shapes, pen / freehand paths, lines & arrows
- Rich text (fragment styling, custom fonts, format painter, auto-fit strategies)
- Images (insert / upload / crop), video, tables (
@mce/table) and charts (@mce/chart)
Motion
- Timeline with frame-based playback
- Keyframe animation with reusable easing (presets + custom cubic-bezier)
- Export to GIF, MP4 and Lottie
Collaboration & history
- CRDT document model (Yjs) in the core — undo / redo and offline persistence (IndexedDB) build on it
- Real-time multi-user editing + awareness (remote cursors / selection / avatars) via
@mce/collaboration(WebSocket / pluggable transport) - Comments anchored to elements (pins follow on move / scale / rotate, threads with replies & resolve) via
@mce/comments
Design systems
- Components / symbols / instances with per-instance overrides and master propagation
- Design tokens / variables (collections + modes) for theming and responsive values
AI (@mce/ai)
- A typed AI canvas action schema — drive edits from an LLM over the existing command & undo stack (model wiring left to the consumer)
Workflow (@mce/workflow)
- A node-graph editing mode: connectable nodes with input / output ports and curved connections
Extensible
- ~40 built-in plugins; a plugin can contribute commands, tools, hotkeys, exporters, loaders, components and events
- Element types & modes are decoupled via extension points (selection redirect, resize override, enter handler, editing state, toolbelt item, icon, mode, statusbar item) — see
mixins/extensions.ts - Unified command system, hotkeys, and i18n
Import & export
- Export:
PNG·JPEG·WebP·SVG·PDF·GIF·MP4·Lottie·PPTX/XLSX/DOCX·JSON - Import:
PPTX/XLSX/DOCX·PSD·SVG·HTML· images ·JSON
These ship as optional plugins; their heavy encoders / parsers are lazy-loaded on first use:
| Package | Adds |
|---|---|
@mce/gif |
GIF export |
@mce/mp4 |
MP4 export |
@mce/pdf |
PDF export |
@mce/svg |
SVG import & export |
@mce/openxml |
PPTX / XLSX / DOCX import & export |
@mce/psd |
PSD import (Photoshop layers → elements) |
@mce/html |
HTML import |
(PNG / JPEG / WebP / JSON / Lottie export are built in.)
Feature plugins
Specialized features also ship as optional packages, registered the same way (plugins: [...]):
| Package | Adds |
|---|---|
@mce/table |
Table element + in-canvas table editor |
@mce/chart |
Chart elements (bar / line / pie / …) |
@mce/ai |
Typed AI canvas action schema (applyAi) |
@mce/workflow |
Node-graph editing mode |
@mce/collaboration |
Real-time collaboration: transport providers + presence (cursors / selection / avatars) |
@mce/comments |
Comments: comment tool + pins anchored to elements + threads (stored on element.comments) |
Install
npm i mceUsage
<script setup lang="ts">
import { Editor, EditorLayout, EditorLayoutItem } from 'mce'
import 'mce/styles'
// Plugins that ship UI components export their own stylesheet — import it or
// their editor styles (chart / table / comments / presence / workflow) are missing.
import '@mce/chart/styles'
import '@mce/collaboration/styles'
import '@mce/comments/styles'
import '@mce/table/styles'
import '@mce/workflow/styles'
import ai from '@mce/ai'
import chart from '@mce/chart'
import collaboration from '@mce/collaboration'
import comments from '@mce/comments'
import gif from '@mce/gif'
import mp4 from '@mce/mp4'
import openxml from '@mce/openxml'
import pdf from '@mce/pdf'
import svg from '@mce/svg'
import table from '@mce/table'
import workflow from '@mce/workflow'
const editor = new Editor({
plugins: [
// export / import formats
gif(),
mp4(),
svg(),
pdf(),
openxml(),
// feature plugins (all optional)
table(),
chart(),
ai(),
workflow(),
collaboration(), // registers the collaboration + presence plugins
comments(),
],
// @mce/gif bundles its encoding worker by default. To self-host it
// (e.g. under a strict CSP), pass `gifWorkerUrl` explicitly:
// import gifWorkerUrl from 'modern-gif/worker?url'
// ...new Editor({ gifWorkerUrl })
locale: { locale: 'en' },
viewport: {
camera: { enabled: true },
zoom: { strategy: 'contain' },
screenPadding: { left: 0, top: 0, right: 0, bottom: 0 },
},
canvas: {
checkerboard: { enabled: true, style: 'grid' },
pixelGrid: { enabled: true },
frame: { outline: false },
watermark: {
url: '/example.jpg',
width: 100,
alpha: 0.05,
rotation: 0.5236,
},
},
ui: {
ruler: { visible: true },
scrollbar: { visible: true },
statusbar: { visible: true },
toolbelt: { visible: true },
madeWith: { visible: false },
},
typography: {
strategy: 'autoHeight',
defaultFont: {
family: 'SourceHanSansCN-Normal',
src: '/fonts/SourceHanSansCN-Normal.woff',
},
},
uploader: async (blob) => URL.createObjectURL(blob),
customContextMenu: (menu) => menu,
doc: {
children: [
{ foreground: '/example.png', style: { rotate: 60, left: 200, top: 10, width: 50, height: 50 } },
{ text: 'test', style: { rotate: 40, left: 100, top: 100, width: 60, height: 40, fontSize: 20, color: '#FF00FF' } },
{
style: { left: 200, top: 100, width: 100, height: 100, fontSize: 22 },
text: [
{
letterSpacing: 3,
fragments: [
{ content: 'He', color: '#00FF00', fontSize: 12 },
{ content: 'llo', color: '#000000' },
],
},
{ content: ', ', color: '#FF0000' },
{ content: 'World!', color: '#0000FF' },
],
},
],
},
})
editor.on('docSet', () => {
editor.load('http://localhost:5173/example.jpg').then((el) => {
editor.addElement(el, {
position: { x: 500, y: 100 },
})
})
})
</script>
<template>
<div style="width: 100vw; height: 100vh">
<EditorLayout :editor="editor">
<template #selection />
<template #floatbar />
<template #drawboard />
<EditorLayoutItem position="top" :size="56" />
<EditorLayoutItem position="left" :size="380" />
<EditorLayoutItem position="right" :size="260" />
</EditorLayout>
</div>
</template>Slot sub component — read editor state via useEditor():
<script setup lang="ts">
import { useEditor } from 'mce'
const { selection } = useEditor()
</script>
<template>
<div>
{{ selection }}
</div>
</template>Commands
Everything the editor does is a command — call editor.exec(name, ...args). A few examples:
// Arrange & layout
editor.exec('alignHorizontalCenter')
editor.exec('distributeHorizontalSpacing')
editor.exec('tidyUp')
// Design tokens / variables
const collection = editor.exec('createVariableCollection', 'Theme', 'Light')
const dark = editor.exec('createVariableMode', collection, 'Dark')
const brand = editor.exec('createVariable', collection, { name: 'brand', type: 'color', value: '#ff0000' })
editor.exec('setVariableValue', brand, dark, '#0000ff')
editor.exec('bindVariable', 'fill.color', brand) // bind selected element's fill
editor.exec('setActiveVariableMode', collection, dark) // theme switch → canvas recolors
// Components / instances
const component = editor.exec('createComponent') // from selection
editor.exec('createInstance', component, { position: { x: 200, y: 200 } })
// Keyframe animation → Lottie
editor.exec('addAnimationKeyframe', 0, { left: 0, opacity: 0 })
editor.exec('addAnimationKeyframe', 1, { left: 300, opacity: 1 })
const lottie = editor.exec('exportLottie')
// AI canvas actions (validated, applied in one undo step) — needs @mce/ai
editor.exec('applyAi', [
{ type: 'createText', text: 'Hello', x: 40, y: 40 },
{ type: 'align', direction: 'left' },
])AI
@mce/ai ships a typed action layer, not a model. It gives you a schema to
put in your prompt and a safe applyAi that validates / sanitizes a batch
of actions and applies them in a single undo step — wiring the LLM call is up to you.
1. Register the plugin
import ai from '@mce/ai'
new Editor({ plugins: [ai()] })2. Build the prompt — getAiPrompt assembles schema + node ids + request for you
const prompt = editor.exec('getAiPrompt', userInput)
// Already includes the action schema and every existing node id (so the model can
// reference current elements). Need the raw schema instead? editor.exec('getAiSchema').3. Call your own model, then apply the returned actions
// ← your LLM / SDK; @mce/ai is model-agnostic
const text = await callYourLLM(prompt)
const actions = JSON.parse(text) // e.g. [{ type: 'createText', text: 'Hi', x: 40, y: 40 }]
const { created, errors } = editor.exec('applyAi', actions)
// created: ids of newly created elements
// errors: rejected actions + reasons (invalid fields / unknown node ids) — skipped, not applied- Model-agnostic — any LLM / SDK works as long as it emits schema-conforming JSON.
- Safe — invalid actions (bad fields, unknown ids) are rejected into
errors, never written to the document. - One undo step — the whole batch is a single undo entry.
- Node ids included —
getAiPromptembeds all current node ids, so actions referencing existing elements (setStyle/move/delete/select/duplicate/align(ids)) validate; building the prompt yourself means adding them manually.
Collaboration
The CRDT document model (Yjs) lives in the core — undo / redo and offline
persistence (IndexedDB) build on it. The network transport and presence
(awareness) layer is the optional @mce/collaboration package.
1. Register the plugin
import collaboration from '@mce/collaboration'
const editor = new Editor({
plugins: [
collaboration(), // registers the collaboration + presence plugins
],
})2. Identify the local user (presence)
editor.presence.setUser({
id: 'u-1', // optional, for dedupe / avatars
name: 'Alice',
color: '#E64980',
avatar: 'https://…', // optional
})3. Connect to a room
// Built-in WebSocket transport (y-websocket compatible server)
editor.collaboration.connect({
url: 'wss://your-server',
room: 'doc-1', // defaults to the current document id
})
// …or a custom / pluggable transport (WebRTC, BroadcastChannel, …)
import { AbstractProvider } from '@mce/collaboration'
editor.collaboration.connect({
provider: doc => new MyProvider(doc), // doc is the document's YDoc
})
editor.collaboration.disconnect() // end the sessionYou can also auto-connect on startup via the editor option:
new Editor({ collaboration: { url: 'wss://your-server', room: 'doc-1' } })4. Reactive status & remote peers
editor.collaboration.connected // Ref<boolean> — transport connected
editor.collaboration.synced // Ref<boolean> — first full sync done
editor.collaboration.active // Ref<boolean> — a session is active
editor.presence.peers // Ref<Peer[]> — remote users (cursor / selection / user)
editor.presence.localUser // Ref<PresenceUser>Remote cursors, selection boxes and a connection/avatars status-bar item render
automatically once a session is active. Document switching rebuilds the provider
on the new document's YDoc; the transport is bound per-document.
Comments (
@mce/comments) live onelement.commentsand are part of the document model, so they sync over the same session automatically.
Packages
Every package ships as ESM and registers the same way (new Editor({ plugins: [pkg()] })).
The default export of each @mce/* package is its plugin function; commands it adds are
called via editor.exec(name, …) rather than imported.
| Package | Description | Key exports |
|---|---|---|
mce |
Headless infinite-canvas editor core (WebGL; export to image / video / PPT). | Editor, EditorLayout, EditorLayoutItem, EditorLayers, createShapeElement / createTextElement / … factories, useEditor |
@mce/ai |
LLM-driven, typed canvas actions (createText / createShape / setStyle / move / select / delete / duplicate / align) applied in one undo step with automatic validation. |
plugin (default); validateAiActions, AI_ACTION_SCHEMA (commands: applyAi, getAiSchema, getAiPrompt) |
@mce/bigesj |
Bigesj design-doc integration: font preloading, clipboard paste detection, and PPTX / XLSX / DOCX loading. | plugin(options) (default); useFonts, bigeLoader, bidTidLoader, clipboardLoader |
@mce/chart |
Bar / line / pie chart elements with a built-in data editor and toolbelt entry. | plugin (default); createChartElement(type, options) |
@mce/collaboration |
Real-time multi-user editing (Yjs CRDT) over a pluggable provider (built-in WebSocket, y-websocket compatible; swap for WebRTC / BroadcastChannel) plus presence (remote cursors / selection / avatars). | plugin (default, registers collaboration + presence); collaborationPlugin, presencePlugin, AbstractProvider, WebsocketProvider |
@mce/comments |
Anchored comments: pins anchored to elements that follow on move / scale / rotate, with thread replies / resolve / reopen / delete. | plugin (default); useComments, createCommentsStore |
@mce/gaoding |
Gaoding design-doc clipboard-paste support. | plugin (default); clipboardLoader |
@mce/gif |
GIF export (frame-by-frame render from timeline keyframes; modern-gif lazy-loaded). |
plugin (default) |
@mce/html |
HTML file / MIME import — DOM converted into canvas elements. | plugin (default) |
@mce/mp4 |
MP4 export (adaptive bitrate, 720p–2160p, 30fps; modern-mp4 lazy-loaded). |
plugin (default) |
@mce/openxml |
PPTX / XLSX / DOCX two-way import & export with smart layer & font mapping (modern-openxml). |
plugin (default) |
@mce/pdf |
PDF export with page metadata (size / margins; modern-pdf lazy-loaded). |
plugin (default) |
@mce/psd |
PSD import — Photoshop layers expanded into elements, layer canvases auto-uploaded as image assets. | plugin (default); psdToFrame |
@mce/svg |
SVG import & export (Path2D path sets + viewBox, multi-MIME copy). | plugin (default) |
@mce/table |
Table element + in-canvas editor (add / remove rows & columns, merge / split cells, style editing, zoom-aware grid, toolbelt entry). | plugin (default); createTableElement(rows, cols, options) |
@mce/workflow |
Node-graph editing mode (connectable nodes, templated node types, preset text / image / video generation nodes). | plugin (default); getWorkflowPorts, toConnectionPoints, INPUT_PORT, OUTPUT_PORT (commands: addWorkflowNode, addWorkflowConnection) |
Architecture
packages/
mce/ # core editor library (npm: mce)
gif/ # GIF export (@mce/gif)
mp4/ # MP4 export (@mce/mp4)
pdf/ # PDF export (@mce/pdf)
svg/ # SVG import & export (@mce/svg)
openxml/ # PPTX/XLSX/DOCX import & export (@mce/openxml)
psd/ # PSD import (@mce/psd)
html/ # HTML import (@mce/html)
table/ # table element + editor (@mce/table)
chart/ # chart elements (@mce/chart)
ai/ # AI canvas actions (@mce/ai)
workflow/ # node-graph mode (@mce/workflow)
collaboration/ # real-time collaboration (@mce/collaboration)
comments/ # comments (@mce/comments)
bigesj/ # Bigesj design-doc integration (@mce/bigesj)
gaoding/ # Gaoding clipboard paste (@mce/gaoding)
playground/ # demo & test app
The Editor is composed from layered mixins and a plugin system. Rendering is powered by
modern-canvas (WebGL), with text / fonts /
document model from modern-text, modern-font and modern-idoc.
The core stays lean: element types and editing modes are decoupled through extension points
(mixins/extensions.ts), so feature packages register their behavior instead of the core
hard-coding it. The CRDT document model (yjs + y-protocols) lives in the core; the
real-time transport and presence layer is the optional @mce/collaboration package.
Development
pnpm dev # start the playground
pnpm build # build core + all plugins
pnpm test # run tests
pnpm -F mce typecheck
pnpm lint