diff --git a/js/Canvas.js b/js/Canvas.js index 466b3bd..f893c5c 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -48,6 +48,7 @@ export class Canvas { this.layers = []; this.onStateChange = callbacks.onStateChange; this.onHistoryChange = callbacks.onHistoryChange; + this.onViewportChange = null; this.lastMousePosition = { x: 0, y: 0 }; this.viewport = { x: -(this.width / 1.5), @@ -60,6 +61,7 @@ export class Canvas { }); this.offscreenCanvas = offscreenCanvas; this.offscreenCtx = offscreenCtx; + this.canvasContainer = null; this.dataInitialized = false; this.pendingDataCheck = null; this.imageCache = new Map(); diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 54a3b68..7c14084 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -44,6 +44,7 @@ export class CanvasInteractions { this.canvas.viewport.zoom = newZoom; this.canvas.viewport.x = worldCoords.x - (mouseBufferX / this.canvas.viewport.zoom); this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom); + this.canvas.onViewportChange?.(); } renderAndSave(shouldSave = false) { this.canvas.render(); @@ -87,6 +88,29 @@ export class CanvasInteractions { this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this)); this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this)); } + /** + * Sprawdza czy punkt znajduje się w obszarze któregokolwiek z zaznaczonych layerów + */ + isPointInSelectedLayers(worldX, worldY) { + for (const layer of this.canvas.canvasSelection.selectedLayers) { + if (!layer.visible) + continue; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + // Przekształć punkt do lokalnego układu współrzędnych layera + const dx = worldX - centerX; + const dy = worldY - centerY; + const rad = -layer.rotation * Math.PI / 180; + const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad); + const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad); + // Sprawdź czy punkt jest wewnątrz prostokąta layera + if (Math.abs(rotatedX) <= layer.width / 2 && + Math.abs(rotatedY) <= layer.height / 2) { + return true; + } + } + return false; + } resetInteractionState() { this.interaction.mode = 'none'; this.interaction.resizeHandle = null; @@ -132,9 +156,10 @@ export class CanvasInteractions { // 2. Inne przyciski myszy if (e.button === 2) { // Prawy przycisk myszy this.preventEventDefaults(e); - const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y); - if (clickedLayerResult && this.canvas.canvasSelection.selectedLayers.includes(clickedLayerResult.layer)) { - this.canvas.canvasLayers.showBlendModeMenu(coords.view.x, coords.view.y); + // Sprawdź czy kliknięto w obszarze któregokolwiek z zaznaczonych layerów (niezależnie od przykrycia) + if (this.isPointInSelectedLayers(coords.world.x, coords.world.y)) { + // Nowa logika przekazuje tylko współrzędne świata, menu pozycjonuje się samo + this.canvas.canvasLayers.showBlendModeMenu(coords.world.x, coords.world.y); } return; } @@ -618,6 +643,7 @@ export class CanvasInteractions { this.canvas.viewport.y -= dy / this.canvas.viewport.zoom; this.interaction.panStart = { x: e.clientX, y: e.clientY }; this.canvas.render(); + this.canvas.onViewportChange?.(); } dragLayers(worldCoords) { if (this.interaction.isAltPressed && !this.interaction.hasClonedInDrag && this.canvas.canvasSelection.selectedLayers.length > 0) { diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 8d91ffa..343674e 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -12,6 +12,9 @@ import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js"; const log = createModuleLogger('CanvasLayers'); export class CanvasLayers { constructor(canvas) { + this.blendMenuElement = null; + this.blendMenuWorldX = 0; + this.blendMenuWorldY = 0; this.addLayerWithImage = withErrorHandling(async (image, layerProps = {}, addMode = 'default', targetArea = null) => { if (!image) { throw createValidationError("Image is required for layer creation"); @@ -95,6 +98,7 @@ export class CanvasLayers { log.info("Layer added successfully"); return layer; }, 'CanvasLayers.addLayerWithImage'); + this.currentCloseMenuListener = null; this.canvas = canvas; this.clipboardManager = new ClipboardManager(canvas); this.distanceFieldCache = new WeakMap(); @@ -569,14 +573,70 @@ export class CanvasLayers { } return null; } - showBlendModeMenu(x, y) { + updateBlendModeMenuPosition() { + if (!this.blendMenuElement) + return; + const screenX = (this.blendMenuWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom; + const screenY = (this.blendMenuWorldY - this.canvas.viewport.y) * this.canvas.viewport.zoom; + this.blendMenuElement.style.transform = `translate(${screenX}px, ${screenY}px)`; + } + showBlendModeMenu(worldX, worldY) { + if (this.canvas.canvasSelection.selectedLayers.length === 0) { + return; + } + // Find which selected layer is at the click position (topmost visible layer at that position) + let selectedLayer = null; + const visibleSelectedLayers = this.canvas.canvasSelection.selectedLayers.filter((layer) => layer.visible); + if (visibleSelectedLayers.length === 0) { + return; + } + // Sort by zIndex descending and find the first one that contains the click point + const sortedLayers = visibleSelectedLayers.sort((a, b) => b.zIndex - a.zIndex); + for (const layer of sortedLayers) { + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + // Transform click point to layer's local coordinates + const dx = worldX - centerX; + const dy = worldY - centerY; + const rad = -layer.rotation * Math.PI / 180; + const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad); + const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad); + const withinX = Math.abs(rotatedX) <= layer.width / 2; + const withinY = Math.abs(rotatedY) <= layer.height / 2; + // Check if click is within layer bounds + if (withinX && withinY) { + selectedLayer = layer; + break; + } + } + // If no layer found at click position, fall back to topmost visible selected layer + if (!selectedLayer) { + selectedLayer = sortedLayers[0]; + } + // At this point selectedLayer is guaranteed to be non-null + if (!selectedLayer) { + return; + } + // Remove any existing event listener first + if (this.currentCloseMenuListener) { + document.removeEventListener('mousedown', this.currentCloseMenuListener); + this.currentCloseMenuListener = null; + } this.closeBlendModeMenu(); + // Calculate position in WORLD coordinates (top-right of viewport) + const viewLeft = this.canvas.viewport.x; + const viewTop = this.canvas.viewport.y; + const viewWidth = this.canvas.canvas.width / this.canvas.viewport.zoom; + // Position near top-right corner + this.blendMenuWorldX = viewLeft + viewWidth - (250 / this.canvas.viewport.zoom); // 250px from right edge + this.blendMenuWorldY = viewTop + (10 / this.canvas.viewport.zoom); // 10px from top edge const menu = document.createElement('div'); + this.blendMenuElement = menu; menu.id = 'blend-mode-menu'; menu.style.cssText = ` - position: fixed; - left: ${x}px; - top: ${y}px; + position: absolute; + top: 0; + left: 0; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; @@ -600,10 +660,13 @@ export class CanvasLayers { align-items: center; `; const titleText = document.createElement('span'); - titleText.textContent = 'Blend Mode'; + titleText.textContent = `Blend Mode: ${selectedLayer.name}`; titleText.style.cssText = ` flex: 1; cursor: move; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; const closeButton = document.createElement('button'); closeButton.textContent = '×'; @@ -648,13 +711,11 @@ export class CanvasLayers { blendAreaSlider.type = 'range'; blendAreaSlider.min = '0'; blendAreaSlider.max = '100'; - const selectedLayerForBlendArea = this.canvas.canvasSelection.selectedLayers[0]; - blendAreaSlider.value = selectedLayerForBlendArea?.blendArea?.toString() ?? '0'; + blendAreaSlider.value = selectedLayer?.blendArea?.toString() ?? '0'; blendAreaSlider.oninput = () => { - if (selectedLayerForBlendArea) { + if (selectedLayer) { const newValue = parseInt(blendAreaSlider.value, 10); - selectedLayerForBlendArea.blendArea = newValue; - log.info(`Blend Area changed to: ${newValue}% for layer: ${selectedLayerForBlendArea.id}`); + selectedLayer.blendArea = newValue; this.canvas.render(); } }; @@ -666,14 +727,14 @@ export class CanvasLayers { content.appendChild(blendAreaContainer); let isDragging = false; let dragOffset = { x: 0, y: 0 }; + // Drag logic needs to update world coordinates, not screen coordinates const handleMouseMove = (e) => { if (isDragging) { - const newX = e.clientX - dragOffset.x; - const newY = e.clientY - dragOffset.y; - const maxX = window.innerWidth - menu.offsetWidth; - const maxY = window.innerHeight - menu.offsetHeight; - menu.style.left = Math.max(0, Math.min(newX, maxX)) + 'px'; - menu.style.top = Math.max(0, Math.min(newY, maxY)) + 'px'; + const dx = e.movementX / this.canvas.viewport.zoom; + const dy = e.movementY / this.canvas.viewport.zoom; + this.blendMenuWorldX += dx; + this.blendMenuWorldY += dy; + this.updateBlendModeMenuPosition(); } }; const handleMouseUp = () => { @@ -685,8 +746,6 @@ export class CanvasLayers { }; titleBar.addEventListener('mousedown', (e) => { isDragging = true; - dragOffset.x = e.clientX - parseInt(menu.style.left, 10); - dragOffset.y = e.clientY - parseInt(menu.style.top, 10); e.preventDefault(); e.stopPropagation(); document.addEventListener('mousemove', handleMouseMove); @@ -711,6 +770,11 @@ export class CanvasLayers { option.style.backgroundColor = '#3a3a3a'; } option.onclick = () => { + // Re-check selected layer at the time of click + const currentSelectedLayer = this.canvas.canvasSelection.selectedLayers[0]; + if (!currentSelectedLayer) { + return; + } // Hide only the opacity sliders within other blend mode containers content.querySelectorAll('.blend-mode-container').forEach(c => { const opacitySlider = c.querySelector('input[type="range"]'); @@ -724,16 +788,18 @@ export class CanvasLayers { }); slider.style.display = 'block'; option.style.backgroundColor = '#3a3a3a'; - if (selectedLayer) { - selectedLayer.blendMode = mode.name; - this.canvas.render(); - } + currentSelectedLayer.blendMode = mode.name; + this.canvas.render(); }; slider.addEventListener('input', () => { - if (selectedLayer) { - selectedLayer.opacity = parseInt(slider.value, 10) / 100; - this.canvas.render(); + // Re-check selected layer at the time of slider input + const currentSelectedLayer = this.canvas.canvasSelection.selectedLayers[0]; + if (!currentSelectedLayer) { + return; } + const newOpacity = parseInt(slider.value, 10) / 100; + currentSelectedLayer.opacity = newOpacity; + this.canvas.render(); }); slider.addEventListener('change', async () => { if (selectedLayer) { @@ -766,20 +832,42 @@ export class CanvasLayers { e.preventDefault(); e.stopPropagation(); }); - const container = this.canvas.canvas.parentElement || document.body; - container.appendChild(menu); + if (!this.canvas.canvasContainer) { + log.error("Canvas container not found, cannot append blend mode menu."); + return; + } + this.canvas.canvasContainer.appendChild(menu); + this.updateBlendModeMenuPosition(); + // Add listener for viewport changes + this.canvas.onViewportChange = () => this.updateBlendModeMenuPosition(); const closeMenu = (e) => { if (e.target instanceof Node && !menu.contains(e.target) && !isDragging) { this.closeBlendModeMenu(); - document.removeEventListener('mousedown', closeMenu); + if (this.currentCloseMenuListener) { + document.removeEventListener('mousedown', this.currentCloseMenuListener); + this.currentCloseMenuListener = null; + } } }; - setTimeout(() => document.addEventListener('mousedown', closeMenu), 0); + // Store the listener reference so we can remove it later + this.currentCloseMenuListener = closeMenu; + setTimeout(() => { + document.addEventListener('mousedown', closeMenu); + }, 0); } closeBlendModeMenu() { - const menu = document.getElementById('blend-mode-menu'); - if (menu && menu.parentNode) { - menu.parentNode.removeChild(menu); + log.info("=== BLEND MODE MENU CLOSING ==="); + if (this.blendMenuElement && this.blendMenuElement.parentNode) { + log.info("Removing blend mode menu from DOM"); + this.blendMenuElement.parentNode.removeChild(this.blendMenuElement); + this.blendMenuElement = null; + } + else { + log.info("Blend mode menu not found or already removed"); + } + // Remove viewport change listener + if (this.canvas.onViewportChange) { + this.canvas.onViewportChange = null; } } /** diff --git a/js/CanvasView.js b/js/CanvasView.js index f50935d..0e098a0 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -762,6 +762,7 @@ async function createCanvasWidget(node, widget, app) { overflow: "hidden" } }, [canvas.canvas]); + canvas.canvasContainer = canvasContainer; const layersPanelContainer = $el("div.painterLayersPanelContainer", { style: { position: "absolute", diff --git a/src/Canvas.ts b/src/Canvas.ts index 0336382..944623c 100644 --- a/src/Canvas.ts +++ b/src/Canvas.ts @@ -47,6 +47,7 @@ const log = createModuleLogger('Canvas'); export class Canvas { batchPreviewManagers: BatchPreviewManager[]; canvas: HTMLCanvasElement; + canvasContainer: HTMLDivElement | null; canvasIO: CanvasIO; canvasInteractions: CanvasInteractions; canvasLayers: CanvasLayers; @@ -84,6 +85,7 @@ export class Canvas { offscreenCanvas: HTMLCanvasElement; offscreenCtx: CanvasRenderingContext2D | null; onHistoryChange: ((historyInfo: { canUndo: boolean; canRedo: boolean; }) => void) | undefined; + onViewportChange: (() => void) | null; onStateChange: (() => void) | undefined; pendingBatchContext: any; pendingDataCheck: number | null; @@ -105,6 +107,7 @@ export class Canvas { this.layers = []; this.onStateChange = callbacks.onStateChange; this.onHistoryChange = callbacks.onHistoryChange; + this.onViewportChange = null; this.lastMousePosition = {x: 0, y: 0}; this.viewport = { @@ -119,6 +122,7 @@ export class Canvas { }); this.offscreenCanvas = offscreenCanvas; this.offscreenCtx = offscreenCtx; + this.canvasContainer = null; this.dataInitialized = false; this.pendingDataCheck = null; diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 8aef7ba..5b5cbd8 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -83,6 +83,8 @@ export class CanvasInteractions { this.canvas.viewport.zoom = newZoom; this.canvas.viewport.x = worldCoords.x - (mouseBufferX / this.canvas.viewport.zoom); this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom); + + this.canvas.onViewportChange?.(); } private renderAndSave(shouldSave: boolean = false): void { @@ -134,6 +136,33 @@ export class CanvasInteractions { this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this) as EventListener); } + /** + * Sprawdza czy punkt znajduje się w obszarze któregokolwiek z zaznaczonych layerów + */ + isPointInSelectedLayers(worldX: number, worldY: number): boolean { + for (const layer of this.canvas.canvasSelection.selectedLayers) { + if (!layer.visible) continue; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + // Przekształć punkt do lokalnego układu współrzędnych layera + const dx = worldX - centerX; + const dy = worldY - centerY; + + const rad = -layer.rotation * Math.PI / 180; + const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad); + const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad); + + // Sprawdź czy punkt jest wewnątrz prostokąta layera + if (Math.abs(rotatedX) <= layer.width / 2 && + Math.abs(rotatedY) <= layer.height / 2) { + return true; + } + } + return false; + } + resetInteractionState(): void { this.interaction.mode = 'none'; this.interaction.resizeHandle = null; @@ -186,9 +215,10 @@ export class CanvasInteractions { if (e.button === 2) { // Prawy przycisk myszy this.preventEventDefaults(e); - const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y); - if (clickedLayerResult && this.canvas.canvasSelection.selectedLayers.includes(clickedLayerResult.layer)) { - this.canvas.canvasLayers.showBlendModeMenu(coords.view.x, coords.view.y); + // Sprawdź czy kliknięto w obszarze któregokolwiek z zaznaczonych layerów (niezależnie od przykrycia) + if (this.isPointInSelectedLayers(coords.world.x, coords.world.y)) { + // Nowa logika przekazuje tylko współrzędne świata, menu pozycjonuje się samo + this.canvas.canvasLayers.showBlendModeMenu(coords.world.x, coords.world.y); } return; } @@ -712,6 +742,7 @@ export class CanvasInteractions { this.canvas.viewport.y -= dy / this.canvas.viewport.zoom; this.interaction.panStart = {x: e.clientX, y: e.clientY}; this.canvas.render(); + this.canvas.onViewportChange?.(); } dragLayers(worldCoords: Point): void { diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts index 7aa7b97..f7a4ad6 100644 --- a/src/CanvasLayers.ts +++ b/src/CanvasLayers.ts @@ -29,6 +29,9 @@ export class CanvasLayers { public internalClipboard: Layer[]; public clipboardPreference: ClipboardPreference; private distanceFieldCache: WeakMap>; + private blendMenuElement: HTMLDivElement | null = null; + private blendMenuWorldX: number = 0; + private blendMenuWorldY: number = 0; constructor(canvas: Canvas) { this.canvas = canvas; @@ -654,15 +657,89 @@ export class CanvasLayers { return null; } - showBlendModeMenu(x: number, y: number): void { + private currentCloseMenuListener: ((e: MouseEvent) => void) | null = null; + + updateBlendModeMenuPosition(): void { + if (!this.blendMenuElement) return; + + const screenX = (this.blendMenuWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom; + const screenY = (this.blendMenuWorldY - this.canvas.viewport.y) * this.canvas.viewport.zoom; + + this.blendMenuElement.style.transform = `translate(${screenX}px, ${screenY}px)`; + } + + showBlendModeMenu(worldX: number, worldY: number): void { + if (this.canvas.canvasSelection.selectedLayers.length === 0) { + return; + } + + // Find which selected layer is at the click position (topmost visible layer at that position) + let selectedLayer: Layer | null = null; + const visibleSelectedLayers = this.canvas.canvasSelection.selectedLayers.filter((layer: Layer) => layer.visible); + + if (visibleSelectedLayers.length === 0) { + return; + } + + // Sort by zIndex descending and find the first one that contains the click point + const sortedLayers = visibleSelectedLayers.sort((a: Layer, b: Layer) => b.zIndex - a.zIndex); + + for (const layer of sortedLayers) { + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + // Transform click point to layer's local coordinates + const dx = worldX - centerX; + const dy = worldY - centerY; + + const rad = -layer.rotation * Math.PI / 180; + const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad); + const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad); + + const withinX = Math.abs(rotatedX) <= layer.width / 2; + const withinY = Math.abs(rotatedY) <= layer.height / 2; + + // Check if click is within layer bounds + if (withinX && withinY) { + selectedLayer = layer; + break; + } + } + + // If no layer found at click position, fall back to topmost visible selected layer + if (!selectedLayer) { + selectedLayer = sortedLayers[0]; + } + + // At this point selectedLayer is guaranteed to be non-null + if (!selectedLayer) { + return; + } + + // Remove any existing event listener first + if (this.currentCloseMenuListener) { + document.removeEventListener('mousedown', this.currentCloseMenuListener); + this.currentCloseMenuListener = null; + } + this.closeBlendModeMenu(); + // Calculate position in WORLD coordinates (top-right of viewport) + const viewLeft = this.canvas.viewport.x; + const viewTop = this.canvas.viewport.y; + const viewWidth = this.canvas.canvas.width / this.canvas.viewport.zoom; + + // Position near top-right corner + this.blendMenuWorldX = viewLeft + viewWidth - (250 / this.canvas.viewport.zoom); // 250px from right edge + this.blendMenuWorldY = viewTop + (10 / this.canvas.viewport.zoom); // 10px from top edge + const menu = document.createElement('div'); + this.blendMenuElement = menu; menu.id = 'blend-mode-menu'; menu.style.cssText = ` - position: fixed; - left: ${x}px; - top: ${y}px; + position: absolute; + top: 0; + left: 0; background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; @@ -688,10 +765,13 @@ export class CanvasLayers { `; const titleText = document.createElement('span'); - titleText.textContent = 'Blend Mode'; + titleText.textContent = `Blend Mode: ${selectedLayer.name}`; titleText.style.cssText = ` flex: 1; cursor: move; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; const closeButton = document.createElement('button'); @@ -747,14 +827,12 @@ export class CanvasLayers { blendAreaSlider.min = '0'; blendAreaSlider.max = '100'; - const selectedLayerForBlendArea = this.canvas.canvasSelection.selectedLayers[0]; - blendAreaSlider.value = selectedLayerForBlendArea?.blendArea?.toString() ?? '0'; + blendAreaSlider.value = selectedLayer?.blendArea?.toString() ?? '0'; blendAreaSlider.oninput = () => { - if (selectedLayerForBlendArea) { + if (selectedLayer) { const newValue = parseInt(blendAreaSlider.value, 10); - selectedLayerForBlendArea.blendArea = newValue; - log.info(`Blend Area changed to: ${newValue}% for layer: ${selectedLayerForBlendArea.id}`); + selectedLayer.blendArea = newValue; this.canvas.render(); } }; @@ -770,17 +848,17 @@ export class CanvasLayers { let isDragging = false; let dragOffset = { x: 0, y: 0 }; + // Drag logic needs to update world coordinates, not screen coordinates const handleMouseMove = (e: MouseEvent) => { if (isDragging) { - const newX = e.clientX - dragOffset.x; - const newY = e.clientY - dragOffset.y; - const maxX = window.innerWidth - menu.offsetWidth; - const maxY = window.innerHeight - menu.offsetHeight; - menu.style.left = Math.max(0, Math.min(newX, maxX)) + 'px'; - menu.style.top = Math.max(0, Math.min(newY, maxY)) + 'px'; + const dx = e.movementX / this.canvas.viewport.zoom; + const dy = e.movementY / this.canvas.viewport.zoom; + this.blendMenuWorldX += dx; + this.blendMenuWorldY += dy; + this.updateBlendModeMenuPosition(); } }; - + const handleMouseUp = () => { if (isDragging) { isDragging = false; @@ -788,11 +866,9 @@ export class CanvasLayers { document.removeEventListener('mouseup', handleMouseUp); } }; - + titleBar.addEventListener('mousedown', (e: MouseEvent) => { isDragging = true; - dragOffset.x = e.clientX - parseInt(menu.style.left, 10); - dragOffset.y = e.clientY - parseInt(menu.style.top, 10); e.preventDefault(); e.stopPropagation(); document.addEventListener('mousemove', handleMouseMove); @@ -822,6 +898,12 @@ export class CanvasLayers { } option.onclick = () => { + // Re-check selected layer at the time of click + const currentSelectedLayer = this.canvas.canvasSelection.selectedLayers[0]; + if (!currentSelectedLayer) { + return; + } + // Hide only the opacity sliders within other blend mode containers content.querySelectorAll('.blend-mode-container').forEach(c => { const opacitySlider = c.querySelector('input[type="range"]'); @@ -837,17 +919,21 @@ export class CanvasLayers { slider.style.display = 'block'; option.style.backgroundColor = '#3a3a3a'; - if (selectedLayer) { - selectedLayer.blendMode = mode.name; - this.canvas.render(); - } + currentSelectedLayer.blendMode = mode.name; + this.canvas.render(); }; slider.addEventListener('input', () => { - if (selectedLayer) { - selectedLayer.opacity = parseInt(slider.value, 10) / 100; - this.canvas.render(); + // Re-check selected layer at the time of slider input + const currentSelectedLayer = this.canvas.canvasSelection.selectedLayers[0]; + if (!currentSelectedLayer) { + return; } + + const newOpacity = parseInt(slider.value, 10) / 100; + + currentSelectedLayer.opacity = newOpacity; + this.canvas.render(); }); slider.addEventListener('change', async () => { @@ -883,22 +969,48 @@ export class CanvasLayers { e.stopPropagation(); }); - const container = this.canvas.canvas.parentElement || document.body; - container.appendChild(menu); + if (!this.canvas.canvasContainer) { + log.error("Canvas container not found, cannot append blend mode menu."); + return; + } + this.canvas.canvasContainer.appendChild(menu); + + this.updateBlendModeMenuPosition(); + + // Add listener for viewport changes + this.canvas.onViewportChange = () => this.updateBlendModeMenuPosition(); const closeMenu = (e: MouseEvent) => { if (e.target instanceof Node && !menu.contains(e.target) && !isDragging) { this.closeBlendModeMenu(); - document.removeEventListener('mousedown', closeMenu); + if (this.currentCloseMenuListener) { + document.removeEventListener('mousedown', this.currentCloseMenuListener); + this.currentCloseMenuListener = null; + } } }; - setTimeout(() => document.addEventListener('mousedown', closeMenu), 0); + + // Store the listener reference so we can remove it later + this.currentCloseMenuListener = closeMenu; + + setTimeout(() => { + document.addEventListener('mousedown', closeMenu); + }, 0); } closeBlendModeMenu(): void { - const menu = document.getElementById('blend-mode-menu'); - if (menu && menu.parentNode) { - menu.parentNode.removeChild(menu); + log.info("=== BLEND MODE MENU CLOSING ==="); + if (this.blendMenuElement && this.blendMenuElement.parentNode) { + log.info("Removing blend mode menu from DOM"); + this.blendMenuElement.parentNode.removeChild(this.blendMenuElement); + this.blendMenuElement = null; + } else { + log.info("Blend mode menu not found or already removed"); + } + + // Remove viewport change listener + if (this.canvas.onViewportChange) { + this.canvas.onViewportChange = null; } } diff --git a/src/CanvasView.ts b/src/CanvasView.ts index f340c50..0292e4f 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -815,6 +815,8 @@ $el("label.clipboard-switch.mask-switch", { } }, [canvas.canvas]) as HTMLDivElement; + canvas.canvasContainer = canvasContainer; + const layersPanelContainer = $el("div.painterLayersPanelContainer", { style: { position: "absolute",