JavaScript is required

Freehand Strokes to Graph Nodes

This example overlays a freehand drawing canvas on top of relation-graph and converts each finished stroke into a new editable graph node. It demonstrates zoom-aware stroke normalization, custom node rendering for stored SVG path data, and selection-based resize and delete workflows after insertion.

Turn Freehand Strokes into Editable Graph Nodes

What This Example Builds

This example builds a small graph editor that lets the user draw directly on top of the graph canvas and turn each finished stroke into a new node. The page starts with a fixed-position seed graph, then adds a full-screen drawing layer, a floating utility window, resize handles for selected nodes, and a one-click delete action for a single selected node.

The visible result is not a temporary annotation layer. Each completed stroke becomes a real graph node with its own position, width, height, stored SVG path data, stroke color, and stroke width. After insertion, the drawn node can enter the same selection flow as the starter nodes, so the important takeaway is the conversion from whiteboard-style input into normal relation-graph content.

How the Data Is Organized

The initial graph is a local RGJsonData object with three nodes and two lines. Because the graph uses a fixed layout, each seed node already includes explicit x and y coordinates before setJsonData(...) runs. There is no external fetch or heavy preprocessing for the starter dataset.

The runtime-generated nodes follow a different structure. When the drawing overlay finishes a stroke, MyGraph parses the emitted M/L path string into points, computes a padded bounding box, converts the box origin from viewport coordinates into graph canvas coordinates, and rewrites the path into node-local coordinates. The inserted node stores that normalized geometry in node.data.path, plus strokeColor and strokeWidth, while the node itself keeps graph-level placement and dimensions.

In a real product, the same pattern can represent handwritten notes, rough diagram shapes, operator markup, map scribbles, or stylus traces that need to remain editable graph entities instead of flat image pixels.

How relation-graph Is Used

The graph uses layoutName: 'fixed', which matches the seed data’s explicit coordinates and makes runtime insertion predictable. The options also define a rectangular default node shape, baseline node and line sizes, and dragEventAction: 'selection', so the canvas starts in a selection-oriented editing mode instead of a read-only viewport mode.

RGProvider supplies context for the hooks used across the example. RGHooks.useGraphInstance() is the main control surface: it loads the initial JSON, recenters the graph, translates stroke coordinates with getCanvasXyByViewXy(...), reads zoom from getOptions(), inserts new nodes with addNodes(...), clears editing state, removes nodes, updates runtime options, and supports image export preparation and restore in the shared floating window. RGHooks.useEditingNodes() drives the custom toolbar so it only appears when exactly one node is selected, while RGHooks.useGraphStore() in DraggableWindow reflects the current drag and wheel settings inside the panel UI.

Two slots carry most of the custom rendering and editing behavior. RGSlotOnView mounts RGEditingNodeController, RGEditingResize, MyNodeToolbar, and RGEditingConnectController directly over the graph surface, so selection and editing stay on-canvas. RGSlotOnNode overrides node rendering for svg-path nodes and draws the saved path data as inline SVG, while normal nodes still fall back to text rendering.

The local style file exists but is effectively a placeholder in this example. Most of the visible customization comes from inline styles and utility classes in SmoothCanvas.tsx, MyGraph.tsx, and MyNodeToolbar.tsx rather than from deeper stylesheet overrides.

Key Interactions

  • A floating switch in DraggableWindow turns free-draw mode on or off by mounting or unmounting the SmoothCanvas overlay.
  • The overlay accepts both mouse and touch input, previews the stroke live, and emits the final SVG path string when drawing stops.
  • The bottom toolbar changes color and line width before the stroke is committed, and those values are persisted on the generated graph node.
  • Clicking a node moves it into editing mode, which exposes resize handles and the custom delete toolbar.
  • Clicking empty canvas clears editing nodes, clears the active editing line, and removes checked highlights.
  • The shared settings panel can switch wheel and drag behavior at runtime and export the current graph as an image.

Key Code Fragments

This fragment shows that the example combines fixed-layout graph options with a free-draw mode toggle in local React state.

const graphInstance = RGHooks.useGraphInstance();
const [freelyDrawMode, setFreelyDrawMode] = useState(true);

