Files
OpenNoodl/dev-docs/reference/CANVAS-OVERLAY-PATTERN.md
2026-01-04 00:17:33 +01:00

4.9 KiB

Canvas Overlay Pattern

Overview

Status: Proven Pattern (CommentLayer is production-ready)
Location: packages/noodl-editor/src/editor/src/views/commentlayer.ts
Created: Phase 4 PREREQ-003

This document describes the pattern for creating React overlays that float above the HTML5 Canvas in the Node Graph Editor. The pattern is proven and production-tested via CommentLayer.

What This Pattern Enables

React components that:

  • Float over the HTML5 Canvas
  • Stay synchronized with canvas pan/zoom
  • Handle mouse events intelligently (overlay vs canvas)
  • Integrate with the existing EventDispatcher system
  • Use modern React 19 APIs

Why This Matters

Phase 4 visualization views need this pattern:

  • VIEW-005: Data Lineage - Glowing path highlights
  • VIEW-006: Impact Radar - Dependency visualization
  • VIEW-007: Semantic Layers - Node visibility filtering

All of these require React UI floating over the canvas with proper coordinate transformation and event handling.

Documentation Structure

This pattern is documented across several focused files:

  1. Architecture Overview - How overlays integrate with NodeGraphEditor
  2. Coordinate Transforms - Canvas space ↔ Screen space conversion
  3. Mouse Event Handling - Intelligent event routing
  4. React Integration - React 19 patterns and lifecycle
  5. Code Examples - Practical implementation examples

Quick Start

Minimal Overlay Example

import React from 'react';
import { createRoot, Root } from 'react-dom/client';

import { NodeGraphEditor } from './nodegrapheditor';

class SimpleOverlay {
  private root: Root;
  private container: HTMLDivElement;

  constructor(private nodegraphEditor: NodeGraphEditor) {}

  renderTo(container: HTMLDivElement) {
    this.container = container;
    this.root = createRoot(container);
    this.render();
  }

  setPanAndScale(panAndScale: { x: number; y: number; scale: number }) {
    const transform = `scale(${panAndScale.scale}) translate(${panAndScale.x}px, ${panAndScale.y}px)`;
    this.container.style.transform = transform;
  }

  private render() {
    this.root.render(<div>My Overlay Content</div>);
  }

  dispose() {
    if (this.root) {
      this.root.unmount();
    }
  }
}

Integration with NodeGraphEditor

// In nodegrapheditor.ts
this.myOverlay = new SimpleOverlay(this);
this.myOverlay.renderTo(overlayDiv);

// Update on pan/zoom
this.myOverlay.setPanAndScale(this.getPanAndScale());

Key Insights from CommentLayer

1. CSS Transform Strategy (Brilliant!)

The entire overlay stays in sync via a single CSS transform on the container:

const transform = `scale(${scale}) translate(${x}px, ${y}px)`;
container.style.transform = transform;

No complex calculations per element - the browser handles it all!

2. React Root Reuse

Create roots once, reuse for all re-renders:

if (!this.root) {
  this.root = createRoot(this.container);
}
this.root.render(<MyComponent {...props} />);

3. Two-Layer System

CommentLayer uses two layers:

  • Background layer - Behind canvas (e.g., colored comment boxes)
  • Foreground layer - In front of canvas (e.g., comment controls, resize handles)

This allows visual layering: comments behind nodes, but controls in front.

4. Mouse Event Forwarding

Complex but powerful: overlay determines if clicks should go to canvas or stay in overlay. See Mouse Event Handling for details.

Common Gotchas

Don't: Create new roots on every render

// BAD - memory leak!
render() {
  this.root = createRoot(this.container);
  this.root.render(<Component />);
}

Do: Create once, reuse

// GOOD
constructor() {
  this.root = createRoot(this.container);
}
render() {
  this.root.render(<Component />);
}

Don't: Manually calculate positions for every element

// BAD - complex and slow
elements.forEach((el) => {
  el.style.left = (el.x + pan.x) * scale + 'px';
  el.style.top = (el.y + pan.y) * scale + 'px';
});

Do: Use container transform

// GOOD - browser handles it
container.style.transform = `scale(${scale}) translate(${pan.x}px, ${pan.y}px)`;

Next Steps