@adecore/drawing
Hand-drawn shapes, arrows, freehand strokes, text and sticky notes as plain JSON, and the code to work with them: hit tests and resizing for an editor, paths for a painter, an SVG export, and a reading of the drawing as lines of text. It needs no DOM, so a backend exports a stored drawing the same way a page draws it.
sh
bun add @adecore/drawingts
import { DEFAULT_PALETTE, readingOrder, toSvg } from '@adecore/drawing';
import { duplicateElementIdIn, migrateDrawing } from '@adecore/drawing/protocol';
const drawing = migrateDrawing(JSON.parse(file));
if (drawing === null || duplicateElementIdIn(drawing.elements) !== null) {
throw new Error('Not a drawing');
}
const svg = toSvg(drawing.elements, { palette: DEFAULT_PALETTE });
const lines = readingOrder(drawing.elements);tsx
import { useMemo, useState } from 'react';
import { readingOrder, toSvg } from '@adecore/drawing';
import type { DrawingRoughness } from '@adecore/drawing/protocol';
import { Segmented } from '@adecore/ui';
import { THEME_EDGE, THEME_PALETTE, THEME_PAPER } from '../shared/canvas-theme.ts';
import { SKETCH } from '../shared/sketch.ts';
type Roughness = 'architect' | 'artist' | 'cartoonist';
const ROUGHNESS: Record<Roughness, DrawingRoughness> = { architect: 0, artist: 1, cartoonist: 2 };
export default function DrawingExportDemo() {
const [roughness, setRoughness] = useState<Roughness>('artist');
const elements = useMemo(() => SKETCH.map((element) => ({ ...element, roughness: ROUGHNESS[roughness] })), [roughness]);
const svg = toSvg(elements, { palette: THEME_PALETTE, paper: THEME_PAPER, edge: THEME_EDGE });
return (
<div className="flex w-full flex-col gap-4">
<Segmented<Roughness>
label="Roughness"
value={roughness}
onValueChange={setRoughness}
options={[
{ id: 'architect', label: 'Architect' },
{ id: 'artist', label: 'Artist' },
{ id: 'cartoonist', label: 'Cartoonist' }
]}
className="self-start"
/>
<div className="[&_svg]:h-auto [&_svg]:w-full" dangerouslySetInnerHTML={{ __html: svg }} />
<ol className="flex flex-col gap-1 font-mono text-code text-text-muted">
{readingOrder(elements).map((line) => (
<li key={line}>{line}</li>
))}
</ol>
</div>
);
}The demo passes one drawing to toSvg and readingOrder. Switching the roughness redraws every shape, and each shape wobbles the same way on every render because it stores its own seed.
What is in it
- Documents: the schemas of a drawing and its seven element kinds, on
@adecore/drawing/protocol. - Geometry: bounds, hit tests, marquee selection and resizing in world coordinates.
- Paths and SVG: the paths of one element for a painter of your own, and
toSvgfor a whole drawing. - Text and reading order: wrapping and measuring text, and
readingOrder, which turns a drawing into lines an agent can read.
Two entry points
| Entry point | Holds | Loads |
|---|---|---|
@adecore/drawing/protocol | Zod schemas, types, migrateDrawing | Zod |
@adecore/drawing | Geometry, paths, SVG, text and reading order | Rough.js and perfect-freehand |
A store that only validates files imports the protocol and never loads the renderer.
What the app does
The package keeps no state and registers no listener. The app owns:
- Storage and conflicts. A document carries a
rev; the app compares it with the stored one on save and writesrev + 1. - Input: gestures, selection, undo, and the transform from screen pixels to world units.
- Theme and fonts. A drawing names palette colors and fonts, and the app says what they are when it paints or exports.
@adecore/diagram builds on this package for graphs that are written rather than drawn.