# Draw on a whiteboard

Use a mounted canvas to draw freehand marks, change a committed mark's style, and remove a mark. Draw a closed outline when you want a freehand shape: it remains ink, not a diagram node.

## When to use this

Use the freehand tool for annotations, sketches, arrows, and outlines that belong on top of a diagram. A committed mark is a vector stroke, not a screenshot. The live draw preview follows the pointer; releasing commits the stroke to the mounted diagram.

[`DiagramInstance`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) is the mounted canvas facade: its `getModel()` reaches the diagram data and its `renderNow()` repaints after a change. [`createDrawTool`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-interaction-functions#createdrawtool) creates a tool for that live canvas, and [`registerTool`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-ext-functions#registertool) connects it to the canvas's pointer-tool registry. The instance satisfies [`WhiteboardHost`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-interaction-interfaces-v-w#whiteboardhost), which the tool factory accepts.

## Mount the canvas and draw

1. Start with an empty diagram and a red pen; draw a line or return to its starting point to make a closed outline.
2. Register the draw tool after the canvas instance exists. Use the toolbar buttons to recolor the first committed mark or remove it.

In the JavaScript, Angular, React, and Vue examples, **Edit first mark** changes the live stroke's style and **Erase first mark** removes it from the live model. Either button does nothing until you draw. Those examples hold the mounted instance in the framework's normal lifecycle state and unregister the tool when that owner unmounts.

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';
import { createDrawTool, registerTool } from '@grafloria/renderer';

const app = document.getElementById('app');
if (!app) throw new Error('Missing #app');

const toolbar = document.createElement('div');
const canvas = document.createElement('div');
canvas.style.height = '400px';
app.append(toolbar, canvas);

const editButton = document.createElement('button');
editButton.textContent = 'Edit first mark';
const eraseButton = document.createElement('button');
eraseButton.textContent = 'Erase first mark';
toolbar.append(editButton, eraseButton);

const instance = render({ nodes: [], edges: [] }, canvas);
const unregisterTool = registerTool(
  createDrawTool(instance, { color: '#e11d48', width: 3, simplifyEpsilon: 0.8 })
);

editButton.addEventListener('click', () => {
  const stroke = instance.getModel().getStrokes()[0];
  if (!stroke) return;
  stroke.setStyle({ color: '#2563eb', width: 5 });
  instance.renderNow();
});

eraseButton.addEventListener('click', () => {
  const stroke = instance.getModel().getStrokes()[0];
  if (!stroke) return;
  instance.getModel().removeStroke(stroke.id);
  instance.renderNow();
});

window.addEventListener('pagehide', () => {
  unregisterTool();
  instance.dispose();
}, { once: true });
```
```ts title="Angular"
import { AfterViewInit, Component, ElementRef, OnDestroy, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { createDrawTool, registerTool, type WhiteboardHost } from '@grafloria/renderer';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div>
      <button type="button" (click)="editFirstMark()">Edit first mark</button>
      <button type="button" (click)="eraseFirstMark()">Erase first mark</button>
    </div>
    <div #host style="display:block; height:400px">
      <grafloria-diagram-canvas style="display:block; height:100%" />
    </div>
  `,
})
export class WhiteboardComponent implements AfterViewInit, OnDestroy {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  readonly hostElement = viewChild.required<ElementRef<HTMLElement>>('host');
  private unregisterTool: (() => void) | undefined;

  ngAfterViewInit(): void {
    const canvas = this.canvas();
    const whiteboardHost: WhiteboardHost = {
      getModel: () => canvas.activeEngine()!.getDiagram()!,
      getEngine: () => canvas.activeEngine() ?? null,
      get viewport() { return canvas.viewportController()!; },
      container: this.hostElement().nativeElement,
      render: () => canvas.scheduleRender(),
    };
    this.unregisterTool = registerTool(
      createDrawTool(whiteboardHost, { color: '#e11d48', width: 3, simplifyEpsilon: 0.8 })
    );
  }

  editFirstMark(): void {
    const canvas = this.canvas();
    const stroke = canvas.activeEngine()?.getDiagram()?.getStrokes()[0];
    if (!stroke) return;
    stroke.setStyle({ color: '#2563eb', width: 5 });
    canvas.scheduleRender();
  }

  eraseFirstMark(): void {
    const canvas = this.canvas();
    const model = canvas.activeEngine()?.getDiagram();
    const stroke = model?.getStrokes()[0];
    if (!model || !stroke) return;
    model.removeStroke(stroke.id);
    canvas.scheduleRender();
  }

  ngOnDestroy(): void {
    this.unregisterTool?.();
  }
}
```
```tsx title="Qwik"
import { $, component$, useSignal } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { createDrawTool, registerTool } from '@grafloria/element';

export default component$(() => {
  const hostRef = useSignal<HTMLDivElement>();

  return (
    <div ref={hostRef} style={{ display: 'block', height: '400px' }}>
      <GrafloriaFlow
        defaultNodes={[]}
        defaultEdges={[]}
        style={{ display: 'block', height: '100%' }}
        onInit$={$((instance: DiagramInstance) => {
          registerTool(createDrawTool(instance, { color: '#e11d48', width: 3, simplifyEpsilon: 0.8 }));
        })}
      />
    </div>
  );
});
```
```tsx title="React"
import { useEffect, useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import { createDrawTool, registerTool } from '@grafloria/renderer';

export function Whiteboard() {
  const instanceRef = useRef<DiagramInstance | null>(null);
  const unregisterRef = useRef<(() => void) | null>(null);

  useEffect(() => () => {
    unregisterRef.current?.();
  }, []);

  const onInit = (instance: DiagramInstance): void => {
    instanceRef.current = instance;
    unregisterRef.current = registerTool(
      createDrawTool(instance, { color: '#e11d48', width: 3, simplifyEpsilon: 0.8 })
    );
  };

  const editFirstMark = (): void => {
    const instance = instanceRef.current;
    const stroke = instance?.getModel().getStrokes()[0];
    if (!instance || !stroke) return;
    stroke.setStyle({ color: '#2563eb', width: 5 });
    instance.renderNow();
  };

  const eraseFirstMark = (): void => {
    const instance = instanceRef.current;
    const stroke = instance?.getModel().getStrokes()[0];
    if (!instance || !stroke) return;
    instance.getModel().removeStroke(stroke.id);
    instance.renderNow();
  };

  return (
    <section>
      <div>
        <button type="button" onClick={editFirstMark}>Edit first mark</button>
        <button type="button" onClick={eraseFirstMark}>Erase first mark</button>
      </div>
      <GrafloriaFlow
        defaultNodes={[]}
        defaultEdges={[]}
        style={{ display: 'block', height: '400px' }}
        onInit={onInit}
      />
    </section>
  );
}
```
```vue title="Vue"
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/vue';
import { createDrawTool, registerTool } from '@grafloria/renderer';

const instance = ref<DiagramInstance | null>(null);
let unregisterTool: (() => void) | undefined;

function onInit(liveInstance: DiagramInstance): void {
  instance.value = liveInstance;
  unregisterTool = registerTool(
    createDrawTool(liveInstance, { color: '#e11d48', width: 3, simplifyEpsilon: 0.8 })
  );
}

function editFirstMark(): void {
  const liveInstance = instance.value;
  const stroke = liveInstance?.getModel().getStrokes()[0];
  if (!liveInstance || !stroke) return;
  stroke.setStyle({ color: '#2563eb', width: 5 });
  liveInstance.renderNow();
}

function eraseFirstMark(): void {
  const liveInstance = instance.value;
  const stroke = liveInstance?.getModel().getStrokes()[0];
  if (!liveInstance || !stroke) return;
  liveInstance.getModel().removeStroke(stroke.id);
  liveInstance.renderNow();
}

onBeforeUnmount(() => unregisterTool?.());
</script>

<template>
  <section>
    <div>
      <button type="button" @click="editFirstMark">Edit first mark</button>
      <button type="button" @click="eraseFirstMark">Erase first mark</button>
    </div>
    <GrafloriaFlow style="display:block; height:400px" @init="onInit" />
  </section>
</template>
```
:::

For Qwik, mount [`GrafloriaFlow`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-qwik#grafloriaflow) and register the draw tool from `onInit$`, which receives the mounted instance. See the [Qwik demo gallery](https://grafloria.com/demos-qwik/) for runnable Qwik examples.

The drawing interaction is the same in every binding: draw with a mouse, pen, or finger, then release to commit a stroke. For a freehand shape, trace a closed path yourself. In the JavaScript, Angular, React, and Vue examples, the first toolbar action recolors and widens the first live stroke; the second removes that whole stroke. Both update the mounted canvas, not a detached model.

## Tune the pen

[`DrawToolOptions`](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/grafloria-renderer-interaction-interfaces-a-t#drawtooloptions) controls the created pen. The defaults below come from the tool implementation; `simplifyEpsilon` is left to the model's tuned default when omitted.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `color` | `string` | `'#1f2933'` | Sets the ink and live preview color. |
| `width` | `number` | `3` | Sets the stroke width and preview width. |
| `opacity` | `number` | Not set | Sets ink opacity; use it for translucent highlighter marks. |
| `simplifyEpsilon` | `number` | Model's tuned default | Sets the Douglas–Peucker tolerance applied when the gesture commits. |
| `label` | `string` | Not set | Gives committed ink an accessible name. |
| `active` | `boolean` | `true` | Set to `false` to create the draw tool inactive. |

## What to know

- `registerTool()` is a registry operation, so keep its returned disposer and call it when the owning view unmounts. Registering another tool with the same id replaces the current one; disposing restores the previous registration.
- These toolbar actions call model methods directly. They update the document and repaint, but they do not create a user-gesture history command. For command-based undo behavior, see [Command history](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history).
- The pen stays active for pointer gestures. Do not leave multiple point-agnostic drawing modes active together.

## See the pointer-driven tools

The freehand demo shows the pen on a blank canvas; drag a line and release to commit crisp vector ink.

Live demo: [Freehand draw](https://grafloria.com/demos/whiteboard/freehand-draw.html) · [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/whiteboard/freehand-draw.html)

The rectangle demo starts ready for a drag that creates a box node, rather than a stroke.

Live demo: [Rectangle tool](https://grafloria.com/demos/whiteboard/rectangle.html) · [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/whiteboard/rectangle.html)

The eraser demo loads with three parallel strokes; its pointer sweep removes whole strokes.

Live demo: [Eraser](https://grafloria.com/demos/whiteboard/eraser.html) · [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/whiteboard/eraser.html)

The stroke-edit demo loads with the draw and edit controls; draw ink, choose edit, and drag a committed mark to translate it.

Live demo: [Stroke edit](https://grafloria.com/demos/whiteboard/stroke-edit.html) · [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e5038/demos/whiteboard/stroke-edit.html)

## Related

- [Command history](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/command-history) — understand how user gestures become undoable commands.
- [Build runnable workflows](https://bench-grafloria-6l.atloria.app/p/bench-grafloria-6l-dqcsEdUbfS/developer/build-runnable-workflows) — mount and configure a usable canvas.
