diff --git a/js/Canvas.js b/js/Canvas.js index 555e4b2..9b97f25 100644 --- a/js/Canvas.js +++ b/js/Canvas.js @@ -2,6 +2,7 @@ import {saveImage, getImage, removeImage} from "./db.js"; import {MaskTool} from "./Mask_tool.js"; import {CanvasState} from "./CanvasState.js"; import {CanvasInteractions} from "./CanvasInteractions.js"; +import {CanvasLayers} from "./CanvasLayers.js"; import {logger, LogLevel} from "./logger.js"; // Inicjalizacja loggera dla modułu Canvas @@ -53,6 +54,7 @@ export class Canvas { this.initCanvas(); this.canvasState = new CanvasState(this); // Nowy moduł zarządzania stanem this.canvasInteractions = new CanvasInteractions(this); // Nowy moduł obsługi interakcji + this.canvasLayers = new CanvasLayers(this); // Nowy moduł operacji na warstwach // Po utworzeniu CanvasInteractions, użyj jego interaction state this.interaction = this.canvasInteractions.interaction; @@ -60,23 +62,11 @@ export class Canvas { this.setupEventListeners(); this.initNodeData(); - this.blendModes = [ - {name: '.', label: 'Normal'}, - {name: '.', label: 'Multiply'}, - {name: '.', label: 'Screen'}, - {name: '.', label: 'Overlay'}, - {name: '.', label: 'Darken'}, - {name: '.', label: 'Lighten'}, - {name: '.', label: 'Color Dodge'}, - {name: '.', label: 'Color Burn'}, - {name: '.', label: 'Hard Light'}, - {name: '.', label: 'Soft Light'}, - {name: '.', label: 'Difference'}, - {name: '.', label: 'Exclusion'} - ]; - this.selectedBlendMode = null; - this.blendOpacity = 100; - this.isAdjustingOpacity = false; + // Przeniesione do CanvasLayers + this.blendModes = this.canvasLayers.blendModes; + this.selectedBlendMode = this.canvasLayers.selectedBlendMode; + this.blendOpacity = this.canvasLayers.blendOpacity; + this.isAdjustingOpacity = this.canvasLayers.isAdjustingOpacity; this.layers = this.layers.map(layer => ({ ...layer, @@ -168,86 +158,17 @@ export class Canvas { // Interaction methods moved to CanvasInteractions module + // Delegacja metod operacji na warstwach do CanvasLayers async copySelectedLayers() { - if (this.selectedLayers.length === 0) return; - this.internalClipboard = this.selectedLayers.map(layer => ({...layer})); - log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`); - try { - const blob = await this.getFlattenedSelectionAsBlob(); - if (blob) { - const item = new ClipboardItem({'image/png': blob}); - await navigator.clipboard.write([item]); - log.info("Flattened selection copied to the system clipboard."); - } - } catch (error) { - log.error("Failed to copy image to system clipboard:", error); - } + return this.canvasLayers.copySelectedLayers(); } - pasteLayers() { - if (this.internalClipboard.length === 0) return; - this.saveState(); - const newLayers = []; - const pasteOffset = 20; - - this.internalClipboard.forEach(clipboardLayer => { - const newLayer = { - ...clipboardLayer, - x: clipboardLayer.x + pasteOffset / this.viewport.zoom, - y: clipboardLayer.y + pasteOffset / this.viewport.zoom, - zIndex: this.layers.length - }; - this.layers.push(newLayer); - newLayers.push(newLayer); - }); - - this.updateSelection(newLayers); - this.render(); - log.info(`Pasted ${newLayers.length} layer(s).`); + return this.canvasLayers.pasteLayers(); } - async handlePaste() { - try { - if (!navigator.clipboard?.read) { - log.info("Browser does not support clipboard read API. Falling back to internal paste."); - this.pasteLayers(); - return; - } - - const clipboardItems = await navigator.clipboard.read(); - let imagePasted = false; - - for (const item of clipboardItems) { - const imageType = item.types.find(type => type.startsWith('image/')); - - if (imageType) { - const blob = await item.getType(imageType); - const reader = new FileReader(); - reader.onload = (event) => { - const img = new Image(); - img.onload = async () => { - await this.addLayerWithImage(img, { - x: this.lastMousePosition.x - img.width / 2, - y: this.lastMousePosition.y - img.height / 2, - }); - }; - img.src = event.target.result; - }; - reader.readAsDataURL(blob); - imagePasted = true; - break; - } - } - if (!imagePasted) { - this.pasteLayers(); - } - - } catch (err) { - log.error("Paste operation failed, falling back to internal paste. Error:", err); - this.pasteLayers(); - } + return this.canvasLayers.handlePaste(); } @@ -289,56 +210,15 @@ export class Canvas { isRotationHandle(x, y) { - if (!this.selectedLayer) return false; - - const handleX = this.selectedLayer.x + this.selectedLayer.width / 2; - const handleY = this.selectedLayer.y - 20; - const handleRadius = 5; - - return Math.sqrt(Math.pow(x - handleX, 2) + Math.pow(y - handleY, 2)) <= handleRadius; + return this.canvasLayers.isRotationHandle(x, y); } async addLayerWithImage(image, layerProps = {}) { - try { - log.debug("Adding layer with image:", image); - - // Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB - const imageId = this.generateUUID(); - await saveImage(imageId, image.src); - this.imageCache.set(imageId, image.src); // Zapisz w pamięci podręcznej jako imageSrc - - const layer = { - image: image, - imageId: imageId, // Dodaj imageId do warstwy - x: (this.width - image.width) / 2, - y: (this.height - image.height) / 2, - width: image.width, - height: image.height, - rotation: 0, - zIndex: this.layers.length, - blendMode: 'normal', - opacity: 1, - ...layerProps // Nadpisz domyślne właściwości, jeśli podano - }; - - this.layers.push(layer); - this.updateSelection([layer]); - this.render(); - this.saveState(); - - log.info("Layer added successfully"); - return layer; - } catch (error) { - log.error("Error adding layer:", error); - throw error; - } + return this.canvasLayers.addLayerWithImage(image, layerProps); } generateUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return this.canvasLayers.generateUUID(); } async addLayer(image) { @@ -381,50 +261,15 @@ export class Canvas { } snapToGrid(value, gridSize = 64) { - return Math.round(value / gridSize) * gridSize; + return this.canvasLayers.snapToGrid(value, gridSize); } getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) { - if (!layer) { - return {dx: 0, dy: 0}; - } - - const layerEdges = { - left: layer.x, - right: layer.x + layer.width, - top: layer.y, - bottom: layer.y + layer.height - }; - const x_adjustments = [ - {type: 'x', delta: this.snapToGrid(layerEdges.left, gridSize) - layerEdges.left}, - {type: 'x', delta: this.snapToGrid(layerEdges.right, gridSize) - layerEdges.right} - ]; - - const y_adjustments = [ - {type: 'y', delta: this.snapToGrid(layerEdges.top, gridSize) - layerEdges.top}, - {type: 'y', delta: this.snapToGrid(layerEdges.bottom, gridSize) - layerEdges.bottom} - ]; - x_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta)); - y_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta)); - const bestXSnap = x_adjustments - .filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9) - .sort((a, b) => a.abs - b.abs)[0]; - const bestYSnap = y_adjustments - .filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9) - .sort((a, b) => a.abs - b.abs)[0]; - return { - dx: bestXSnap ? bestXSnap.delta : 0, - dy: bestYSnap ? bestYSnap.delta : 0 - }; + return this.canvasLayers.getSnapAdjustment(layer, gridSize, snapThreshold); } moveLayer(fromIndex, toIndex) { - if (fromIndex >= 0 && fromIndex < this.layers.length && - toIndex >= 0 && toIndex < this.layers.length) { - const layer = this.layers.splice(fromIndex, 1)[0]; - this.layers.splice(toIndex, 0, layer); - this.render(); - } + return this.canvasLayers.moveLayer(fromIndex, toIndex); } resizeLayer(scale) { @@ -445,21 +290,7 @@ export class Canvas { } updateCanvasSize(width, height, saveHistory = true) { - if (saveHistory) { - this.saveState(); - } - this.width = width; - this.height = height; - this.maskTool.resize(width, height); - - this.canvas.width = width; - this.canvas.height = height; - - this.render(); - - if (saveHistory) { - this.saveStateToDB(); - } + return this.canvasLayers.updateCanvasSize(width, height, saveHistory); } render() { @@ -756,81 +587,19 @@ export class Canvas { getHandles(layer) { - if (!layer) return {}; - - const centerX = layer.x + layer.width / 2; - const centerY = layer.y + layer.height / 2; - const rad = layer.rotation * Math.PI / 180; - const cos = Math.cos(rad); - const sin = Math.sin(rad); - - const halfW = layer.width / 2; - const halfH = layer.height / 2; - const localHandles = { - 'n': {x: 0, y: -halfH}, - 'ne': {x: halfW, y: -halfH}, - 'e': {x: halfW, y: 0}, - 'se': {x: halfW, y: halfH}, - 's': {x: 0, y: halfH}, - 'sw': {x: -halfW, y: halfH}, - 'w': {x: -halfW, y: 0}, - 'nw': {x: -halfW, y: -halfH}, - 'rot': {x: 0, y: -halfH - 20 / this.viewport.zoom} - }; - - const worldHandles = {}; - for (const key in localHandles) { - const p = localHandles[key]; - worldHandles[key] = { - x: centerX + (p.x * cos - p.y * sin), - y: centerY + (p.x * sin + p.y * cos) - }; - } - return worldHandles; + return this.canvasLayers.getHandles(layer); } getHandleAtPosition(worldX, worldY) { - if (this.selectedLayers.length === 0) return null; - - const handleRadius = 8 / this.viewport.zoom; - for (let i = this.selectedLayers.length - 1; i >= 0; i--) { - const layer = this.selectedLayers[i]; - const handles = this.getHandles(layer); - - for (const key in handles) { - const handlePos = handles[key]; - const dx = worldX - handlePos.x; - const dy = worldY - handlePos.y; - if (dx * dx + dy * dy <= handleRadius * handleRadius) { - return {layer: layer, handle: key}; - } - } - } - return null; + return this.canvasLayers.getHandleAtPosition(worldX, worldY); } worldToLocal(worldX, worldY, layerProps) { - const dx = worldX - layerProps.centerX; - const dy = worldY - layerProps.centerY; - const rad = -layerProps.rotation * Math.PI / 180; - const cos = Math.cos(rad); - const sin = Math.sin(rad); - - return { - x: dx * cos - dy * sin, - y: dx * sin + dy * cos - }; + return this.canvasLayers.worldToLocal(worldX, worldY, layerProps); } localToWorld(localX, localY, layerProps) { - const rad = layerProps.rotation * Math.PI / 180; - const cos = Math.cos(rad); - const sin = Math.sin(rad); - - return { - x: layerProps.centerX + localX * cos - localY * sin, - y: layerProps.centerY + localX * sin + localY * cos - }; + return this.canvasLayers.localToWorld(localX, localY, layerProps); } @@ -1059,314 +828,45 @@ export class Canvas { } async getFlattenedCanvasAsBlob() { - return new Promise((resolve, reject) => { - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = this.width; - tempCanvas.height = this.height; - const tempCtx = tempCanvas.getContext('2d'); - - const sortedLayers = [...this.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(); - }); - - tempCanvas.toBlob((blob) => { - if (blob) { - resolve(blob); - } else { - reject(new Error('Canvas toBlob failed.')); - } - }, 'image/png'); - }); + return this.canvasLayers.getFlattenedCanvasAsBlob(); } async getFlattenedSelectionAsBlob() { - if (this.selectedLayers.length === 0) { - return null; - } - - return new Promise((resolve) => { - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - this.selectedLayers.forEach(layer => { - const centerX = layer.x + layer.width / 2; - const centerY = layer.y + layer.height / 2; - const rad = layer.rotation * Math.PI / 180; - const cos = Math.cos(rad); - const sin = Math.sin(rad); - - const halfW = layer.width / 2; - const halfH = layer.height / 2; - - const corners = [ - {x: -halfW, y: -halfH}, - {x: halfW, y: -halfH}, - {x: halfW, y: halfH}, - {x: -halfW, y: halfH} - ]; - - corners.forEach(p => { - const worldX = centerX + (p.x * cos - p.y * sin); - const worldY = centerY + (p.x * sin + p.y * cos); - - minX = Math.min(minX, worldX); - minY = Math.min(minY, worldY); - maxX = Math.max(maxX, worldX); - maxY = Math.max(maxY, worldY); - }); - }); - - const newWidth = Math.ceil(maxX - minX); - const newHeight = Math.ceil(maxY - minY); - - if (newWidth <= 0 || newHeight <= 0) { - resolve(null); - return; - } - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = newWidth; - tempCanvas.height = newHeight; - const tempCtx = tempCanvas.getContext('2d'); - - tempCtx.translate(-minX, -minY); - - const sortedSelection = [...this.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); - - sortedSelection.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(); - }); - tempCanvas.toBlob((blob) => { - resolve(blob); - }, 'image/png'); - }); + return this.canvasLayers.getFlattenedSelectionAsBlob(); } moveLayerUp() { - if (this.selectedLayers.length === 0) return; - const selectedIndicesSet = new Set(this.selectedLayers.map(layer => this.layers.indexOf(layer))); - - const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => b - a); - - sortedIndices.forEach(index => { - const targetIndex = index + 1; - - if (targetIndex < this.layers.length && !selectedIndicesSet.has(targetIndex)) { - [this.layers[index], this.layers[targetIndex]] = [this.layers[targetIndex], this.layers[index]]; - } - }); - this.layers.forEach((layer, i) => layer.zIndex = i); - this.render(); - this.saveState(); + return this.canvasLayers.moveLayerUp(); } moveLayerDown() { - if (this.selectedLayers.length === 0) return; - const selectedIndicesSet = new Set(this.selectedLayers.map(layer => this.layers.indexOf(layer))); - - const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => a - b); - - sortedIndices.forEach(index => { - const targetIndex = index - 1; - - if (targetIndex >= 0 && !selectedIndicesSet.has(targetIndex)) { - [this.layers[index], this.layers[targetIndex]] = [this.layers[targetIndex], this.layers[index]]; - } - }); - this.layers.forEach((layer, i) => layer.zIndex = i); - this.render(); - this.saveState(); + return this.canvasLayers.moveLayerDown(); } getLayerAtPosition(worldX, worldY) { - - for (let i = this.layers.length - 1; i >= 0; i--) { - const layer = this.layers[i]; - - const centerX = layer.x + layer.width / 2; - const centerY = layer.y + layer.height / 2; - - 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); - - if (Math.abs(rotatedX) <= layer.width / 2 && Math.abs(rotatedY) <= layer.height / 2) { - const localX = rotatedX + layer.width / 2; - const localY = rotatedY + layer.height / 2; - - return { - layer: layer, - localX: localX, - localY: localY - }; - } - } - return null; + return this.canvasLayers.getLayerAtPosition(worldX, worldY); } getResizeHandle(x, y) { - if (!this.selectedLayer) return null; - - const handleRadius = 5; - const handles = { - 'nw': {x: this.selectedLayer.x, y: this.selectedLayer.y}, - 'ne': {x: this.selectedLayer.x + this.selectedLayer.width, y: this.selectedLayer.y}, - 'se': { - x: this.selectedLayer.x + this.selectedLayer.width, - y: this.selectedLayer.y + this.selectedLayer.height - }, - 'sw': {x: this.selectedLayer.x, y: this.selectedLayer.y + this.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; + return this.canvasLayers.getResizeHandle(x, y); } async mirrorHorizontal() { - if (this.selectedLayers.length === 0) return; - - const promises = this.selectedLayers.map(layer => { - return new Promise(resolve => { - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - tempCanvas.width = layer.image.width; - tempCanvas.height = layer.image.height; - - tempCtx.translate(tempCanvas.width, 0); - tempCtx.scale(-1, 1); - tempCtx.drawImage(layer.image, 0, 0); - - const newImage = new Image(); - newImage.onload = () => { - layer.image = newImage; - resolve(); - }; - newImage.src = tempCanvas.toDataURL(); - }); - }); - - await Promise.all(promises); - this.render(); - this.saveState(); + return this.canvasLayers.mirrorHorizontal(); } async mirrorVertical() { - if (this.selectedLayers.length === 0) return; - - const promises = this.selectedLayers.map(layer => { - return new Promise(resolve => { - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - tempCanvas.width = layer.image.width; - tempCanvas.height = layer.image.height; - - tempCtx.translate(0, tempCanvas.height); - tempCtx.scale(1, -1); - tempCtx.drawImage(layer.image, 0, 0); - - const newImage = new Image(); - newImage.onload = () => { - layer.image = newImage; - resolve(); - }; - newImage.src = tempCanvas.toDataURL(); - }); - }); - - await Promise.all(promises); - this.render(); - this.saveState(); + return this.canvasLayers.mirrorVertical(); } async getLayerImageData(layer) { - try { - const tempCanvas = document.createElement('canvas'); - const tempCtx = tempCanvas.getContext('2d'); - - tempCanvas.width = layer.width; - tempCanvas.height = layer.height; - - tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); - - tempCtx.save(); - tempCtx.translate(layer.width / 2, layer.height / 2); - tempCtx.rotate(layer.rotation * Math.PI / 180); - tempCtx.drawImage( - layer.image, - -layer.width / 2, - -layer.height / 2, - layer.width, - layer.height - ); - tempCtx.restore(); - - const dataUrl = tempCanvas.toDataURL('image/png'); - if (!dataUrl.startsWith('data:image/png;base64,')) { - throw new Error("Invalid image data format"); - } - - return dataUrl; - } catch (error) { - log.error("Error getting layer image data:", error); - throw error; - } + return this.canvasLayers.getLayerImageData(layer); } addMattedLayer(image, mask) { - const layer = { - image: image, - mask: mask, - x: 0, - y: 0, - width: image.width, - height: image.height, - rotation: 0, - zIndex: this.layers.length - }; - - this.layers.push(layer); - this.selectedLayer = layer; - this.render(); + return this.canvasLayers.addMattedLayer(image, mask); } processInputData(nodeData) { @@ -1811,151 +1311,18 @@ export class Canvas { } showBlendModeMenu(x, y) { - - const existingMenu = document.getElementById('blend-mode-menu'); - if (existingMenu) { - document.body.removeChild(existingMenu); - } - - const menu = document.createElement('div'); - menu.id = 'blend-mode-menu'; - menu.style.cssText = ` - position: fixed; - left: ${x}px; - top: ${y}px; - background: #2a2a2a; - border: 1px solid #3a3a3a; - border-radius: 4px; - padding: 5px; - z-index: 1000; - box-shadow: 0 2px 10px rgba(0,0,0,0.3); - `; - - this.blendModes.forEach(mode => { - const container = document.createElement('div'); - container.className = 'blend-mode-container'; - container.style.cssText = ` - margin-bottom: 5px; - `; - - const option = document.createElement('div'); - option.style.cssText = ` - padding: 5px 10px; - color: white; - cursor: pointer; - transition: background-color 0.2s; - `; - option.textContent = `${mode.label} (${mode.name})`; - - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = '0'; - slider.max = '100'; - - slider.value = this.selectedLayer.opacity ? Math.round(this.selectedLayer.opacity * 100) : 100; - slider.style.cssText = ` - width: 100%; - margin: 5px 0; - display: none; - `; - - if (this.selectedLayer.blendMode === mode.name) { - slider.style.display = 'block'; - option.style.backgroundColor = '#3a3a3a'; - } - - option.onclick = () => { - - menu.querySelectorAll('input[type="range"]').forEach(s => { - s.style.display = 'none'; - }); - menu.querySelectorAll('.blend-mode-container div').forEach(d => { - d.style.backgroundColor = ''; - }); - - slider.style.display = 'block'; - option.style.backgroundColor = '#3a3a3a'; - - if (this.selectedLayer) { - this.selectedLayer.blendMode = mode.name; - this.render(); - } - }; - - slider.addEventListener('input', () => { - if (this.selectedLayer) { - this.selectedLayer.opacity = slider.value / 100; - this.render(); - } - }); - - slider.addEventListener('change', async () => { - if (this.selectedLayer) { - this.selectedLayer.opacity = slider.value / 100; - this.render(); - - await this.saveToServer(this.widget.value); - if (this.node) { - app.graph.runStep(); - } - } - }); - - container.appendChild(option); - container.appendChild(slider); - menu.appendChild(container); - }); - - document.body.appendChild(menu); - - const closeMenu = (e) => { - if (!menu.contains(e.target)) { - document.body.removeChild(menu); - document.removeEventListener('mousedown', closeMenu); - } - }; - setTimeout(() => { - document.addEventListener('mousedown', closeMenu); - }, 0); + return this.canvasLayers.showBlendModeMenu(x, y); } 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); - } + return this.canvasLayers.handleBlendModeSelection(mode); } showOpacitySlider(mode) { - - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = '0'; - slider.max = '100'; - slider.value = this.blendOpacity; - slider.className = 'blend-opacity-slider'; - - slider.addEventListener('input', (e) => { - this.blendOpacity = parseInt(e.target.value); - - }); - - const modeElement = document.querySelector(`[data-blend-mode="${mode}"]`); - if (modeElement) { - modeElement.appendChild(slider); - } + return this.canvasLayers.showOpacitySlider(mode); } applyBlendMode(mode, opacity) { - - this.currentLayer.style.mixBlendMode = mode; - this.currentLayer.style.opacity = opacity / 100; - - this.selectedBlendMode = null; - this.isAdjustingOpacity = false; + return this.canvasLayers.applyBlendMode(mode, opacity); } } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js new file mode 100644 index 0000000..9aa775f --- /dev/null +++ b/js/CanvasLayers.js @@ -0,0 +1,816 @@ +import {saveImage, getImage, removeImage} from "./db.js"; +import {logger, LogLevel} from "./logger.js"; + +// Inicjalizacja loggera dla modułu CanvasLayers +const log = { + debug: (...args) => logger.debug('CanvasLayers', ...args), + info: (...args) => logger.info('CanvasLayers', ...args), + warn: (...args) => logger.warn('CanvasLayers', ...args), + error: (...args) => logger.error('CanvasLayers', ...args) +}; + +// Konfiguracja loggera dla modułu CanvasLayers +logger.setModuleLevel('CanvasLayers', LogLevel.INFO); // Domyślnie INFO, można zmienić na DEBUG dla szczegółowych logów + +export class CanvasLayers { + constructor(canvas) { + this.canvas = canvas; + this.blendModes = [ + {name: 'normal', label: 'Normal'}, + {name: 'multiply', label: 'Multiply'}, + {name: 'screen', label: 'Screen'}, + {name: 'overlay', label: 'Overlay'}, + {name: 'darken', label: 'Darken'}, + {name: 'lighten', label: 'Lighten'}, + {name: 'color-dodge', label: 'Color Dodge'}, + {name: 'color-burn', label: 'Color Burn'}, + {name: 'hard-light', label: 'Hard Light'}, + {name: 'soft-light', label: 'Soft Light'}, + {name: 'difference', label: 'Difference'}, + {name: 'exclusion', label: 'Exclusion'} + ]; + this.selectedBlendMode = null; + this.blendOpacity = 100; + this.isAdjustingOpacity = false; + this.internalClipboard = []; + } + + // Generowanie unikalnego identyfikatora + generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + // Operacje na warstwach + async copySelectedLayers() { + if (this.canvas.selectedLayers.length === 0) return; + this.internalClipboard = this.canvas.selectedLayers.map(layer => ({...layer})); + log.info(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`); + try { + const blob = await this.getFlattenedSelectionAsBlob(); + if (blob) { + const item = new ClipboardItem({'image/png': blob}); + await navigator.clipboard.write([item]); + log.info("Flattened selection copied to the system clipboard."); + } + } catch (error) { + log.error("Failed to copy image to system clipboard:", error); + } + } + + pasteLayers() { + if (this.internalClipboard.length === 0) return; + this.canvas.saveState(); + const newLayers = []; + const pasteOffset = 20; + + this.internalClipboard.forEach(clipboardLayer => { + const newLayer = { + ...clipboardLayer, + x: clipboardLayer.x + pasteOffset / this.canvas.viewport.zoom, + y: clipboardLayer.y + pasteOffset / this.canvas.viewport.zoom, + zIndex: this.canvas.layers.length + }; + this.canvas.layers.push(newLayer); + newLayers.push(newLayer); + }); + + this.canvas.updateSelection(newLayers); + this.canvas.render(); + log.info(`Pasted ${newLayers.length} layer(s).`); + } + + async handlePaste() { + try { + if (!navigator.clipboard?.read) { + log.info("Browser does not support clipboard read API. Falling back to internal paste."); + this.pasteLayers(); + return; + } + + const clipboardItems = await navigator.clipboard.read(); + let imagePasted = false; + + for (const item of clipboardItems) { + const imageType = item.types.find(type => type.startsWith('image/')); + + if (imageType) { + const blob = await item.getType(imageType); + const reader = new FileReader(); + reader.onload = (event) => { + const img = new Image(); + img.onload = async () => { + await this.addLayerWithImage(img, { + x: this.canvas.lastMousePosition.x - img.width / 2, + y: this.canvas.lastMousePosition.y - img.height / 2, + }); + }; + img.src = event.target.result; + }; + reader.readAsDataURL(blob); + imagePasted = true; + break; + } + } + if (!imagePasted) { + this.pasteLayers(); + } + + } catch (err) { + log.error("Paste operation failed, falling back to internal paste. Error:", err); + this.pasteLayers(); + } + } + + async addLayerWithImage(image, layerProps = {}) { + try { + log.debug("Adding layer with image:", image); + + // Wygeneruj unikalny identyfikator dla obrazu i zapisz go do IndexedDB + const imageId = this.generateUUID(); + await saveImage(imageId, image.src); + this.canvas.imageCache.set(imageId, image.src); // Zapisz w pamięci podręcznej jako imageSrc + + const layer = { + image: image, + imageId: imageId, // Dodaj imageId do warstwy + x: (this.canvas.width - image.width) / 2, + y: (this.canvas.height - image.height) / 2, + width: image.width, + height: image.height, + rotation: 0, + zIndex: this.canvas.layers.length, + blendMode: 'normal', + opacity: 1, + ...layerProps // Nadpisz domyślne właściwości, jeśli podano + }; + + this.canvas.layers.push(layer); + this.canvas.updateSelection([layer]); + this.canvas.render(); + this.canvas.saveState(); + + log.info("Layer added successfully"); + return layer; + } catch (error) { + log.error("Error adding layer:", error); + throw error; + } + } + + async addLayer(image) { + return this.addLayerWithImage(image); + } + + async removeLayer(index) { + if (index >= 0 && index < this.canvas.layers.length) { + const layer = this.canvas.layers[index]; + if (layer.imageId) { + // Usuń obraz z IndexedDB, jeśli nie jest używany przez inne warstwy + const isImageUsedElsewhere = this.canvas.layers.some((l, i) => i !== index && l.imageId === layer.imageId); + if (!isImageUsedElsewhere) { + await removeImage(layer.imageId); + this.canvas.imageCache.delete(layer.imageId); // Usuń z pamięci podręcznej + } + } + this.canvas.layers.splice(index, 1); + this.canvas.selectedLayer = this.canvas.layers[this.canvas.layers.length - 1] || null; + this.canvas.render(); + } + } + + moveLayer(fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < this.canvas.layers.length && + toIndex >= 0 && toIndex < this.canvas.layers.length) { + const layer = this.canvas.layers.splice(fromIndex, 1)[0]; + this.canvas.layers.splice(toIndex, 0, layer); + this.canvas.render(); + } + } + + moveLayerUp() { + if (this.canvas.selectedLayers.length === 0) return; + const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer))); + + const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => b - a); + + sortedIndices.forEach(index => { + const targetIndex = index + 1; + + if (targetIndex < this.canvas.layers.length && !selectedIndicesSet.has(targetIndex)) { + [this.canvas.layers[index], this.canvas.layers[targetIndex]] = [this.canvas.layers[targetIndex], this.canvas.layers[index]]; + } + }); + this.canvas.layers.forEach((layer, i) => layer.zIndex = i); + this.canvas.render(); + this.canvas.saveState(); + } + + moveLayerDown() { + if (this.canvas.selectedLayers.length === 0) return; + const selectedIndicesSet = new Set(this.canvas.selectedLayers.map(layer => this.canvas.layers.indexOf(layer))); + + const sortedIndices = Array.from(selectedIndicesSet).sort((a, b) => a - b); + + sortedIndices.forEach(index => { + const targetIndex = index - 1; + + if (targetIndex >= 0 && !selectedIndicesSet.has(targetIndex)) { + [this.canvas.layers[index], this.canvas.layers[targetIndex]] = [this.canvas.layers[targetIndex], this.canvas.layers[index]]; + } + }); + this.canvas.layers.forEach((layer, i) => layer.zIndex = i); + this.canvas.render(); + this.canvas.saveState(); + } + + getLayerAtPosition(worldX, worldY) { + for (let i = this.canvas.layers.length - 1; i >= 0; i--) { + const layer = this.canvas.layers[i]; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + 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); + + if (Math.abs(rotatedX) <= layer.width / 2 && Math.abs(rotatedY) <= layer.height / 2) { + const localX = rotatedX + layer.width / 2; + const localY = rotatedY + layer.height / 2; + + return { + layer: layer, + localX: localX, + localY: localY + }; + } + } + return null; + } + + resizeLayer(scale) { + this.canvas.selectedLayers.forEach(layer => { + layer.width *= scale; + layer.height *= scale; + }); + this.canvas.render(); + this.canvas.saveState(); + } + + rotateLayer(angle) { + this.canvas.selectedLayers.forEach(layer => { + layer.rotation += angle; + }); + this.canvas.render(); + this.canvas.saveState(); + } + + async mirrorHorizontal() { + if (this.canvas.selectedLayers.length === 0) return; + + const promises = this.canvas.selectedLayers.map(layer => { + return new Promise(resolve => { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = layer.image.width; + tempCanvas.height = layer.image.height; + + tempCtx.translate(tempCanvas.width, 0); + tempCtx.scale(-1, 1); + tempCtx.drawImage(layer.image, 0, 0); + + const newImage = new Image(); + newImage.onload = () => { + layer.image = newImage; + resolve(); + }; + newImage.src = tempCanvas.toDataURL(); + }); + }); + + await Promise.all(promises); + this.canvas.render(); + this.canvas.saveState(); + } + + async mirrorVertical() { + if (this.canvas.selectedLayers.length === 0) return; + + const promises = this.canvas.selectedLayers.map(layer => { + return new Promise(resolve => { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = layer.image.width; + tempCanvas.height = layer.image.height; + + tempCtx.translate(0, tempCanvas.height); + tempCtx.scale(1, -1); + tempCtx.drawImage(layer.image, 0, 0); + + const newImage = new Image(); + newImage.onload = () => { + layer.image = newImage; + resolve(); + }; + newImage.src = tempCanvas.toDataURL(); + }); + }); + + await Promise.all(promises); + this.canvas.render(); + this.canvas.saveState(); + } + + async getLayerImageData(layer) { + try { + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + + tempCanvas.width = layer.width; + tempCanvas.height = layer.height; + + tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); + + tempCtx.save(); + tempCtx.translate(layer.width / 2, layer.height / 2); + tempCtx.rotate(layer.rotation * Math.PI / 180); + tempCtx.drawImage( + layer.image, + -layer.width / 2, + -layer.height / 2, + layer.width, + layer.height + ); + tempCtx.restore(); + + const dataUrl = tempCanvas.toDataURL('image/png'); + if (!dataUrl.startsWith('data:image/png;base64,')) { + throw new Error("Invalid image data format"); + } + + return dataUrl; + } catch (error) { + log.error("Error getting layer image data:", error); + throw error; + } + } + + snapToGrid(value, gridSize = 64) { + return Math.round(value / gridSize) * gridSize; + } + + getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) { + if (!layer) { + return {dx: 0, dy: 0}; + } + + const layerEdges = { + left: layer.x, + right: layer.x + layer.width, + top: layer.y, + bottom: layer.y + layer.height + }; + const x_adjustments = [ + {type: 'x', delta: this.snapToGrid(layerEdges.left, gridSize) - layerEdges.left}, + {type: 'x', delta: this.snapToGrid(layerEdges.right, gridSize) - layerEdges.right} + ]; + + const y_adjustments = [ + {type: 'y', delta: this.snapToGrid(layerEdges.top, gridSize) - layerEdges.top}, + {type: 'y', delta: this.snapToGrid(layerEdges.bottom, gridSize) - layerEdges.bottom} + ]; + x_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta)); + y_adjustments.forEach(adj => adj.abs = Math.abs(adj.delta)); + const bestXSnap = x_adjustments + .filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9) + .sort((a, b) => a.abs - b.abs)[0]; + const bestYSnap = y_adjustments + .filter(adj => adj.abs < snapThreshold && adj.abs > 1e-9) + .sort((a, b) => a.abs - b.abs)[0]; + return { + dx: bestXSnap ? bestXSnap.delta : 0, + dy: bestYSnap ? bestYSnap.delta : 0 + }; + } + + updateCanvasSize(width, height, saveHistory = true) { + if (saveHistory) { + this.canvas.saveState(); + } + this.canvas.width = width; + this.canvas.height = height; + this.canvas.maskTool.resize(width, height); + + this.canvas.canvas.width = width; + this.canvas.canvas.height = height; + + this.canvas.render(); + + if (saveHistory) { + this.canvas.saveStateToDB(); + } + } + + addMattedLayer(image, mask) { + const layer = { + image: image, + mask: mask, + x: 0, + y: 0, + width: image.width, + height: image.height, + rotation: 0, + zIndex: this.canvas.layers.length + }; + + this.canvas.layers.push(layer); + this.canvas.selectedLayer = layer; + this.canvas.render(); + } + + // Funkcje pomocnicze dla transformacji warstw + isRotationHandle(x, y) { + if (!this.canvas.selectedLayer) return false; + + const handleX = this.canvas.selectedLayer.x + this.canvas.selectedLayer.width / 2; + const handleY = this.canvas.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 {}; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + const rad = layer.rotation * Math.PI / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + const halfW = layer.width / 2; + const halfH = layer.height / 2; + const localHandles = { + 'n': {x: 0, y: -halfH}, + 'ne': {x: halfW, y: -halfH}, + 'e': {x: halfW, y: 0}, + 'se': {x: halfW, y: halfH}, + 's': {x: 0, y: halfH}, + 'sw': {x: -halfW, y: halfH}, + 'w': {x: -halfW, y: 0}, + 'nw': {x: -halfW, y: -halfH}, + 'rot': {x: 0, y: -halfH - 20 / this.canvas.viewport.zoom} + }; + + const worldHandles = {}; + for (const key in localHandles) { + const p = localHandles[key]; + worldHandles[key] = { + x: centerX + (p.x * cos - p.y * sin), + y: centerY + (p.x * sin + p.y * cos) + }; + } + return worldHandles; + } + + getHandleAtPosition(worldX, worldY) { + if (this.canvas.selectedLayers.length === 0) return null; + + const handleRadius = 8 / this.canvas.viewport.zoom; + for (let i = this.canvas.selectedLayers.length - 1; i >= 0; i--) { + const layer = this.canvas.selectedLayers[i]; + const handles = this.getHandles(layer); + + for (const key in handles) { + const handlePos = handles[key]; + const dx = worldX - handlePos.x; + const dy = worldY - handlePos.y; + if (dx * dx + dy * dy <= handleRadius * handleRadius) { + return {layer: layer, handle: key}; + } + } + } + return null; + } + + getResizeHandle(x, y) { + if (!this.canvas.selectedLayer) return null; + + const handleRadius = 5; + const handles = { + 'nw': {x: this.canvas.selectedLayer.x, y: this.canvas.selectedLayer.y}, + 'ne': {x: this.canvas.selectedLayer.x + this.canvas.selectedLayer.width, y: this.canvas.selectedLayer.y}, + 'se': { + x: this.canvas.selectedLayer.x + this.canvas.selectedLayer.width, + y: this.canvas.selectedLayer.y + this.canvas.selectedLayer.height + }, + 'sw': {x: this.canvas.selectedLayer.x, y: this.canvas.selectedLayer.y + this.canvas.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; + } + + worldToLocal(worldX, worldY, layerProps) { + const dx = worldX - layerProps.centerX; + const dy = worldY - layerProps.centerY; + const rad = -layerProps.rotation * Math.PI / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + return { + x: dx * cos - dy * sin, + y: dx * sin + dy * cos + }; + } + + localToWorld(localX, localY, layerProps) { + const rad = layerProps.rotation * Math.PI / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + return { + x: layerProps.centerX + localX * cos - localY * sin, + y: layerProps.centerY + localX * sin + localY * cos + }; + } + + // Funkcje związane z blend mode i opacity + showBlendModeMenu(x, y) { + const existingMenu = document.getElementById('blend-mode-menu'); + if (existingMenu) { + document.body.removeChild(existingMenu); + } + + const menu = document.createElement('div'); + menu.id = 'blend-mode-menu'; + menu.style.cssText = ` + position: fixed; + left: ${x}px; + top: ${y}px; + background: #2a2a2a; + border: 1px solid #3a3a3a; + border-radius: 4px; + padding: 5px; + z-index: 1000; + box-shadow: 0 2px 10px rgba(0,0,0,0.3); + `; + + this.blendModes.forEach(mode => { + const container = document.createElement('div'); + container.className = 'blend-mode-container'; + container.style.cssText = ` + margin-bottom: 5px; + `; + + const option = document.createElement('div'); + option.style.cssText = ` + padding: 5px 10px; + color: white; + cursor: pointer; + transition: background-color 0.2s; + `; + option.textContent = `${mode.label} (${mode.name})`; + + const slider = document.createElement('input'); + slider.type = 'range'; + slider.min = '0'; + slider.max = '100'; + + slider.value = this.canvas.selectedLayer.opacity ? Math.round(this.canvas.selectedLayer.opacity * 100) : 100; + slider.style.cssText = ` + width: 100%; + margin: 5px 0; + display: none; + `; + + if (this.canvas.selectedLayer.blendMode === mode.name) { + slider.style.display = 'block'; + option.style.backgroundColor = '#3a3a3a'; + } + + option.onclick = () => { + menu.querySelectorAll('input[type="range"]').forEach(s => { + s.style.display = 'none'; + }); + menu.querySelectorAll('.blend-mode-container div').forEach(d => { + d.style.backgroundColor = ''; + }); + + slider.style.display = 'block'; + option.style.backgroundColor = '#3a3a3a'; + + if (this.canvas.selectedLayer) { + this.canvas.selectedLayer.blendMode = mode.name; + this.canvas.render(); + } + }; + + slider.addEventListener('input', () => { + if (this.canvas.selectedLayer) { + this.canvas.selectedLayer.opacity = slider.value / 100; + this.canvas.render(); + } + }); + + slider.addEventListener('change', async () => { + if (this.canvas.selectedLayer) { + this.canvas.selectedLayer.opacity = slider.value / 100; + this.canvas.render(); + + await this.canvas.saveToServer(this.canvas.widget.value); + if (this.canvas.node) { + app.graph.runStep(); + } + } + }); + + container.appendChild(option); + container.appendChild(slider); + menu.appendChild(container); + }); + + document.body.appendChild(menu); + + const closeMenu = (e) => { + if (!menu.contains(e.target)) { + document.body.removeChild(menu); + document.removeEventListener('mousedown', closeMenu); + } + }; + setTimeout(() => { + document.addEventListener('mousedown', closeMenu); + }, 0); + } + + closeBlendModeMenu() { + const menu = document.getElementById('blend-mode-menu'); + if (menu) { + document.body.removeChild(menu); + } + } + + 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'; + slider.min = '0'; + slider.max = '100'; + slider.value = this.blendOpacity; + slider.className = 'blend-opacity-slider'; + + slider.addEventListener('input', (e) => { + this.blendOpacity = parseInt(e.target.value); + }); + + const modeElement = document.querySelector(`[data-blend-mode="${mode}"]`); + if (modeElement) { + modeElement.appendChild(slider); + } + } + + applyBlendMode(mode, opacity) { + this.currentLayer.style.mixBlendMode = mode; + this.currentLayer.style.opacity = opacity / 100; + + this.selectedBlendMode = null; + this.isAdjustingOpacity = false; + } + + // Funkcje do generowania blob z canvasu + async getFlattenedCanvasAsBlob() { + return new Promise((resolve, reject) => { + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.canvas.width; + tempCanvas.height = this.canvas.height; + const tempCtx = tempCanvas.getContext('2d'); + + const sortedLayers = [...this.canvas.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(); + }); + + tempCanvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error('Canvas toBlob failed.')); + } + }, 'image/png'); + }); + } + + // Funkcja do generowania blob z zaznaczonych warstw + async getFlattenedSelectionAsBlob() { + if (this.canvas.selectedLayers.length === 0) { + return null; + } + + return new Promise((resolve) => { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + this.canvas.selectedLayers.forEach(layer => { + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + const rad = layer.rotation * Math.PI / 180; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + const halfW = layer.width / 2; + const halfH = layer.height / 2; + + const corners = [ + {x: -halfW, y: -halfH}, + {x: halfW, y: -halfH}, + {x: halfW, y: halfH}, + {x: -halfW, y: halfH} + ]; + + corners.forEach(p => { + const worldX = centerX + (p.x * cos - p.y * sin); + const worldY = centerY + (p.x * sin + p.y * cos); + + minX = Math.min(minX, worldX); + minY = Math.min(minY, worldY); + maxX = Math.max(maxX, worldX); + maxY = Math.max(maxY, worldY); + }); + }); + + const newWidth = Math.ceil(maxX - minX); + const newHeight = Math.ceil(maxY - minY); + + if (newWidth <= 0 || newHeight <= 0) { + resolve(null); + return; + } + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; + const tempCtx = tempCanvas.getContext('2d'); + + tempCtx.translate(-minX, -minY); + + const sortedSelection = [...this.canvas.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); + + sortedSelection.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(); + }); + tempCanvas.toBlob((blob) => { + resolve(blob); + }, 'image/png'); + }); + } +} \ No newline at end of file