# Canvas Overlay Coordinate Transforms ## Overview This document explains how coordinate transformation works between canvas space and screen space in overlay systems. ## Coordinate Systems ### Canvas Space (Graph Space) - **Origin**: Arbitrary (user-defined) - **Units**: Graph units (nodes have x, y positions) - **Affected by**: Nothing - absolute positions in the graph - **Example**: Node at `{ x: 500, y: 300 }` in canvas space ### Screen Space (Pixel Space) - **Origin**: Top-left of the canvas element - **Units**: CSS pixels - **Affected by**: Pan and zoom transformations - **Example**: Same node might be at `{ x: 800, y: 450 }` on screen when zoomed in ## The Transform Strategy CommentLayer uses CSS transforms on the container to handle all coordinate transformation automatically: ```typescript setPanAndScale(viewport: { x: number; y: number; scale: number }) { const transform = `scale(${viewport.scale}) translate(${viewport.x}px, ${viewport.y}px)`; this.container.style.transform = transform; } ``` ### Why This Is Brilliant 1. **No per-element calculations** - Set transform once on container 2. **Browser-optimized** - Hardware accelerated CSS transforms 3. **Simple** - Child elements automatically transform 4. **Performant** - Avoids layout thrashing ### How It Works ``` User pans/zooms canvas ↓ NodeGraphEditor.paint() called ↓ overlay.setPanAndScale({ x, y, scale }) ↓ CSS transform applied to container ↓ Browser automatically transforms all children ``` ## Transform Math (If You Need It) Sometimes you need manual transformations (e.g., calculating if a point hits an element): ### Canvas to Screen ```typescript function canvasToScreen( canvasPoint: { x: number; y: number }, viewport: { x: number; y: number; scale: number } ): { x: number; y: number } { return { x: (canvasPoint.x + viewport.x) * viewport.scale, y: (canvasPoint.y + viewport.y) * viewport.scale }; } ``` **Example:** ```typescript const nodePos = { x: 100, y: 200 }; // Canvas space const viewport = { x: 50, y: 30, scale: 1.5 }; const screenPos = canvasToScreen(nodePos, viewport); // Result: { x: 225, y: 345 } ``` ### Screen to Canvas ```typescript function screenToCanvas( screenPoint: { x: number; y: number }, viewport: { x: number; y: number; scale: number } ): { x: number; y: number } { return { x: screenPoint.x / viewport.scale - viewport.x, y: screenPoint.y / viewport.scale - viewport.y }; } ``` **Example:** ```typescript const clickPos = { x: 225, y: 345 }; // Screen pixels const viewport = { x: 50, y: 30, scale: 1.5 }; const canvasPos = screenToCanvas(clickPos, viewport); // Result: { x: 100, y: 200 } ``` ## React Component Positioning ### Using Transform (Preferred) React components positioned in canvas space: ```tsx function OverlayElement({ x, y, children }: Props) { return (
{children}
); } ``` The parent container's CSS transform automatically converts canvas coords to screen coords. ### Manual Calculation (Avoid) Only if you must position outside the transformed container: ```tsx function OverlayElement({ x, y, viewport, children }: Props) { const screenPos = canvasToScreen({ x, y }, viewport); return (
{children}
); } ``` ## Common Patterns ### Pattern 1: Node Overlay Badge Show a badge on a specific node: ```tsx function NodeBadge({ nodeId, nodegraphEditor }: Props) { const node = nodegraphEditor.nodeGraphModel.findNodeWithId(nodeId); if (!node) return null; // Use canvas coordinates directly return (
!
); } ``` ### Pattern 2: Connection Path Highlight Highlight a connection between two nodes: ```tsx function ConnectionHighlight({ fromNode, toNode }: Props) { // Calculate path in canvas space const path = `M ${fromNode.x} ${fromNode.y} L ${toNode.x} ${toNode.y}`; return ( ); } ``` ### Pattern 3: Mouse Hit Testing Determine if a click hits an overlay element: ```typescript function handleMouseDown(evt: MouseEvent) { // Get click position relative to canvas const canvasElement = this.nodegraphEditor.canvasElement; const rect = canvasElement.getBoundingClientRect(); const screenPos = { x: evt.clientX - rect.left, y: evt.clientY - rect.top }; // Convert to canvas space for hit testing const canvasPos = this.nodegraphEditor.relativeCoordsToNodeGraphCords(screenPos); // Check if click hits any of our elements const hitElement = this.elements.find((el) => pointInsideRectangle(canvasPos, el.bounds)); } ``` ## Scale Considerations ### Scale-Dependent Sizes Some overlay elements should scale with the canvas: ```tsx // Node comment - scales with canvas
{comment}
``` ### Scale-Independent Sizes Some elements should stay the same pixel size regardless of zoom: ```tsx // Control button - stays same size
×
``` ## Best Practices ### ✅ Do 1. **Use container transform** - Let CSS do the work 2. **Store positions in canvas space** - Easier to reason about 3. **Calculate once** - Transform in render, not on every frame 4. **Cache viewport** - Store current viewport for calculations ### ❌ Don't 1. **Don't recalculate on every mouse move** - Only when needed 2. **Don't mix coordinate systems** - Be consistent 3. **Don't forget about scale** - Always consider zoom level 4. **Don't transform twice** - Either container OR manual, not both ## Debugging Tips ### Visualize Coordinate Systems ```tsx function CoordinateDebugger({ viewport }: Props) { return ( <> {/* Canvas origin */}
{/* Grid lines every 100 canvas units */} {Array.from({ length: 20 }, (_, i) => ( ))} ); } ``` ### Log Transforms ```typescript console.log('Canvas pos:', { x: node.x, y: node.y }); console.log('Viewport:', viewport); console.log('Screen pos:', canvasToScreen({ x: node.x, y: node.y }, viewport)); ``` ## Related Documentation - [Main Overview](./CANVAS-OVERLAY-PATTERN.md) - [Architecture](./CANVAS-OVERLAY-ARCHITECTURE.md) - [Mouse Events](./CANVAS-OVERLAY-EVENTS.md)