const graphOptions: RGOptions = {
    debug: false,
    defaultNodeShape: RGNodeShape.rect,
    defaultNodeWidth: 100,
    defaultNodeHeight: 40,
    defaultLineWidth: 2,
    layout: {
        layoutName: 'fixed',
    },
    dragEventAction: 'selection',
    disableDragNode: false,
};

This fragment is the core stroke-to-node conversion step: it translates viewport coordinates into graph coordinates and rewrites the stroke as node-local SVG data.

const canvasPos = graphInstance.getCanvasXyByViewXy({ x: minX, y: minY });

const zoom = graphInstance.getOptions().canvasZoom / 100;
const nodeW = Math.max(viewWidth / zoom, 20);
const nodeH = Math.max(viewHeight / zoom, 20);

const relativePath = coords.map((p, i) => {
    const relX = (p.x - minX) / zoom;
    const relY = (p.y - minY) / zoom;
    return `${i === 0 ? 'M' : 'L'}${relX.toFixed(1)} ${relY.toFixed(1)}`;
}).join(' ');

This fragment shows how the newly created node preserves the sketch as graph data instead of flattening it into a screenshot.

const newNode = {
    id: newNodeId,
    type: 'svg-path',
    x: canvasPos.x,
    y: canvasPos.y,
    width: nodeW,
    height: nodeH,
    text: '',
    color: 'transparent',
    borderColor: 'transparent',
    data: {
        path: relativePath,
        originalViewBox: `0 0 ${nodeW} ${nodeH}`,
        strokeColor: color,
        strokeWidth: width
    }
};

graphInstance.addNodes([newNode]);

This fragment shows the custom node slot that renders stored stroke geometry as inline SVG for svg-path nodes.

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => {
        if (node.type === 'svg-path') {
            return (
                <div style={{ width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none' }}>
                    <svg width="100%" height="100%" style={{ display: 'block' }}>
                        <path
                            d={node.data?.path}
                            fill="none"
                            stroke={node.data?.strokeColor || '#FF0000'}
                            strokeWidth={node.data?.strokeWidth || 4}
                        />
                    </svg>
                </div>
            );
        }

This fragment shows that the overlay is a real input surface with live drawing plus a bottom toolbar for stroke styling.

<canvas
    ref={canvasRef}
    onMouseDown={startDrawing}
    onMouseMove={draw}
    onMouseUp={stopDrawing}
    onMouseLeave={stopDrawing}
    onTouchStart={startDrawing}
    onTouchMove={draw}
    onTouchEnd={stopDrawing}
    style={{
        position: 'absolute',
        top: 0,
        left: 0,
        zIndex: 2,
        touchAction: 'none',
        cursor: 'crosshair',
        background: 'transparent',
        pointerEvents: 'auto'
    }}
/>

What Makes This Example Distinct

Its rare point is not simply “drawing on canvas.” The comparison data shows that this example is notable because it places a full-canvas freehand layer over relation-graph, accepts both mouse and touch input, lets the user choose stroke color and width, and then converts the finished mark into a normal graph node.

Compared with canvas-selection, this example preserves the actual sketch geometry and stroke styling inside node data instead of reducing the gesture to a built-in rectangle or circle. Compared with resize-focused examples such as gee-node-resize, the main lesson here is not the resize controller by itself, but the full workflow of drawing, inserting, selecting, resizing, and deleting. Compared with connection or line-editing examples such as line-vertex-on-node and change-line-path, the emphasis is sketch-based node authoring rather than edge creation or route editing.

The strongest reusable combination is the freehand overlay, zoom-aware stroke normalization, RGSlotOnNode rendering for stored SVG paths, selection-based editing overlays, and the selection-mounted delete chip on one compact screen. The prepared comparison data supports describing that combination as rare; it does not support claiming this is the only example with gesture-driven node creation.

Where Else This Pattern Applies

This pattern transfers well to products where users sketch first and organize later. Examples include whiteboard-style brainstorming tools that promote pen strokes into persistent graph items, field-service or operations tools that let staff mark rough paths and then keep them as structured nodes, lightweight process-mapping tools where hand-drawn shapes later enter a normal editing workflow, and educational or annotation tools that turn stylus input into graph objects instead of static images.

The same approach can also be extended to persistence and richer editing. A production variant could save the generated svg-path node data to a backend, attach labels after insertion, classify strokes by shape type, or add a second-stage editor for reshaping existing paths while keeping the relation-graph selection model intact.