mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-24 14:02:11 -03:00
Refactor layer operations into CanvasLayers module
Moved all layer-related logic from Canvas.js to a new CanvasLayers.js module, including blend modes, clipboard operations, transformations, and utility functions. Canvas.js now delegates these operations to CanvasLayers, improving code organization and maintainability.
This commit is contained in:
705
js/Canvas.js
705
js/Canvas.js
@@ -2,6 +2,7 @@ import {saveImage, getImage, removeImage} from "./db.js";
|
|||||||
import {MaskTool} from "./Mask_tool.js";
|
import {MaskTool} from "./Mask_tool.js";
|
||||||
import {CanvasState} from "./CanvasState.js";
|
import {CanvasState} from "./CanvasState.js";
|
||||||
import {CanvasInteractions} from "./CanvasInteractions.js";
|
import {CanvasInteractions} from "./CanvasInteractions.js";
|
||||||
|
import {CanvasLayers} from "./CanvasLayers.js";
|
||||||
import {logger, LogLevel} from "./logger.js";
|
import {logger, LogLevel} from "./logger.js";
|
||||||
|
|
||||||
// Inicjalizacja loggera dla modułu Canvas
|
// Inicjalizacja loggera dla modułu Canvas
|
||||||
@@ -53,6 +54,7 @@ export class Canvas {
|
|||||||
this.initCanvas();
|
this.initCanvas();
|
||||||
this.canvasState = new CanvasState(this); // Nowy moduł zarządzania stanem
|
this.canvasState = new CanvasState(this); // Nowy moduł zarządzania stanem
|
||||||
this.canvasInteractions = new CanvasInteractions(this); // Nowy moduł obsługi interakcji
|
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
|
// Po utworzeniu CanvasInteractions, użyj jego interaction state
|
||||||
this.interaction = this.canvasInteractions.interaction;
|
this.interaction = this.canvasInteractions.interaction;
|
||||||
@@ -60,23 +62,11 @@ export class Canvas {
|
|||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.initNodeData();
|
this.initNodeData();
|
||||||
|
|
||||||
this.blendModes = [
|
// Przeniesione do CanvasLayers
|
||||||
{name: '.', label: 'Normal'},
|
this.blendModes = this.canvasLayers.blendModes;
|
||||||
{name: '.', label: 'Multiply'},
|
this.selectedBlendMode = this.canvasLayers.selectedBlendMode;
|
||||||
{name: '.', label: 'Screen'},
|
this.blendOpacity = this.canvasLayers.blendOpacity;
|
||||||
{name: '.', label: 'Overlay'},
|
this.isAdjustingOpacity = this.canvasLayers.isAdjustingOpacity;
|
||||||
{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;
|
|
||||||
|
|
||||||
this.layers = this.layers.map(layer => ({
|
this.layers = this.layers.map(layer => ({
|
||||||
...layer,
|
...layer,
|
||||||
@@ -168,86 +158,17 @@ export class Canvas {
|
|||||||
// Interaction methods moved to CanvasInteractions module
|
// Interaction methods moved to CanvasInteractions module
|
||||||
|
|
||||||
|
|
||||||
|
// Delegacja metod operacji na warstwach do CanvasLayers
|
||||||
async copySelectedLayers() {
|
async copySelectedLayers() {
|
||||||
if (this.selectedLayers.length === 0) return;
|
return this.canvasLayers.copySelectedLayers();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pasteLayers() {
|
pasteLayers() {
|
||||||
if (this.internalClipboard.length === 0) return;
|
return this.canvasLayers.pasteLayers();
|
||||||
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).`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async handlePaste() {
|
async handlePaste() {
|
||||||
try {
|
return this.canvasLayers.handlePaste();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -289,56 +210,15 @@ export class Canvas {
|
|||||||
|
|
||||||
|
|
||||||
isRotationHandle(x, y) {
|
isRotationHandle(x, y) {
|
||||||
if (!this.selectedLayer) return false;
|
return this.canvasLayers.isRotationHandle(x, y);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addLayerWithImage(image, layerProps = {}) {
|
async addLayerWithImage(image, layerProps = {}) {
|
||||||
try {
|
return this.canvasLayers.addLayerWithImage(image, layerProps);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUUID() {
|
generateUUID() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return this.canvasLayers.generateUUID();
|
||||||
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addLayer(image) {
|
async addLayer(image) {
|
||||||
@@ -381,50 +261,15 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
snapToGrid(value, gridSize = 64) {
|
snapToGrid(value, gridSize = 64) {
|
||||||
return Math.round(value / gridSize) * gridSize;
|
return this.canvasLayers.snapToGrid(value, gridSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) {
|
getSnapAdjustment(layer, gridSize = 64, snapThreshold = 10) {
|
||||||
if (!layer) {
|
return this.canvasLayers.getSnapAdjustment(layer, gridSize, snapThreshold);
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayer(fromIndex, toIndex) {
|
moveLayer(fromIndex, toIndex) {
|
||||||
if (fromIndex >= 0 && fromIndex < this.layers.length &&
|
return this.canvasLayers.moveLayer(fromIndex, toIndex);
|
||||||
toIndex >= 0 && toIndex < this.layers.length) {
|
|
||||||
const layer = this.layers.splice(fromIndex, 1)[0];
|
|
||||||
this.layers.splice(toIndex, 0, layer);
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeLayer(scale) {
|
resizeLayer(scale) {
|
||||||
@@ -445,21 +290,7 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCanvasSize(width, height, saveHistory = true) {
|
updateCanvasSize(width, height, saveHistory = true) {
|
||||||
if (saveHistory) {
|
return this.canvasLayers.updateCanvasSize(width, height, 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -756,81 +587,19 @@ export class Canvas {
|
|||||||
|
|
||||||
|
|
||||||
getHandles(layer) {
|
getHandles(layer) {
|
||||||
if (!layer) return {};
|
return this.canvasLayers.getHandles(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 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHandleAtPosition(worldX, worldY) {
|
getHandleAtPosition(worldX, worldY) {
|
||||||
if (this.selectedLayers.length === 0) return null;
|
return this.canvasLayers.getHandleAtPosition(worldX, worldY);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
worldToLocal(worldX, worldY, layerProps) {
|
worldToLocal(worldX, worldY, layerProps) {
|
||||||
const dx = worldX - layerProps.centerX;
|
return this.canvasLayers.worldToLocal(worldX, worldY, layerProps);
|
||||||
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) {
|
localToWorld(localX, localY, layerProps) {
|
||||||
const rad = layerProps.rotation * Math.PI / 180;
|
return this.canvasLayers.localToWorld(localX, localY, layerProps);
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1059,314 +828,45 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFlattenedCanvasAsBlob() {
|
async getFlattenedCanvasAsBlob() {
|
||||||
return new Promise((resolve, reject) => {
|
return this.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getFlattenedSelectionAsBlob() {
|
async getFlattenedSelectionAsBlob() {
|
||||||
if (this.selectedLayers.length === 0) {
|
return this.canvasLayers.getFlattenedSelectionAsBlob();
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayerUp() {
|
moveLayerUp() {
|
||||||
if (this.selectedLayers.length === 0) return;
|
return this.canvasLayers.moveLayerUp();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayerDown() {
|
moveLayerDown() {
|
||||||
if (this.selectedLayers.length === 0) return;
|
return this.canvasLayers.moveLayerDown();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getLayerAtPosition(worldX, worldY) {
|
getLayerAtPosition(worldX, worldY) {
|
||||||
|
return this.canvasLayers.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getResizeHandle(x, y) {
|
getResizeHandle(x, y) {
|
||||||
if (!this.selectedLayer) return null;
|
return this.canvasLayers.getResizeHandle(x, y);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async mirrorHorizontal() {
|
async mirrorHorizontal() {
|
||||||
if (this.selectedLayers.length === 0) return;
|
return this.canvasLayers.mirrorHorizontal();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async mirrorVertical() {
|
async mirrorVertical() {
|
||||||
if (this.selectedLayers.length === 0) return;
|
return this.canvasLayers.mirrorVertical();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLayerImageData(layer) {
|
async getLayerImageData(layer) {
|
||||||
try {
|
return this.canvasLayers.getLayerImageData(layer);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addMattedLayer(image, mask) {
|
addMattedLayer(image, mask) {
|
||||||
const layer = {
|
return this.canvasLayers.addMattedLayer(image, mask);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processInputData(nodeData) {
|
processInputData(nodeData) {
|
||||||
@@ -1811,151 +1311,18 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showBlendModeMenu(x, y) {
|
showBlendModeMenu(x, y) {
|
||||||
|
return this.canvasLayers.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlendModeSelection(mode) {
|
handleBlendModeSelection(mode) {
|
||||||
if (this.selectedBlendMode === mode && !this.isAdjustingOpacity) {
|
return this.canvasLayers.handleBlendModeSelection(mode);
|
||||||
this.applyBlendMode(mode, this.blendOpacity);
|
|
||||||
this.closeBlendModeMenu();
|
|
||||||
} else {
|
|
||||||
this.selectedBlendMode = mode;
|
|
||||||
this.isAdjustingOpacity = true;
|
|
||||||
this.showOpacitySlider(mode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showOpacitySlider(mode) {
|
showOpacitySlider(mode) {
|
||||||
|
return this.canvasLayers.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) {
|
applyBlendMode(mode, opacity) {
|
||||||
|
return this.canvasLayers.applyBlendMode(mode, opacity);
|
||||||
this.currentLayer.style.mixBlendMode = mode;
|
|
||||||
this.currentLayer.style.opacity = opacity / 100;
|
|
||||||
|
|
||||||
this.selectedBlendMode = null;
|
|
||||||
this.isAdjustingOpacity = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
816
js/CanvasLayers.js
Normal file
816
js/CanvasLayers.js
Normal file
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user