Whiteboard Features
Vue Flow is designed for building node-based UIs like workflow editors, flowcharts, and diagrams. It isn’t a whiteboard library, but you might still want common whiteboard features to annotate or sketch alongside your nodes and edges. These examples show how to add drawing capabilities to your application.
Examples
✏️ Freehand draw (Pro)
Draw smooth curves on your Vue Flow pane, useful for annotations or sketching around existing nodes.
Features:
- Mouse/touch drawing
- Adjustable brush size and color
- Converts drawn paths into custom nodes
Common uses:
- Annotating flowcharts
- Adding notes to diagrams
- Sketching ideas around nodes
Freehand draw is available as a Vue Flow Pro example.
🎯 Lasso selection
Select multiple elements by drawing a freeform selection area, with an option to include partially selected elements.
Features:
- Freeform selection shapes
- Partial selection of elements
Common uses:
- Selecting nodes and annotations together
- Complex selections in mixed content
import getStroke from 'perfect-freehand';
export const pathOptions = {
size: 7,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
easing: (t: number) => t,
start: {
taper: 0,
easing: (t: number) => t,
cap: true,
},
end: {
taper: 0.1,
easing: (t: number) => t,
cap: true,
},
};
export function getSvgPathFromStroke(stroke: number[][]) {
if (!stroke.length) {
return '';
}
const d = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length];
acc.push(x0, y0, ',', (x0 + x1) / 2, (y0 + y1) / 2);
return acc;
},
['M', ...stroke[0], 'Q'] as (string | number)[],
);
d.push('Z');
return d.join(' ');
}
export function pointsToPath(points: [number, number, number][], zoom = 1) {
const stroke = getStroke(points, {
...pathOptions,
size: pathOptions.size * zoom,
});
return getSvgPathFromStroke(stroke);
}🧹 Eraser
Remove items by “erasing” over them. Uses collision detection to determine what to delete.
Features:
- Collision-based erasing
- Visual eraser cursor
Common uses:
- Removing parts of a flow
// Utility functions for geometric intersection detection
type Point = [number, number];
type Rectangle = { x: number; y: number; width: number; height: number };
// Check if two line segments intersect
function lineSegmentsIntersect(p1: Point, p2: Point, p3: Point, p4: Point): boolean {
const [x1, y1] = p1;
const [x2, y2] = p2;
const [x3, y3] = p3;
const [x4, y4] = p4;
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (Math.abs(denom) < 1e-10) {
return false; // Lines are parallel
}
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
}
// Check if a point is inside a rectangle
function pointInRectangle(point: Point, rect: Rectangle): boolean {
const [x, y] = point;
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
// Get the four edges of a rectangle as line segments
function getRectangleEdges(rect: Rectangle): [Point, Point][] {
const { x, y, width, height } = rect;
return [
[[x, y], [x + width, y]], // top edge
[[x + width, y], [x + width, y + height]], // right edge
[[x + width, y + height], [x, y + height]], // bottom edge
[[x, y + height], [x, y]], // left edge
];
}
// Check if a polyline (series of connected line segments) intersects with a rectangle
export function polylineIntersectsRectangle(points: Point[], rect: Rectangle): boolean {
if (points.length < 2) {
return false;
}
// Early return if any point is inside the rectangle
for (const point of points) {
if (pointInRectangle(point, rect)) {
return true;
}
}
// Check if any line segment intersects with rectangle edges
const rectEdges = getRectangleEdges(rect);
for (let i = 0; i < points.length - 1; i++) {
const lineStart = points[i];
const lineEnd = points[i + 1];
for (const [edgeStart, edgeEnd] of rectEdges) {
if (lineSegmentsIntersect(lineStart, lineEnd, edgeStart, edgeEnd)) {
return true;
}
}
}
return false;
}
// Calculate distance between two points
function distanceBetweenPoints(p1: Point, p2: Point): number {
const [x1, y1] = p1;
const [x2, y2] = p2;
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
// Calculate the closest point on a line segment to a given point
function closestPointOnSegment(point: Point, segmentStart: Point, segmentEnd: Point): Point {
const [px, py] = point;
const [x1, y1] = segmentStart;
const [x2, y2] = segmentEnd;
const dx = x2 - x1;
const dy = y2 - y1;
const lengthSquared = dx * dx + dy * dy;
if (lengthSquared === 0) {
return segmentStart; // Segment is a point
}
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lengthSquared));
return [x1 + t * dx, y1 + t * dy];
}
// Check if two paths intersect using a more efficient approach
export function pathsIntersect(path1: Point[], path2: Point[], threshold = 1): boolean {
if (path1.length < 2 || path2.length < 2) {
return false;
}
// First, do the more precise line segment intersection check
for (let i = 0; i < path1.length - 1; i++) {
for (let j = 0; j < path2.length - 1; j++) {
if (lineSegmentsIntersect(path1[i], path1[i + 1], path2[j], path2[j + 1])) {
return true;
}
}
}
// If no exact intersection, check for proximity based on threshold
if (threshold > 0) {
for (let i = 0; i < path1.length - 1; i++) {
const segment1Start = path1[i];
const segment1End = path1[i + 1];
for (let j = 0; j < path2.length - 1; j++) {
const segment2Start = path2[j];
const segment2End = path2[j + 1];
// Check distance between segment endpoints and closest points
const distances = [
distanceBetweenPoints(segment1Start, closestPointOnSegment(segment1Start, segment2Start, segment2End)),
distanceBetweenPoints(segment1End, closestPointOnSegment(segment1End, segment2Start, segment2End)),
distanceBetweenPoints(segment2Start, closestPointOnSegment(segment2Start, segment1Start, segment1End)),
distanceBetweenPoints(segment2End, closestPointOnSegment(segment2End, segment1Start, segment1End)),
];
if (Math.min(...distances) <= threshold) {
return true;
}
}
}
}
return false;
}📐 Rectangle draw
Create rectangular shapes by clicking and dragging. Good for highlighting areas or creating backgrounds for node groups.
Features:
- Click-and-drag rectangle creation
- Customizable colors
Common uses:
- Creating background containers
- Grouping related nodes visually
- Highlighting sections of diagrams
import type { Node } from '@xyflow/vue';
export type RectangleNodeType = Node<{ color: string }, 'rectangle'>;Whiteboard libraries
If you’re looking for a more complete whiteboard solution, consider libraries designed specifically for it, like tldraw or Excalidraw . These provide a full set of features for collaborative drawing, shapes, text, and more.