Back to Tldraw

Scribble

apps/docs/content/sdk-features/scribble.mdx

5.4.010.1 KB
Original Source

The scribble system draws temporary freehand paths for pointer-based interactions. Use scribbles to show visual feedback during tool operations like erasing, laser pointer drawing, or scribble-brush selection. Access the system through Editor#scribbles.

Scribbles exist only in instance state and fade out automatically after the tool operation completes. They're never persisted to the document, though they are broadcast to collaborators through presence so peers see your eraser and laser trails.

How it works

Scribble lifecycle

A scribble's state is one of five values from TLScribble:

StateDescription
StartingThe scribble collects points until it has more than 8. This prevents flickering for very short strokes.
ActiveThe scribble accumulates points as the pointer moves.
CompleteDrawing finished but fading hasn't started. Set with ScribbleManager#complete so the end cap tapers when the pointer lifts.
StoppingThe scribble fades out by progressively removing points from its tail. The manager deletes the scribble once all points clear.
PausedDefined in the schema but not used by the manager.

The ScribbleManager#tick method updates all scribbles on every animation frame, handling state transitions, point management, and fade-out timing.

Fade-out behavior

During fade-out, the scribble shrinks from the tail by removing points at regular intervals. By default (shrink: 0.1) the stroke width also decreases as points are removed; set shrink: 0 to fade at constant width.

The delay property controls how long a scribble stays at full length before shrinking. Stopping a scribble caps any remaining delay at 200ms. Self-consuming scribbles (the default) remove points from the start as you draw, maintaining a constant length.

Using scribbles

The ScribbleManager provides two APIs: a direct API for single self-consuming strokes, and a session API for when several strokes should fade together, as the laser pointer's do.

Direct API

The direct API works well for tools like the eraser that use self-consuming scribbles:

typescript
import { StateNode, TLPointerEventInfo } from '@tldraw/editor'

export class Erasing extends StateNode {
	static override id = 'erasing'

	private scribbleId = ''

	override onEnter(info: TLPointerEventInfo) {
		const scribble = this.editor.scribbles.addScribble({
			color: 'muted-1',
			size: 12,
		})
		this.scribbleId = scribble.id
		this.pushPointToScribble()
	}

	override onExit() {
		this.editor.scribbles.stop(this.scribbleId)
	}

	override onPointerMove() {
		this.pushPointToScribble()
	}

	private pushPointToScribble() {
		const { x, y } = this.editor.inputs.getCurrentPagePoint()
		this.editor.scribbles.addPoint(this.scribbleId, x, y)
	}
}

ScribbleManager#addScribble takes optional configuration and returns a ScribbleItem containing the scribble's ID. ScribbleManager#addPoint ignores points less than one page unit from the previous one; pass an optional z value after the coordinates (default 0.5) for pressure-based width. ScribbleManager#stop moves the scribble to the stopping state, and the manager removes it once all points clear.

The scribble-brush selection in the select tool uses the same API with color: 'selection-stroke', opacity: 0.32, and size: 12.

Session API

Sessions group multiple scribbles together and control how they fade. The laser pointer uses a session so that every stroke from one drawing burst fades together. Simplified from the real LaserTool:

typescript
import { StateNode } from '@tldraw/editor'

export class LaserTool extends StateNode {
	static override id = 'laser'
	static override initial = 'idle'
	static override children() {
		return [Idle, Lasering]
	}

	private sessionId: string | null = null

	getSessionId(): string {
		// Reuse existing session if it's still active
		if (this.sessionId && this.editor.scribbles.isSessionActive(this.sessionId)) {
			return this.sessionId
		}

		// Create a new session
		this.sessionId = this.editor.scribbles.startSession({
			selfConsume: false,
			idleTimeoutMs: this.editor.options.laserDelayMs,
			fadeMode: 'grouped',
			fadeEasing: 'ease-in',
		})

		return this.sessionId
	}

