Skip to content
Inputs

Drawing Board

An infinite canvas you can actually draw on: pen, shapes, arrows, text and sticky notes, with pan, zoom, selection, undo and export.

Walkthrough

1. Basic

Pick a tool and draw. Scroll to pan, ⌘-scroll or pinch to zoom, space to drag the canvas, ⌘Z to undo. The board is infinite in every direction.

Stroke
Weight
Stroke style
Opacity
<script lang="ts">	import { DrawingBoard, type DrawingElement } from 'omaris';​	let elements = $state<DrawingElement[]>([]);</script>​<DrawingBoard bind:elements height={460} class="w-full" />

2. From data

A board is a document, not a picture: this diagram is built from an array and then edited by hand like anything else. bind:elements is the whole state of the drawing, so saving it is JSON.stringify.

<script lang="ts">	import {		DrawingBoard,		createDrawingElement,		defaultDrawingStyle,		measureDrawingText,		type DrawingElement,		type DrawingText	} from 'omaris';​	const stage = (x: number, y: number, label: string, color: string): DrawingElement[] => [		createDrawingElement(			'rect',			{ ...defaultDrawingStyle, color, background: color, fillStyle: 'soft' },			{ x, y, width: 170, height: 72 }		),		measureDrawingText({			...createDrawingElement(				'text',				{ ...defaultDrawingStyle, color, fontSize: 18 },				{					x: x + 22,					y: y + 26				}			),			text: label		} as DrawingText)	];​	const arrow = (x: number, y: number, dx: number, dy: number): DrawingElement =>		({			...createDrawingElement('arrow', defaultDrawingStyle, {				x,				y,				width: Math.abs(dx),				height: Math.abs(dy)			}),			points: [				{ x: 0, y: 0 },				{ x: dx, y: dy }			]		}) as DrawingElement;​	let elements = $state<DrawingElement[]>([		...stage(60, 60, 'Draft', 'var(--ink-blue)'),		arrow(240, 96, 80, 0),		...stage(330, 60, 'In review', 'var(--ink-orange)'),		arrow(415, 132, 0, 70),		...stage(330, 210, 'Shipped', 'var(--ink-green)')	]);</script>​<DrawingBoard bind:elements tool="select" grid="lines" height={460} class="w-full" />

3. Annotating

Marking something up: a picture on the canvas, four tools, snapping off, and the strip at the foot so it does not cover the thing being annotated. Drop an image on the board, or paste one, to add another.

Stroke
Weight
Stroke style
Opacity
<script lang="ts">	import {		DrawingBoard,		createDrawingElement,		defaultDrawingStyle,		type DrawingElement,		type DrawingImage	} from 'omaris';​	/** A drawn screenshot, so the demo needs no network. */	const SHOT = `data:image/svg+xml;utf8,${encodeURIComponent(`		<svg xmlns="http://www.w3.org/2000/svg" width="720" height="440">			<rect width="720" height="440" fill="#f4f5f7"/>			<rect width="720" height="56" fill="#ffffff"/>			<rect x="24" y="20" width="120" height="16" rx="8" fill="#c9ccd4"/>			<rect x="24" y="88" width="300" height="120" rx="12" fill="#ffffff"/>			<rect x="352" y="88" width="344" height="120" rx="12" fill="#ffffff"/>			<rect x="24" y="232" width="672" height="184" rx="12" fill="#ffffff"/>			<rect x="48" y="120" width="180" height="14" rx="7" fill="#dfe1e6"/>			<rect x="48" y="150" width="120" height="30" rx="8" fill="#4b7bec"/>			<rect x="376" y="120" width="240" height="14" rx="7" fill="#dfe1e6"/>			<rect x="376" y="150" width="200" height="14" rx="7" fill="#eceef1"/>		</svg>`)}`;​	let elements = $state<DrawingElement[]>([		{			...createDrawingElement(				'image',				{ ...defaultDrawingStyle, strokeWidth: 0 },				{ x: 0, y: 0, width: 720, height: 440 }			),			src: SHOT,			alt: 'A dashboard screenshot'		} as DrawingImage	]);</script>​<DrawingBoard	bind:elements	tool="arrow"	ink={{		...defaultDrawingStyle,		color: 'var(--ink-red)',		background: 'var(--ink-yellow)',		strokeWidth: 4	}}	tools={['select', 'hand', 'arrow', 'rect', 'highlighter', 'text', 'note', 'image']}	toolbarPosition="bottom"	grid="none"	camera={{ x: -40, y: -20, zoom: 0.7 }}	height={480}	class="w-full"/>

4. Exporting

