@dadaki/editor
v1.0.0-beta.3
Published
Dadaki — a high-performance, embeddable in-browser vector graphics editor (CanvasKit + Rust/WASM).
Maintainers
Readme
@dadaki/editor
A high-performance, embeddable in-browser vector graphics editor. The rendering + geometry core is compiled from Rust to WebAssembly and drawn with CanvasKit (Skia); the editor UI (tools, layers, properties, rulers, multi-document tabs) is TypeScript.
It ships as a single embeddable component: give it a DOM container and a CanvasKit instance, and it builds the whole editor inside that container.
Install
npm install @dadaki/editor [email protected]canvaskit-wasm is a peer dependency: the host loads CanvasKit and passes
the instance in, so there is exactly one copy of Skia on the page.
From inside this repository, use the workspace instead:
// package.json
{
"dependencies": {
"@dadaki/editor": "workspace:*" // or "link:../vector-editor/packages/editor"
}
}What the package ships, and what your bundler needs
The package is distributed as TypeScript source — exports points at
src/index.ts — together with the compiled wasm engine in engine/pkg/. Your
build compiles it along with your own code, which is what lets you follow a
stack trace into the editor and what keeps the wasm a single artifact rather
than one copy per consumer.
That means the package expects a Vite-class bundler:
src/index.tsimports the editor chrome as./chrome.html?raw, so the build must understand the?rawsuffix (Vite does; other bundlers need a raw loader).- It must resolve
.wasmas an asset —engine/pkg/engine.jsloadsengine_bg.wasmrelative to its own module URL. - If you typecheck the source, add
"vite/client"tocompilerOptions.types, which supplies the?rawmodule declaration.
Exclude the package from dependency pre-bundling. Vite's dev-mode optimizer
pre-bundles anything under node_modules without running the plugins the
source relies on, so vite dev fails on the ?raw import unless you opt out.
vite build does not go through the optimizer and works either way — which
means skipping this step gives you a project that builds but will not start:
// vite.config.ts
export default defineConfig({
optimizeDeps: { exclude: ['@dadaki/editor'] },
});Usage
import { createEditor } from '@dadaki/editor';
import '@dadaki/editor/style.css';
// The host loads CanvasKit (via the canvaskit.js <script> or the npm package)
// and passes the instance in. Optionally load the `lucide` global for icons.
const canvasKit = await CanvasKitInit({ locateFile: (f) => `/${f}` });
const editor = await createEditor(document.getElementById('app')!, {
canvasKit,
// optional: receive analytics events
analyticsSink: (name, props) => console.log('event', name, props),
});
// The handle exposes the pieces a host legitimately needs:
editor.documentManager; // open/create/rename/close documents
editor.activeDocument(); // currently active document
editor.destroy(); // tear down + clear the containercreateEditor(container, options)
| Option | Type | Description |
| --------------- | --------------- | ------------------------------------------------------- |
| canvasKit | CanvasKit | Required. A loaded CanvasKit instance. |
| analyticsSink | AnalyticsSink | Optional. Receives (eventName, props) for every event.|
Returns an EditorHandle with scene, ui, input, renderer,
documentManager, fileService, activeDocument(), stress(), and
destroy().
Host contract
- The host loads CanvasKit and passes the instance to
createEditor— the library never fetches it. (canvaskit.js+canvaskit.wasmare served by the host; see the@dadaki/appdemo shell.) - The host may load the
lucideicon global (<script src=".../lucide">). Icons are optional chrome; the editor works without it. - The library owns everything inside
container. It never imports an analytics or backend SDK, reads environment variables, or reaches into host-page structure. - Documents persist locally (IndexedDB) by default — there is no backend
dependency. Cloud sync is layered on top by a host app (see
@dadaki/cloud). - A host that puts more than one session in the same document must assign
each a distinct
siteId(see below). Single-user hosts can ignore it.
Object identity (siteId)
Node ids are partitioned by site:
id = (siteId << 22) | counter siteId 0…1023, counter 0…4_194_303A "site" is one editing session, not one user and not one account — two tabs are two sites. Give concurrent sessions different site ids and they can create objects at the same time without ever producing the same id, which is the prerequisite for merging their edits at all.
const editor = await createEditor(el, { canvasKit, siteId: 3 });
editor.setSiteId(7); // if the host learns its site later (e.g. a presence handshake)Three properties worth knowing, each covered by tests in
engine/src/lib.rs (mod identity_tests):
siteId: 0is the legacy numbering.make_id(0, n) === n, so documents written before sites existed are bit-identical and keep allocating where they left off. This is why the default is 0 and why single-user hosts see no change.- Site ids are reusable. On load, an engine resumes its own site's counter past the highest id that site already has in the document, so a later session reusing site 3 cannot reissue ids the earlier one created. Uniqueness is only required among sessions that overlap in time — which is what keeps 10 bits enough.
- Undo never recycles an id. Undo rewinds the serialized counter (so a snapshot round-trip is byte-identical) but allocation is floored by a session watermark. An id retired by undo is not handed to a different object — a peer may already know that id, and reissuing it would give two distinct objects one identity.
Changing siteId mid-session is safe: existing objects keep their ids, and the
counter for the new site resumes from what the document already contains.
Limitations (v1)
The injected chrome uses stable element ids, so one editor instance per document is supported. Full container-scoped, multi-instance isolation is a planned hardening step.
License
MIT © Dadaki
