Files
2026-01-18 00:13:02 +09:00

319 lines
8.2 KiB
JavaScript

/**
* 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;
}