Pure TypeScript · WebGPU · libmypaint
Layers, libmypaint brushes, selections, transforms, filters and text, with undo that also works when several people paint on the same canvas. Bring your own UI.
Load .myb files. Pressure, tilt, barrel rotation, smudge and spectral pigment mixing.
Paint, folder, adjustment and text layers with 26 blend modes and pass-through.
Marquee, lasso, polygon, magic wand, quick mask. Affine, perspective and mesh warps.
Destructive or as adjustment layers, from one deterministic integer kernel.
Per-range styles, point and area text, vertical writing. Editable after affine transforms.
A sequencer orders operations; every peer recomputes strokes from their samples.
Save as CBOR, export PNG, JPEG and layered PSD with text.
Synchronous snapshots that keep identity. React, Vue, Svelte and Solid fit in a few lines.
Run the engine in a worker, open a document, attach a canvas. Input, tools, history and rendering are handled for you.
import { ops } from 'peinten';
import { connectRuntime } from 'peinten/worker';
import type { Tools } from './tools';
const runtime = await connectRuntime<Tools>(new Worker(new URL('./engine.worker.ts', import.meta.url), { type: 'module' }));
const doc = await runtime.createDocument({ width: 1600, height: 1200 });
await runtime.createView({ canvas, document: doc });
const layer = ops.addLayer({ name: 'Ink' });
await doc.dispatch(layer);
await doc.setActiveNode(layer.nodeId);
await runtime.setTool('brush'); // the brush now paints on "Ink"State comes from synchronous snapshots, so a hook is a few lines of useSyncExternalStore
(see how):
import type { PaintDocument } from 'peinten';
import { useSnapshot } from './hooks/use-snapshot'; // the hook from "State for the UI"
export function UndoButton({ doc }: { doc: PaintDocument }) {
const canUndo = useSnapshot(doc, (s) => s.history.canUndo);
return <button disabled={!canUndo} onClick={() => doc.undo()}>Undo</button>;
}