	override onCancel() {
		if (this.sessionId && this.editor.scribbles.isSessionActive(this.sessionId)) {
			this.editor.scribbles.clearSession(this.sessionId)
			this.sessionId = null
		}
	}
}

The idle state adds a scribble to the session with ScribbleManager#addScribbleToSession and hands its id to the lasering state:

typescript
export class Idle extends StateNode {
	static override id = 'idle'

	override onPointerDown() {
		const sessionId = (this.parent as LaserTool).getSessionId()
		const scribble = this.editor.scribbles.addScribbleToSession(sessionId, {
			color: 'laser',
			opacity: 0.7,
			size: 4,
			taper: false,
		})
		this.parent.transition('lasering', { sessionId, scribbleId: scribble.id })
	}
}

The lasering state adds points with ScribbleManager#addPointToSession and keeps the session alive with ScribbleManager#extendSession:

typescript
export class Lasering extends StateNode {
	static override id = 'lasering'

	private scribbleId = ''
	private sessionId = ''

	override onEnter(info: { sessionId: string; scribbleId: string }) {
		this.sessionId = info.sessionId
		this.scribbleId = info.scribbleId
		this.pushPointToScribble()
	}

	override onPointerMove() {
		this.pushPointToScribble()
	}

	private pushPointToScribble() {
		const { x, y } = this.editor.inputs.getCurrentPagePoint()
		this.editor.scribbles.addPointToSession(this.sessionId, this.scribbleId, x, y)
	}

	override onTick() {
		// Reset idle timeout on activity
		this.editor.scribbles.extendSession(this.sessionId)
	}

	override onPointerUp() {
		// Mark complete to apply taper, then let session handle fade
		this.editor.scribbles.complete(this.scribbleId)
		this.parent.transition('idle')
	}
}

Scribble properties

Scribbles support these visual properties:

PropertyDefaultDescription
idauto-generatedUnique identifier for the scribble
color'accent'Canvas UI color: 'accent', 'white', 'black', 'selection-stroke', 'selection-fill', 'laser', 'muted-1'
size20Stroke width in screen pixels
opacity0.8Transparency from 0 to 1
delay0Milliseconds before shrinking starts (for self-consuming scribbles)
shrink0.1Rate at which stroke width decreases during fade-out (0 to 1)
tapertrueWhether the stroke tapers at the ends

All properties have defaults, so you only need to specify what you want to change.

Session options

When using the session API, you can configure how scribbles behave:

PropertyDefaultDescription
idauto-generatedSession identifier
selfConsumetrueWhether scribbles eat their own tail as you draw
idleTimeoutMs0Auto-stop session after this many milliseconds of inactivity (0 disables)
fadeMode'individual'How scribbles fade: 'individual' (each on its own) or 'grouped' (fade together)
fadeEasing'linear' or 'ease-in'Easing for grouped fade. Defaults to 'ease-in' when fadeMode is 'grouped'
fadeDurationMslaserFadeoutMs (500ms)Duration of the fade in milliseconds

When selfConsume is false, points accumulate while the session is active and only fade after the session stops. A session stops when you call ScribbleManager#stopSession, or automatically after idleTimeoutMs of no activity. In grouped fade mode, the manager removes points from all scribbles in the session proportionally over fadeDurationMs; 'ease-in' removes them slowly at first and faster toward the end. ScribbleManager#clearSession removes everything immediately, and ScribbleManager#isSessionActive tells you whether a session is still accepting points.

Customizing scribble rendering

Scribbles are rendered on an HTML Canvas overlay via the ScribbleOverlayUtil from the tldraw package. To customize rendering, extend this class (keeping its static type) and pass it in the overlayUtils prop, which replaces the default util of the same type. See Overlay utils for how overlay utils are registered and replaced.

  • Tools - How tools use state nodes and handle pointer events
  • Overlay utils - The canvas overlay system that renders scribbles