bind:this is the rest of the component: undo, fit, load, toSVG, toBlob, toDataURL. The export replays the scene at whatever size you ask for, with the theme's own colours baked in — draw something and take a picture of it.

Stroke
Weight
Stroke style
Opacity
0 elements
<script lang="ts">	import { Button, DrawingBoard, type DrawingElement } from 'omaris';​	let board = $state<DrawingBoard | null>(null);	let elements = $state<DrawingElement[]>([]);	let png = $state<string | null>(null);​	async function shoot() {		png = (await board?.toDataURL({ scale: 2, padding: 16 })) ?? null;	}</script>​<div class="flex w-full flex-col gap-3">	<DrawingBoard bind:this={board} bind:elements height={380} class="w-full" />​	<div class="flex flex-wrap items-center gap-2">		<Button size="sm" onclick={shoot} disabled={!elements.length}>Export PNG</Button>		<Button size="sm" variant="tonal" onclick={() => board?.zoomToFit()}>Fit</Button>		<Button size="sm" variant="text" onclick={() => board?.clear()}>Clear</Button>		<span class="text-label-md text-muted-foreground">{elements.length} elements</span>	</div>​	{#if png}		<img			src={png}			alt="The board, exported"			class="max-w-sm rounded-shape-md border border-border"		/>	{/if}</div>

5. On a phone

A phone-width board. Below about 30rem the chrome folds into one bar that scrolls sideways — palette, undo, redo, the tools, zoom, clear — and the paint panel becomes a sheet the palette button opens, so the drawing keeps the board. Two fingers pan and zoom, and a double-tap opens text.

Stroke
Weight
Stroke style
Opacity
<script lang="ts">	import { DrawingBoard, type DrawingElement } from 'omaris';​	let elements = $state<DrawingElement[]>([]);</script>​<div class="w-full max-w-[22.5rem]">	<DrawingBoard bind:elements height={460} /></div>

6. Overridden

The overridden case: a narrow container, a replaced height, three tools, no panels of its own, and chrome of your own over the canvas through children. Every part is reachable through classes.

<script lang="ts">	import { DrawingBoard, type DrawingElement, type DrawingTool } from 'omaris';​	let elements = $state<DrawingElement[]>([]);	let tool = $state<DrawingTool>('pen');	let board = $state<DrawingBoard | null>(null);​	const picks: { tool: DrawingTool; label: string }[] = [		{ tool: 'pen', label: 'Draw' },		{ tool: 'eraser', label: 'Erase' },		{ tool: 'select', label: 'Move' }	];</script>​<div class="w-full max-w-sm">	<DrawingBoard		bind:this={board}		bind:elements		bind:tool		height="16rem"		grid="none"		toolbar={false}		stylePanel={false}		zoomControls={false}		historyControls={false}		ink={{			color: 'var(--ink-violet)',			background: 'transparent',			strokeWidth: 4,			strokeStyle: 'solid',			fillStyle: 'soft',			opacity: 1,			fontSize: 20,			fontFamily: 'sans'		}}		class="rounded-shape-2xl border-2 border-primary/25"		classes={{ canvas: 'bg-surface-container-low' }}	>		<div			class="pointer-events-auto absolute inset-x-3 bottom-3 flex items-center justify-between				gap-1 rounded-full bg-surface-container/95 p-1 shadow-2 backdrop-blur-sm"		>			{#each picks as pick (pick.tool)}				<button					type="button"					class="flex-1 cursor-pointer rounded-full px-3 py-1.5 text-label-md text-muted-foreground						transition-[background-color,color] duration-150 ease-standard hover:text-foreground						aria-pressed:bg-primary aria-pressed:text-primary-foreground motion-reduce:transition-none"					aria-pressed={tool === pick.tool}					onclick={() => (tool = pick.tool)}				>					{pick.label}				</button>			{/each}			<button				type="button"				class="cursor-pointer rounded-full px-3 py-1.5 text-label-md text-muted-foreground					transition-colors duration-150 ease-standard hover:text-destructive					motion-reduce:transition-none"				onclick={() => board?.clear()}			>				Clear			</button>		</div>	</DrawingBoard></div>

7. Ai

The optional AI. ai takes the same generate as AI Image, and a sparkle joins the strip: select part of the drawing (or nothing, for all of it), say what it should become, and the picture lands on the board over what it came from. Leave ai off and the board is exactly as before.

Stroke
Weight
Stroke style
Opacity
<script lang="ts">	import { DrawingBoard, mockGenerate } from 'omaris';</script>​<DrawingBoard	height={420}	ai={{ generate: mockGenerate({ delay: [1200, 2200] }) }}	class="w-full"/>