diff --git a/js/Canvas.js b/js/Canvas.js index 3f05401..3e73d20 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -13,14 +13,6 @@ import { mask_editor_showing } from "./utils/mask_utils.js"; const log = createModuleLogger('Canvas'); -/** - * Canvas - Fasada dla systemu rysowania - * - * Klasa Canvas pełni rolę fasady, oferując uproszczony interfejs wysokiego poziomu - * dla złożonego systemu rysowania. Zamiast eksponować wszystkie metody modułów, - * udostępnia tylko kluczowe operacje i umożliwia bezpośredni dostęp do modułów - * gdy potrzebna jest bardziej szczegółowa kontrola. - */ export class Canvas { constructor(node, widget, callbacks = {}) { this.node = node; @@ -49,115 +41,187 @@ export class Canvas { this.dataInitialized = false; this.pendingDataCheck = null; - this.imageCache = new Map(); - - // Inicjalizacja modułów - this._initializeModules(callbacks); - - // Podstawowa konfiguracja - this._setupCanvas(); - - // Delegacja interaction dla kompatybilności wstecznej - this.interaction = this.canvasInteractions.interaction; - } - - /** - * Inicjalizuje moduły systemu canvas - * @private - */ - _initializeModules(callbacks) { - // Moduły są publiczne dla bezpośredniego dostępu gdy potrzebne this.maskTool = new MaskTool(this, {onStateChange: this.onStateChange}); + this.initCanvas(); this.canvasState = new CanvasState(this); this.canvasInteractions = new CanvasInteractions(this); this.canvasLayers = new CanvasLayers(this); this.canvasRenderer = new CanvasRenderer(this); this.canvasIO = new CanvasIO(this); this.imageReferenceManager = new ImageReferenceManager(this); - } + this.interaction = this.canvasInteractions.interaction; + + this.setupEventListeners(); + this.initNodeData(); - /** - * Konfiguruje podstawowe właściwości canvas - * @private - */ - _setupCanvas() { - this.initCanvas(); - this.canvasInteractions.setupEventListeners(); - this.canvasIO.initNodeData(); - - // Inicjalizacja warstw z domyślną przezroczystością this.layers = this.layers.map(layer => ({ ...layer, opacity: 1 })); + + this.imageCache = new Map(); } - // ========================================== - // GŁÓWNE OPERACJE FASADY - // ========================================== + async loadStateFromDB() { + return this.canvasState.loadStateFromDB(); + } + + async saveStateToDB(immediate = false) { + return this.canvasState.saveStateToDB(immediate); + } - /** - * Ładuje stan canvas z bazy danych - */ async loadInitialState() { log.info("Loading initial state for node:", this.node.id); - const loaded = await this.canvasState.loadStateFromDB(); + const loaded = await this.loadStateFromDB(); if (!loaded) { log.info("No saved state found, initializing from node data."); - await this.canvasIO.initNodeData(); + await this.initNodeData(); } this.saveState(); this.render(); } - /** - * Zapisuje obecny stan - * @param {boolean} replaceLast - Czy zastąpić ostatni stan w historii - */ + _notifyStateChange() { + if (this.onStateChange) { + this.onStateChange(); + } + } + saveState(replaceLast = false) { this.canvasState.saveState(replaceLast); this.incrementOperationCount(); this._notifyStateChange(); } - /** - * Cofnij ostatnią operację - */ undo() { this.canvasState.undo(); this.incrementOperationCount(); this._notifyStateChange(); } - /** - * Ponów cofniętą operację - */ redo() { this.canvasState.redo(); this.incrementOperationCount(); this._notifyStateChange(); } - /** - * Renderuje canvas - */ - render() { - this.canvasRenderer.render(); + updateSelectionAfterHistory() { + const newSelectedLayers = []; + if (this.selectedLayers) { + this.selectedLayers.forEach(sl => { + const found = this.layers.find(l => l.id === sl.id); + if (found) newSelectedLayers.push(found); + }); + } + this.updateSelection(newSelectedLayers); } - /** - * Dodaje warstwę z obrazem - * @param {Image} image - Obraz do dodania - * @param {Object} layerProps - Właściwości warstwy - * @param {string} addMode - Tryb dodawania - */ - async addLayer(image, layerProps = {}, addMode = 'default') { + updateHistoryButtons() { + if (this.onHistoryChange) { + const historyInfo = this.canvasState.getHistoryInfo(); + this.onHistoryChange({ + canUndo: historyInfo.canUndo, + canRedo: historyInfo.canRedo + }); + } + } + + initCanvas() { + this.canvas.width = this.width; + this.canvas.height = this.height; + this.canvas.style.border = '1px solid black'; + this.canvas.style.maxWidth = '100%'; + this.canvas.style.backgroundColor = '#606060'; + this.canvas.style.width = '100%'; + this.canvas.style.height = '100%'; + + + this.canvas.tabIndex = 0; + this.canvas.style.outline = 'none'; + } + + setupEventListeners() { + this.canvasInteractions.setupEventListeners(); + } + + updateSelection(newSelection) { + this.selectedLayers = newSelection || []; + this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null; + if (this.onSelectionChange) { + this.onSelectionChange(); + } + } + + async copySelectedLayers() { + return this.canvasLayers.copySelectedLayers(); + } + + pasteLayers() { + return this.canvasLayers.pasteLayers(); + } + + async handlePaste(addMode) { + return this.canvasLayers.handlePaste(addMode); + } + + + handleMouseMove(e) { + this.canvasInteractions.handleMouseMove(e); + } + + + handleMouseUp(e) { + this.canvasInteractions.handleMouseUp(e); + } + + + handleMouseLeave(e) { + this.canvasInteractions.handleMouseLeave(e); + } + + + handleWheel(e) { + this.canvasInteractions.handleWheel(e); + } + + handleKeyDown(e) { + this.canvasInteractions.handleKeyDown(e); + } + + handleKeyUp(e) { + this.canvasInteractions.handleKeyUp(e); + } + + + isRotationHandle(x, y) { + return this.canvasLayers.isRotationHandle(x, y); + } + + async addLayerWithImage(image, layerProps = {}, addMode = 'default') { return this.canvasLayers.addLayerWithImage(image, layerProps, addMode); } - /** - * Usuwa wybrane warstwy - */ + + async addLayer(image, addMode = 'default') { + return this.addLayerWithImage(image, {}, addMode); + } + + async removeLayer(index) { + if (index >= 0 && index < this.layers.length) { + const layer = this.layers[index]; + if (layer.imageId) { + const isImageUsedElsewhere = this.layers.some((l, i) => i !== index && l.imageId === layer.imageId); + if (!isImageUsedElsewhere) { + await removeImage(layer.imageId); + this.imageCache.delete(layer.imageId); + } + } + this.layers.splice(index, 1); + this.selectedLayer = this.layers[this.layers.length - 1] || null; + this.render(); + } + } + removeSelectedLayers() { if (this.selectedLayers.length > 0) { this.saveState(); @@ -168,49 +232,239 @@ export class Canvas { } } - /** - * Aktualizuje zaznaczenie warstw - * @param {Array} newSelection - Nowa lista zaznaczonych warstw - */ - updateSelection(newSelection) { - this.selectedLayers = newSelection || []; - this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null; - if (this.onSelectionChange) { - this.onSelectionChange(); - } + getMouseWorldCoordinates(e) { + const rect = this.canvas.getBoundingClientRect(); + + const mouseX_DOM = e.clientX - rect.left; + const mouseY_DOM = e.clientY - rect.top; + + const scaleX = this.offscreenCanvas.width / rect.width; + const scaleY = this.offscreenCanvas.height / rect.height; + + const mouseX_Buffer = mouseX_DOM * scaleX; + const mouseY_Buffer = mouseY_DOM * scaleY; + + const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x; + const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y; + + return {x: worldX, y: worldY}; + } + + getMouseViewCoordinates(e) { + const rect = this.canvas.getBoundingClientRect(); + const mouseX_DOM = e.clientX - rect.left; + const mouseY_DOM = e.clientY - rect.top; + + const scaleX = this.canvas.width / rect.width; + const scaleY = this.canvas.height / rect.height; + + const mouseX_Canvas = mouseX_DOM * scaleX; + const mouseY_Canvas = mouseY_DOM * scaleY; + + return { x: mouseX_Canvas, y: mouseY_Canvas }; + } + + + moveLayer(fromIndex, toIndex) { + return this.canvasLayers.moveLayer(fromIndex, toIndex); + } + + resizeLayer(scale) { + this.selectedLayers.forEach(layer => { + layer.width *= scale; + layer.height *= scale; + }); + this.render(); + this.saveState(); + } + + rotateLayer(angle) { + this.selectedLayers.forEach(layer => { + layer.rotation += angle; + }); + this.render(); + this.saveState(); } - /** - * Zmienia rozmiar obszaru wyjściowego - * @param {number} width - Nowa szerokość - * @param {number} height - Nowa wysokość - * @param {boolean} saveHistory - Czy zapisać w historii - */ updateOutputAreaSize(width, height, saveHistory = true) { return this.canvasLayers.updateOutputAreaSize(width, height, saveHistory); } - /** - * Eksportuje spłaszczony canvas jako blob - */ + render() { + this.canvasRenderer.render(); + } + + + getHandles(layer) { + return this.canvasLayers.getHandles(layer); + } + + getHandleAtPosition(worldX, worldY) { + return this.canvasLayers.getHandleAtPosition(worldX, worldY); + } + + async getFlattenedCanvasAsBlob() { return this.canvasLayers.getFlattenedCanvasAsBlob(); } - /** - * Importuje najnowszy obraz - */ + async getFlattenedSelectionAsBlob() { + return this.canvasLayers.getFlattenedSelectionAsBlob(); + } + + moveLayerUp() { + return this.canvasLayers.moveLayerUp(); + } + + moveLayerDown() { + return this.canvasLayers.moveLayerDown(); + } + + + getLayerAtPosition(worldX, worldY) { + return this.canvasLayers.getLayerAtPosition(worldX, worldY); + } + + getResizeHandle(x, y) { + return this.canvasLayers.getResizeHandle(x, y); + } + + async mirrorHorizontal() { + return this.canvasLayers.mirrorHorizontal(); + } + + async mirrorVertical() { + return this.canvasLayers.mirrorVertical(); + } + + async getLayerImageData(layer) { + return this.canvasLayers.getLayerImageData(layer); + } + + addMattedLayer(image, mask) { + return this.canvasLayers.addMattedLayer(image, mask); + } + + async addInputToCanvas(inputImage, inputMask) { + return this.canvasIO.addInputToCanvas(inputImage, inputMask); + } + + async convertTensorToImage(tensor) { + return this.canvasIO.convertTensorToImage(tensor); + } + + async convertTensorToMask(tensor) { + return this.canvasIO.convertTensorToMask(tensor); + } + + async initNodeData() { + return this.canvasIO.initNodeData(); + } + + scheduleDataCheck() { + return this.canvasIO.scheduleDataCheck(); + } + + async processImageData(imageData) { + return this.canvasIO.processImageData(imageData); + } + + addScaledLayer(image, scale) { + return this.canvasIO.addScaledLayer(image, scale); + } + + convertTensorToImageData(tensor) { + return this.canvasIO.convertTensorToImageData(tensor); + } + + async createImageFromData(imageData) { + return this.canvasIO.createImageFromData(imageData); + } + + async retryDataLoad(maxRetries = 3, delay = 1000) { + return this.canvasIO.retryDataLoad(maxRetries, delay); + } + + async processMaskData(maskData) { + return this.canvasIO.processMaskData(maskData); + } + + async loadImageFromCache(base64Data) { + return this.canvasIO.loadImageFromCache(base64Data); + } + + async importImage(cacheData) { + return this.canvasIO.importImage(cacheData); + } + async importLatestImage() { return this.canvasIO.importLatestImage(); } - // ========================================== - // OPERACJE NA MASCE - // ========================================== + showBlendModeMenu(x, y) { + return this.canvasLayers.showBlendModeMenu(x, y); + } + + handleBlendModeSelection(mode) { + return this.canvasLayers.handleBlendModeSelection(mode); + } + + showOpacitySlider(mode) { + return this.canvasLayers.showOpacitySlider(mode); + } /** - * Uruchamia edytor masek + * Zwiększa licznik operacji (wywoływane przy każdej operacji na canvas) */ + incrementOperationCount() { + if (this.imageReferenceManager) { + this.imageReferenceManager.incrementOperationCount(); + } + } + + /** + * Ręczne uruchomienie garbage collection + */ + async runGarbageCollection() { + if (this.imageReferenceManager) { + await this.imageReferenceManager.manualGarbageCollection(); + } + } + + /** + * Zwraca statystyki garbage collection + */ + getGarbageCollectionStats() { + if (this.imageReferenceManager) { + const stats = this.imageReferenceManager.getStats(); + return { + ...stats, + operationCount: this.imageReferenceManager.operationCount, + operationThreshold: this.imageReferenceManager.operationThreshold + }; + } + return null; + } + + /** + * Ustawia próg operacji dla automatycznego GC + */ + setGarbageCollectionThreshold(threshold) { + if (this.imageReferenceManager) { + this.imageReferenceManager.setOperationThreshold(threshold); + } + } + + /** + * Czyści zasoby canvas (wywoływane przy usuwaniu) + */ + destroy() { + if (this.imageReferenceManager) { + this.imageReferenceManager.destroy(); + } + log.info("Canvas destroyed"); + } + async startMaskEditor() { const blob = await this.canvasLayers.getFlattenedCanvasAsBlob(); if (!blob) { @@ -257,133 +511,16 @@ export class Canvas { } } - // ========================================== - // METODY POMOCNICZE - // ========================================== - - /** - * Inicjalizuje podstawowe właściwości canvas - */ - initCanvas() { - this.canvas.width = this.width; - this.canvas.height = this.height; - this.canvas.style.border = '1px solid black'; - this.canvas.style.maxWidth = '100%'; - this.canvas.style.backgroundColor = '#606060'; - this.canvas.style.width = '100%'; - this.canvas.style.height = '100%'; - this.canvas.tabIndex = 0; - this.canvas.style.outline = 'none'; - } - - /** - * Pobiera współrzędne myszy w układzie świata - * @param {MouseEvent} e - Zdarzenie myszy - */ - getMouseWorldCoordinates(e) { - const rect = this.canvas.getBoundingClientRect(); - - const mouseX_DOM = e.clientX - rect.left; - const mouseY_DOM = e.clientY - rect.top; - - const scaleX = this.offscreenCanvas.width / rect.width; - const scaleY = this.offscreenCanvas.height / rect.height; - - const mouseX_Buffer = mouseX_DOM * scaleX; - const mouseY_Buffer = mouseY_DOM * scaleY; - - const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x; - const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y; - - return {x: worldX, y: worldY}; - } - - /** - * Pobiera współrzędne myszy w układzie widoku - * @param {MouseEvent} e - Zdarzenie myszy - */ - getMouseViewCoordinates(e) { - const rect = this.canvas.getBoundingClientRect(); - const mouseX_DOM = e.clientX - rect.left; - const mouseY_DOM = e.clientY - rect.top; - - const scaleX = this.canvas.width / rect.width; - const scaleY = this.canvas.height / rect.height; - - const mouseX_Canvas = mouseX_DOM * scaleX; - const mouseY_Canvas = mouseY_DOM * scaleY; - - return { x: mouseX_Canvas, y: mouseY_Canvas }; - } - - /** - * Aktualizuje zaznaczenie po operacji historii - */ - updateSelectionAfterHistory() { - const newSelectedLayers = []; - if (this.selectedLayers) { - this.selectedLayers.forEach(sl => { - const found = this.layers.find(l => l.id === sl.id); - if (found) newSelectedLayers.push(found); - }); - } - this.updateSelection(newSelectedLayers); - } - - /** - * Aktualizuje przyciski historii - */ - updateHistoryButtons() { - if (this.onHistoryChange) { - const historyInfo = this.canvasState.getHistoryInfo(); - this.onHistoryChange({ - canUndo: historyInfo.canUndo, - canRedo: historyInfo.canRedo - }); - } - } - - /** - * Zwiększa licznik operacji (dla garbage collection) - */ - incrementOperationCount() { - if (this.imageReferenceManager) { - this.imageReferenceManager.incrementOperationCount(); - } - } - - /** - * Czyści zasoby canvas - */ - destroy() { - if (this.imageReferenceManager) { - this.imageReferenceManager.destroy(); - } - log.info("Canvas destroyed"); - } - - /** - * Powiadamia o zmianie stanu - * @private - */ - _notifyStateChange() { - if (this.onStateChange) { - this.onStateChange(); - } - } - - // ========================================== - // METODY DLA EDYTORA MASEK - // ========================================== - waitWhileMaskEditing() { + // Czekamy, aż edytor się pojawi, a potem zniknie. if (mask_editor_showing(app)) { this.editorWasShowing = true; } if (!mask_editor_showing(app) && this.editorWasShowing) { + // Edytor był widoczny i już go nie ma this.editorWasShowing = false; - setTimeout(() => this.handleMaskEditorClose(), 100); + setTimeout(() => this.handleMaskEditorClose(), 100); // Dajemy chwilę na aktualizację } else { setTimeout(this.waitWhileMaskEditing.bind(this), 100); } @@ -410,22 +547,28 @@ export class Canvas { return; } + // Używamy wymiarów naszego płótna, aby zapewnić spójność const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.width; tempCanvas.height = this.height; const tempCtx = tempCanvas.getContext('2d'); + // Rysujemy obrazek z edytora, który zawiera maskę w kanale alfa tempCtx.drawImage(resultImage, 0, 0, this.width, this.height); const imageData = tempCtx.getImageData(0, 0, this.width, this.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { - const originalAlpha = data[i + 3]; - data[i] = 255; - data[i + 1] = 255; - data[i + 2] = 255; - data[i + 3] = 255 - originalAlpha; + const originalAlpha = data[i + 3]; + + // Ustawiamy biały kolor + data[i] = 255; // R + data[i + 1] = 255; // G + data[i + 2] = 255; // B + + // Odwracamy kanał alfa + data[i + 3] = 255 - originalAlpha; } tempCtx.putImageData(imageData, 0, 0); @@ -434,17 +577,19 @@ export class Canvas { maskAsImage.src = tempCanvas.toDataURL(); await new Promise(resolve => maskAsImage.onload = resolve); + // Łączymy nową maskę z istniejącą, zamiast ją nadpisywać const maskCtx = this.maskTool.maskCtx; const destX = -this.maskTool.x; const destY = -this.maskTool.y; maskCtx.globalCompositeOperation = 'screen'; maskCtx.drawImage(maskAsImage, destX, destY); - maskCtx.globalCompositeOperation = 'source-over'; + maskCtx.globalCompositeOperation = 'source-over'; // Przywracamy domyślny tryb this.render(); this.saveState(); + // Zaktualizuj podgląd węzła nowym, spłaszczonym obrazem const new_preview = new Image(); const blob = await this.canvasLayers.getFlattenedCanvasAsBlob(); if (blob) { @@ -457,72 +602,4 @@ export class Canvas { this.render(); } - - // ========================================== - // METODY DELEGUJĄCE DLA KOMPATYBILNOŚCI - // ========================================== - - /** - * Te metody są zachowane tymczasowo dla kompatybilności wstecznej. - * W nowych implementacjach należy używać bezpośrednio odpowiednich modułów: - * - this.canvasLayers dla operacji na warstwach - * - this.canvasInteractions dla obsługi interakcji - * - this.canvasIO dla operacji I/O - * - this.canvasState dla zarządzania stanem - */ - - // Delegacje do CanvasState - async saveStateToDB(immediate = false) { return this.canvasState.saveStateToDB(immediate); } - - // Delegacje do CanvasLayers - async copySelectedLayers() { return this.canvasLayers.copySelectedLayers(); } - async handlePaste(addMode) { return this.canvasLayers.handlePaste(addMode); } - async addLayerWithImage(image, layerProps = {}, addMode = 'default') { - return this.canvasLayers.addLayerWithImage(image, layerProps, addMode); - } - moveLayerUp() { return this.canvasLayers.moveLayerUp(); } - moveLayerDown() { return this.canvasLayers.moveLayerDown(); } - resizeLayer(scale) { - this.selectedLayers.forEach(layer => { - layer.width *= scale; - layer.height *= scale; - }); - this.render(); - this.saveState(); - } - rotateLayer(angle) { - this.selectedLayers.forEach(layer => { - layer.rotation += angle; - }); - this.render(); - this.saveState(); - } - getLayerAtPosition(worldX, worldY) { return this.canvasLayers.getLayerAtPosition(worldX, worldY); } - getHandles(layer) { return this.canvasLayers.getHandles(layer); } - getHandleAtPosition(worldX, worldY) { return this.canvasLayers.getHandleAtPosition(worldX, worldY); } - async mirrorHorizontal() { return this.canvasLayers.mirrorHorizontal(); } - async mirrorVertical() { return this.canvasLayers.mirrorVertical(); } - async getLayerImageData(layer) { return this.canvasLayers.getLayerImageData(layer); } - showBlendModeMenu(x, y) { return this.canvasLayers.showBlendModeMenu(x, y); } - // Delegacje do CanvasInteractions - handleMouseMove(e) { this.canvasInteractions.handleMouseMove(e); } - - - // Delegacje do ImageReferenceManager - async runGarbageCollection() { - if (this.imageReferenceManager) { - await this.imageReferenceManager.manualGarbageCollection(); - } - } - getGarbageCollectionStats() { - if (this.imageReferenceManager) { - const stats = this.imageReferenceManager.getStats(); - return { - ...stats, - operationCount: this.imageReferenceManager.operationCount, - operationThreshold: this.imageReferenceManager.operationThreshold - }; - } - return null; - } } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 1b30120..afeed8b 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -162,6 +162,32 @@ export class CanvasLayers { return this.addLayerWithImage(image); } + async removeLayer(index) { + if (index >= 0 && index < this.canvasLayers.layers.length) { + const layer = this.canvasLayers.layers[index]; + if (layer.imageId) { + const isImageUsedElsewhere = this.canvasLayers.layers.some((l, i) => i !== index && l.imageId === layer.imageId); + if (!isImageUsedElsewhere) { + await removeImage(layer.imageId); + this.canvasLayers.imageCache.delete(layer.imageId); + } + } + this.canvasLayers.layers.splice(index, 1); + this.canvasLayers.selectedLayer = this.canvasLayers.layers[this.canvasLayers.layers.length - 1] || null; + this.canvasLayers.render(); + this.canvasLayers.saveState(); + } + } + + moveLayer(fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < this.canvasLayers.layers.length && + toIndex >= 0 && toIndex < this.canvasLayers.layers.length) { + const layer = this.canvasLayers.layers.splice(fromIndex, 1)[0]; + this.canvasLayers.layers.splice(toIndex, 0, layer); + this.canvasLayers.render(); + } + } + moveLayerUp() { if (this.canvasLayers.selectedLayers.length === 0) return; const selectedIndicesSet = new Set(this.canvasLayers.selectedLayers.map(layer => this.canvasLayers.layers.indexOf(layer))); @@ -335,6 +361,33 @@ export class CanvasLayers { } } + addMattedLayer(image, mask) { + const layer = { + image: image, + mask: mask, + x: 0, + y: 0, + width: image.width, + height: image.height, + rotation: 0, + zIndex: this.canvasLayers.layers.length + }; + + this.canvasLayers.layers.push(layer); + this.canvasLayers.selectedLayer = layer; + this.canvasLayers.render(); + } + + isRotationHandle(x, y) { + if (!this.canvasLayers.selectedLayer) return false; + + const handleX = this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width / 2; + const handleY = this.canvasLayers.selectedLayer.y - 20; + const handleRadius = 5; + + return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius; + } + getHandles(layer) { if (!layer) return {}; @@ -389,6 +442,34 @@ export class CanvasLayers { return null; } + getResizeHandle(x, y) { + if (!this.canvasLayers.selectedLayer) return null; + + const handleRadius = 5; + const handles = { + 'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y}, + 'ne': { + x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, + y: this.canvasLayers.selectedLayer.y + }, + 'se': { + x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, + y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height + }, + 'sw': { + x: this.canvasLayers.selectedLayer.x, + y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height + } + }; + + for (const [position, point] of Object.entries(handles)) { + if (Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)) <= handleRadius) { + return position; + } + } + return null; + } + showBlendModeMenu(x, y) { this.closeBlendModeMenu(); @@ -510,6 +591,17 @@ export class CanvasLayers { } } + handleBlendModeSelection(mode) { + if (this.selectedBlendMode === mode && !this.isAdjustingOpacity) { + this.applyBlendMode(mode, this.blendOpacity); + this.closeBlendModeMenu(); + } else { + this.selectedBlendMode = mode; + this.isAdjustingOpacity = true; + this.showOpacitySlider(mode); + } + } + showOpacitySlider(mode) { const slider = document.createElement('input'); slider.type = 'range'; @@ -642,4 +734,36 @@ export class CanvasLayers { }, 'image/png'); }); } + + async getFlattenedCanvasAsDataURL() { + if (this.canvasLayers.layers.length === 0) return null; + + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvasLayers.width; + tempCanvas.height = this.canvasLayers.height; + const tempCtx = tempCanvas.getContext('2d'); + + const sortedLayers = [...this.canvasLayers.layers].sort((a, b) => a.zIndex - b.zIndex); + sortedLayers.forEach(layer => { + if (!layer.image) return; + + tempCtx.save(); + tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; + tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + tempCtx.translate(centerX, centerY); + tempCtx.rotate(layer.rotation * Math.PI / 180); + tempCtx.drawImage( + layer.image, + -layer.width / 2, + -layer.height / 2, + layer.width, + layer.height + ); + tempCtx.restore(); + }); + + return tempCanvas.toDataURL('image/png'); + } }