/** * Annotation Overlay Module * * Creates and manages the overlay canvas that sits above the LiteGraph node editor. * Handles: * - Overlay DOM element creation and positioning * - High-DPI canvas scaling for crisp rendering * - Resize observation and canvas dimension updates * - Pointer events mode switching (draw/erase vs pass-through) */ const OVERLAY_ID = 'annotation-overlay'; const CANVAS_ID = 'annotation-canvas'; // State let overlayElement = null; let canvasElement = null; let canvasContext = null; let resizeObserver = null; let litegraphCanvas = null; // Callbacks let onResizeCallback = null; /** * Creates the overlay container and drawing canvas. * @param {HTMLElement} container - The parent element (canvas.parentElement) * @returns {{ overlay: HTMLDivElement, canvas: HTMLCanvasElement }} */ export function createOverlay(container) { if (overlayElement) { console.warn('[Annotations] Overlay already exists'); return { overlay: overlayElement, canvas: canvasElement }; } // Create overlay container with inline styles (ensures visibility) // z-index 10 keeps overlay above LiteGraph canvas but below ComfyUI's main UI // (sidebars, menus, dialogs typically have z-index 100+) overlayElement = document.createElement('div'); overlayElement.id = OVERLAY_ID; overlayElement.className = 'annotation-overlay'; overlayElement.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; overflow: hidden; pointer-events: none; `; // Create drawing canvas with inline styles canvasElement = document.createElement('canvas'); canvasElement.id = CANVAS_ID; canvasElement.className = 'annotation-canvas'; canvasElement.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: inherit; touch-action: none; `; overlayElement.appendChild(canvasElement); container.appendChild(overlayElement); console.log('[Annotations] Overlay element created:', overlayElement); console.log('[Annotations] Canvas element created:', canvasElement); console.log('[Annotations] Container:', container); // Get 2D context with optimizations canvasContext = canvasElement.getContext('2d', { alpha: true, desynchronized: true, // Reduces latency on supported browsers willReadFrequently: false }); // Find and store reference to LiteGraph canvas litegraphCanvas = container.querySelector('canvas:not(#' + CANVAS_ID + ')'); // Initial sizing updateCanvasSize(); // Setup resize observer setupResizeObserver(container); // Default to pass-through mode (annotations OFF) setPointerEventsMode('none'); console.log('[Annotations] Overlay created'); return { overlay: overlayElement, canvas: canvasElement }; } /** * Updates canvas dimensions to match container, accounting for devicePixelRatio. * This ensures crisp rendering on high-DPI displays. */ export function updateCanvasSize() { if (!canvasElement || !overlayElement) return; const rect = overlayElement.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; // Set display size (CSS pixels) const displayWidth = rect.width; const displayHeight = rect.height; // Set actual canvas buffer size (physical pixels) const bufferWidth = Math.floor(displayWidth * dpr); const bufferHeight = Math.floor(displayHeight * dpr); // Only resize if dimensions actually changed if (canvasElement.width !== bufferWidth || canvasElement.height !== bufferHeight) { canvasElement.width = bufferWidth; canvasElement.height = bufferHeight; // Scale context to account for DPR // This means we can draw in CSS pixel coordinates canvasContext.setTransform(dpr, 0, 0, dpr, 0, 0); console.log(`[Annotations] Canvas resized: ${displayWidth}x${displayHeight} (buffer: ${bufferWidth}x${bufferHeight}, DPR: ${dpr})`); // Notify listeners if (onResizeCallback) { onResizeCallback(displayWidth, displayHeight, dpr); } } } /** * Sets up a ResizeObserver to track container size changes. * @param {HTMLElement} container */ function setupResizeObserver(container) { if (resizeObserver) { resizeObserver.disconnect(); } resizeObserver = new ResizeObserver((entries) => { // Debounce rapid resize events requestAnimationFrame(() => { updateCanvasSize(); }); }); resizeObserver.observe(container); // Also listen for window resize as fallback window.addEventListener('resize', handleWindowResize); } /** * Window resize handler (fallback for ResizeObserver) */ function handleWindowResize() { requestAnimationFrame(() => { updateCanvasSize(); }); } /** * Sets the pointer-events mode on the overlay. * @param {'none' | 'auto'} mode - 'none' passes through, 'auto' captures events */ export function setPointerEventsMode(mode) { if (!overlayElement) return; overlayElement.style.pointerEvents = mode; // Update cursor based on mode if (mode === 'none') { canvasElement.style.cursor = 'default'; } } /** * Sets the cursor style for the canvas. * @param {string} cursor - CSS cursor value */ export function setCursor(cursor) { if (canvasElement) { canvasElement.style.cursor = cursor; } } /** * Shows the overlay (makes it visible). */ export function showOverlay() { if (overlayElement) { overlayElement.style.display = 'block'; } } /** * Hides the overlay (display: none). */ export function hideOverlay() { if (overlayElement) { overlayElement.style.display = 'none'; } } /** * Checks if overlay is currently visible. * @returns {boolean} */ export function isOverlayVisible() { return overlayElement?.style.display !== 'none'; } /** * Gets the 2D rendering context. * @returns {CanvasRenderingContext2D | null} */ export function getContext() { return canvasContext; } /** * Gets the canvas element. * @returns {HTMLCanvasElement | null} */ export function getCanvas() { return canvasElement; } /** * Gets the overlay container element. * @returns {HTMLDivElement | null} */ export function getOverlay() { return overlayElement; } /** * Gets the LiteGraph canvas element. * @returns {HTMLCanvasElement | null} */ export function getLitegraphCanvas() { return litegraphCanvas; } /** * Gets canvas dimensions in CSS pixels. * @returns {{ width: number, height: number, dpr: number }} */ export function getCanvasDimensions() { if (!canvasElement) { return { width: 0, height: 0, dpr: 1 }; } const dpr = window.devicePixelRatio || 1; return { width: canvasElement.width / dpr, height: canvasElement.height / dpr, dpr }; } /** * Registers a callback for resize events. * @param {(width: number, height: number, dpr: number) => void} callback */ export function onResize(callback) { onResizeCallback = callback; } /** * Clears the entire canvas. */ export function clearCanvas() { if (!canvasContext || !canvasElement) return; // Reset to identity transform and clear canvasContext.setTransform(1, 0, 0, 1, 0, 0); canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height); } /** * Destroys the overlay and cleans up resources. */ export function destroyOverlay() { // Remove resize observer if (resizeObserver) { resizeObserver.disconnect(); resizeObserver = null; } // Remove window listener window.removeEventListener('resize', handleWindowResize); // Remove DOM elements if (overlayElement && overlayElement.parentElement) { overlayElement.parentElement.removeChild(overlayElement); } // Clear references overlayElement = null; canvasElement = null; canvasContext = null; litegraphCanvas = null; onResizeCallback = null; console.log('[Annotations] Overlay destroyed'); } /** * Checks if overlay has been initialized. * @returns {boolean} */ export function isInitialized() { return overlayElement !== null && canvasElement !== null; }