Files
Comfyui-LayerForge/js/Canvas.js
2025-06-20 19:30:29 +02:00

1929 lines
66 KiB
JavaScript

export class Canvas {
constructor(node, widget) {
this.node = node;
this.widget = widget;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.width = 512;
this.height = 512;
this.layers = [];
this.selectedLayer = null;
this.selectedLayers = [];
this.viewport = {
x: -(this.width / 4),
y: -(this.height / 4),
zoom: 0.8,
};
this.interaction = {
mode: 'none', // 'none', 'panning', 'dragging', 'resizing', 'rotating', 'resizingCanvas'
panStart: {x: 0, y: 0},
dragStart: {x: 0, y: 0},
transformOrigin: {},
resizeHandle: null,
resizeAnchor: {x: 0, y: 0},
canvasResizeStart: {x: 0, y: 0},
isCtrlPressed: false,
lastClickTime: 0,
};
this.originalLayerPositions = new Map();
this.canvasResizeRect = null;
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
alpha: false
});
this.renderAnimationFrame = null;
this.lastRenderTime = 0;
this.renderInterval = 1000 / 60;
this.isDirty = false;
this.dataInitialized = false;
this.pendingDataCheck = null;
this.initCanvas();
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;
this.layers = this.layers.map(layer => ({
...layer,
opacity: 1
}));
}
initCanvas() {
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.style.border = '1px solid black';
this.canvas.style.maxWidth = '100%';
this.canvas.style.backgroundColor = '#606060';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
}
setupEventListeners() {
// Używamy .bind(this), aby upewnić się, że 'this' wewnątrz handlerów odnosi się do instancji klasy Canvas
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
this.canvas.addEventListener('wheel', this.handleWheel.bind(this), {passive: false});
document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this));
}
/**
* Resetuje stan interakcji do wartości domyślnych.
*/
resetInteractionState() {
this.interaction.mode = 'none';
this.interaction.resizeHandle = null;
this.originalLayerPositions.clear();
this.canvasResizeRect = null;
this.canvas.style.cursor = 'default';
}
/**
* Główna metoda obsługująca wciśnięcie przycisku myszy.
*/
handleMouseDown(e) {
const currentTime = Date.now();
const worldCoords = this.getMouseWorldCoordinates(e);
// Deselekcja po szybkim kliknięciu (pseudo-double-click)
if (currentTime - this.interaction.lastClickTime < 300) {
this.selectedLayers = [];
this.selectedLayer = null;
this.resetInteractionState();
this.render();
return;
}
this.interaction.lastClickTime = currentTime;
const handle = this.getHandleAtPosition(worldCoords.x, worldCoords.y);
// 1. Interakcja z uchwytem (skalowanie/rotacja)
if (this.selectedLayer && handle) {
this.startLayerTransform(handle, worldCoords);
return;
}
const clickedLayerResult = this.getLayerAtPosition(worldCoords.x, worldCoords.y);
// 2. Interakcja z warstwą (przesuwanie/selekcja)
if (clickedLayerResult) {
if (e.shiftKey && this.selectedLayers.includes(clickedLayerResult.layer)) {
this.showBlendModeMenu(e.clientX, e.clientY);
return;
}
this.startLayerDrag(clickedLayerResult.layer, worldCoords);
return;
}
// 3. Interakcja z tłem (zmiana rozmiaru canvasu lub panoramowanie)
if (e.shiftKey) {
this.startCanvasResize(worldCoords);
} else {
this.startPanning(e);
}
this.render();
}
/**
* Główna metoda obsługująca ruch myszy.
*/
handleMouseMove(e) {
const worldCoords = this.getMouseWorldCoordinates(e);
switch (this.interaction.mode) {
case 'panning':
this.panViewport(e);
break;
case 'dragging':
this.dragLayers(worldCoords);
break;
case 'resizing':
this.resizeLayerFromHandle(worldCoords, e.shiftKey);
break;
case 'rotating':
this.rotateLayerFromHandle(worldCoords, e.shiftKey);
break;
case 'resizingCanvas':
this.updateCanvasResize(worldCoords);
break;
default:
this.updateCursor(worldCoords);
break;
}
}
/**
* Metoda obsługująca puszczenie przycisku myszy.
*/
handleMouseUp(e) {
if (this.interaction.mode === 'resizingCanvas') {
this.finalizeCanvasResize();
}
this.resetInteractionState();
this.render();
}
/**
* Metoda obsługująca opuszczenie obszaru canvas przez kursor.
*/
handleMouseLeave(e) {
if (this.interaction.mode !== 'none') {
this.resetInteractionState();
this.render();
}
}
/**
* Metoda obsługująca kółko myszy (zoom / skalowanie / rotacja warstwy).
*/
handleWheel(e) {
e.preventDefault();
if (this.selectedLayer) {
const scaleFactor = e.deltaY > 0 ? 0.95 : 1.05;
const rotationStep = 5 * (e.deltaY > 0 ? -1 : 1);
this.selectedLayers.forEach(layer => {
if (e.shiftKey) { // Rotacja
layer.rotation += rotationStep;
} else { // Skalowanie
const oldWidth = layer.width;
const oldHeight = layer.height;
layer.width *= scaleFactor;
layer.height *= scaleFactor;
layer.x += (oldWidth - layer.width) / 2;
layer.y += (oldHeight - layer.height) / 2;
}
});
} else { // Zoom widoku
const worldCoords = this.getMouseWorldCoordinates(e);
const rect = this.canvas.getBoundingClientRect();
const mouseBufferX = (e.clientX - rect.left) * (this.offscreenCanvas.width / rect.width);
const mouseBufferY = (e.clientY - rect.top) * (this.offscreenCanvas.height / rect.height);
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
const newZoom = this.viewport.zoom * zoomFactor;
this.viewport.zoom = Math.max(0.1, Math.min(10, newZoom));
this.viewport.x = worldCoords.x - (mouseBufferX / this.viewport.zoom);
this.viewport.y = worldCoords.y - (mouseBufferY / this.viewport.zoom);
}
this.render();
}
/**
* Metoda obsługująca wciśnięcie klawisza.
*/
handleKeyDown(e) {
if (e.key === 'Control') this.interaction.isCtrlPressed = true;
if (e.key === 'Alt') {
e.preventDefault();
}
if (this.selectedLayer) {
if (e.key === 'Delete') {
const index = this.layers.indexOf(this.selectedLayer);
this.removeLayer(index);
return;
}
const step = e.shiftKey ? 10 : 1;
let needsRender = false;
switch (e.key) {
case 'ArrowLeft':
this.selectedLayers.forEach(l => l.x -= step);
needsRender = true;
break;
case 'ArrowRight':
this.selectedLayers.forEach(l => l.x += step);
needsRender = true;
break;
case 'ArrowUp':
this.selectedLayers.forEach(l => l.y -= step);
needsRender = true;
break;
case 'ArrowDown':
this.selectedLayers.forEach(l => l.y += step);
needsRender = true;
break;
case '[':
this.selectedLayers.forEach(l => l.rotation -= step);
needsRender = true;
break;
case ']':
this.selectedLayers.forEach(l => l.rotation += step);
needsRender = true;
break;
}
if (needsRender) {
e.preventDefault();
this.render();
}
}
}
/**
* Metoda obsługująca puszczenie klawisza.
*/
handleKeyUp(e) {
if (e.key === 'Control') this.interaction.isCtrlPressed = false;
}
updateCursor(worldCoords) {
const handle = this.getHandleAtPosition(worldCoords.x, worldCoords.y);
if (handle) {
const cursorMap = {
'n': 'ns-resize', 's': 'ns-resize', 'e': 'ew-resize', 'w': 'ew-resize',
'nw': 'nwse-resize', 'se': 'nwse-resize', 'ne': 'nesw-resize', 'sw': 'nesw-resize',
'rot': 'grab'
};
this.canvas.style.cursor = cursorMap[handle];
} else if (this.getLayerAtPosition(worldCoords.x, worldCoords.y)) {
this.canvas.style.cursor = 'move';
} else {
this.canvas.style.cursor = 'default';
}
}
startLayerTransform(handle, worldCoords) {
const layer = this.selectedLayer;
this.interaction.transformOrigin = {
x: layer.x, y: layer.y,
width: layer.width, height: layer.height,
rotation: layer.rotation,
centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2
};
this.interaction.dragStart = {...worldCoords};
if (handle === 'rot') {
this.interaction.mode = 'rotating';
} else {
this.interaction.mode = 'resizing';
this.interaction.resizeHandle = handle;
const handles = this.getHandles(layer);
const oppositeHandleKey = {
'n': 's', 's': 'n', 'e': 'w', 'w': 'e',
'nw': 'se', 'se': 'nw', 'ne': 'sw', 'sw': 'ne'
}[handle];
this.interaction.resizeAnchor = handles[oppositeHandleKey];
}
this.render();
}
startLayerDrag(layer, worldCoords) {
this.interaction.mode = 'dragging';
this.interaction.dragStart = {...worldCoords};
if (this.interaction.isCtrlPressed) {
const index = this.selectedLayers.indexOf(layer);
if (index === -1) this.selectedLayers.push(layer);
else this.selectedLayers.splice(index, 1);
} else {
if (!this.selectedLayers.includes(layer)) {
this.selectedLayers = [layer];
}
}
this.selectedLayer = this.selectedLayers.length > 0 ? this.selectedLayers[this.selectedLayers.length - 1] : null;
this.originalLayerPositions.clear();
this.selectedLayers.forEach(l => {
this.originalLayerPositions.set(l, {x: l.x, y: l.y});
});
}
startCanvasResize(worldCoords) {
this.interaction.mode = 'resizingCanvas';
const startX = this.snapToGrid(worldCoords.x);
const startY = this.snapToGrid(worldCoords.y);
this.interaction.canvasResizeStart = {x: startX, y: startY};
this.canvasResizeRect = {x: startX, y: startY, width: 0, height: 0};
this.render();
}
startPanning(e) {
if (!this.interaction.isCtrlPressed) {
this.selectedLayers = [];
this.selectedLayer = null;
}
this.interaction.mode = 'panning';
this.interaction.panStart = {x: e.clientX, y: e.clientY};
}
panViewport(e) {
const dx = e.clientX - this.interaction.panStart.x;
const dy = e.clientY - this.interaction.panStart.y;
this.viewport.x -= dx / this.viewport.zoom;
this.viewport.y -= dy / this.viewport.zoom;
this.interaction.panStart = {x: e.clientX, y: e.clientY};
this.render();
}
dragLayers(worldCoords) {
const totalDx = worldCoords.x - this.interaction.dragStart.x;
const totalDy = worldCoords.y - this.interaction.dragStart.y;
let finalDx = totalDx, finalDy = totalDy;
if (this.interaction.isCtrlPressed && this.selectedLayer) {
const originalPos = this.originalLayerPositions.get(this.selectedLayer);
if (originalPos) {
const tempLayerForSnap = {
...this.selectedLayer,
x: originalPos.x + totalDx,
y: originalPos.y + totalDy
};
const snapAdjustment = this.getSnapAdjustment(tempLayerForSnap);
finalDx += snapAdjustment.dx;
finalDy += snapAdjustment.dy;
}
}
this.selectedLayers.forEach(layer => {
const originalPos = this.originalLayerPositions.get(layer);
if (originalPos) {
layer.x = originalPos.x + finalDx;
layer.y = originalPos.y + finalDy;
}
});
this.render();
}
resizeLayerFromHandle(worldCoords, isShiftPressed) {
let mouseX = worldCoords.x;
let mouseY = worldCoords.y;
if (this.interaction.isCtrlPressed) {
const snapThreshold = 10 / this.viewport.zoom;
const snappedMouseX = this.snapToGrid(mouseX);
if (Math.abs(mouseX - snappedMouseX) < snapThreshold) mouseX = snappedMouseX;
const snappedMouseY = this.snapToGrid(mouseY);
if (Math.abs(mouseY - snappedMouseY) < snapThreshold) mouseY = snappedMouseY;
}
const layer = this.selectedLayer;
const o = this.interaction.transformOrigin;
const handle = this.interaction.resizeHandle;
const anchor = this.interaction.resizeAnchor;
const rad = o.rotation * Math.PI / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const vecX = mouseX - anchor.x;
const vecY = mouseY - anchor.y;
let newWidth = vecX * cos + vecY * sin;
let newHeight = vecY * cos - vecX * sin;
let signX = handle.includes('e') ? 1 : (handle.includes('w') ? -1 : 0);
let signY = handle.includes('s') ? 1 : (handle.includes('n') ? -1 : 0);
newWidth *= signX;
newHeight *= signY;
if (signX === 0) newWidth = o.width;
if (signY === 0) newHeight = o.height;
if (newWidth < 10) newWidth = 10;
if (newHeight < 10) newHeight = 10;
layer.width = newWidth;
layer.height = newHeight;
const deltaW = newWidth - o.width;
const deltaH = newHeight - o.height;
const shiftX = (deltaW / 2) * signX;
const shiftY = (deltaH / 2) * signY;
const worldShiftX = shiftX * cos - shiftY * sin;
const worldShiftY = shiftX * sin + shiftY * cos;
const newCenterX = o.centerX + worldShiftX;
const newCenterY = o.centerY + worldShiftY;
layer.x = newCenterX - layer.width / 2;
layer.y = newCenterY - layer.height / 2;
this.render();
}
rotateLayerFromHandle(worldCoords, isShiftPressed) {
const o = this.interaction.transformOrigin;
const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX);
const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX);
let angleDiff = (currentAngle - startAngle) * 180 / Math.PI;
let newRotation = o.rotation + angleDiff;
if (isShiftPressed) {
newRotation = Math.round(newRotation / 15) * 15;
}
this.selectedLayer.rotation = newRotation;
this.render();
}
updateCanvasResize(worldCoords) {
const snappedMouseX = this.snapToGrid(worldCoords.x);
const snappedMouseY = this.snapToGrid(worldCoords.y);
const start = this.interaction.canvasResizeStart;
this.canvasResizeRect.x = Math.min(snappedMouseX, start.x);
this.canvasResizeRect.y = Math.min(snappedMouseY, start.y);
this.canvasResizeRect.width = Math.abs(snappedMouseX - start.x);
this.canvasResizeRect.height = Math.abs(snappedMouseY - start.y);
this.render();
}
finalizeCanvasResize() {
if (this.canvasResizeRect && this.canvasResizeRect.width > 1 && this.canvasResizeRect.height > 1) {
const newWidth = Math.round(this.canvasResizeRect.width);
const newHeight = Math.round(this.canvasResizeRect.height);
const rectX = this.canvasResizeRect.x;
const rectY = this.canvasResizeRect.y;
this.updateCanvasSize(newWidth, newHeight);
this.layers.forEach(layer => {
layer.x -= rectX;
layer.y -= rectY;
});
this.viewport.x -= rectX;
this.viewport.y -= rectY;
}
}
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;
}
addLayer(image) {
try {
console.log("Adding layer with image:", image);
const layer = {
image: image,
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
};
this.layers.push(layer);
this.selectedLayer = layer;
this.render();
console.log("Layer added successfully");
} catch (error) {
console.error("Error adding layer:", error);
throw error;
}
}
removeLayer(index) {
if (index >= 0 && index < this.layers.length) {
this.layers.splice(index, 1);
this.selectedLayer = this.layers[this.layers.length - 1] || null;
this.render();
}
}
getMouseWorldCoordinates(e) {
const rect = this.canvas.getBoundingClientRect();
const mouseX_DOM = e.clientX - rect.left;
const mouseY_DOM = e.clientY - rect.top;
const scaleX = this.offscreenCanvas.width / rect.width;
const scaleY = this.offscreenCanvas.height / rect.height;
const mouseX_Buffer = mouseX_DOM * scaleX;
const mouseY_Buffer = mouseY_DOM * scaleY;
const worldX = (mouseX_Buffer / this.viewport.zoom) + this.viewport.x;
const worldY = (mouseY_Buffer / this.viewport.zoom) + this.viewport.y;
return {x: worldX, y: worldY};
}
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
};
}
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();
}
}
resizeLayer(scale) {
this.selectedLayers.forEach(layer => {
layer.width *= scale;
layer.height *= scale;
});
this.render();
}
rotateLayer(angle) {
this.selectedLayers.forEach(layer => {
layer.rotation += angle;
});
this.render();
}
updateCanvasSize(width, height) {
this.width = width;
this.height = height;
this.canvas.width = width;
this.canvas.height = height;
this.render();
}
render() {
if (this.renderAnimationFrame) {
this.isDirty = true;
return;
}
this.renderAnimationFrame = requestAnimationFrame(() => {
const now = performance.now();
if (now - this.lastRenderTime >= this.renderInterval) {
this.lastRenderTime = now;
this.actualRender();
this.isDirty = false;
}
if (this.isDirty) {
this.renderAnimationFrame = null;
this.render();
} else {
this.renderAnimationFrame = null;
}
});
}
actualRender() {
if (this.offscreenCanvas.width !== this.canvas.clientWidth ||
this.offscreenCanvas.height !== this.canvas.clientHeight) {
const newWidth = Math.max(1, this.canvas.clientWidth);
const newHeight = Math.max(1, this.canvas.clientHeight);
this.offscreenCanvas.width = newWidth;
this.offscreenCanvas.height = newHeight;
}
const ctx = this.offscreenCtx;
ctx.fillStyle = '#606060';
ctx.fillRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
ctx.save();
ctx.scale(this.viewport.zoom, this.viewport.zoom);
ctx.translate(-this.viewport.x, -this.viewport.y);
this.drawGrid(ctx);
this.drawCanvasOutline(ctx);
const sortedLayers = [...this.layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => {
if (!layer.image) return;
ctx.save();
const currentTransform = ctx.getTransform();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalCompositeOperation = layer.blendMode || 'normal';
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
ctx.setTransform(currentTransform);
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
ctx.translate(centerX, centerY);
ctx.rotate(layer.rotation * Math.PI / 180);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(
layer.image, -layer.width / 2, -layer.height / 2,
layer.width,
layer.height
);
if (layer.mask) { /* Logika maski */
}
if (this.selectedLayers.includes(layer)) {
this.drawSelectionFrame(ctx, layer);
}
ctx.restore();
});
if (this.interaction.mode === 'resizingCanvas' && this.canvasResizeRect) {
const rect = this.canvasResizeRect;
ctx.save();
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
ctx.lineWidth = 2 / this.viewport.zoom;
ctx.setLineDash([8 / this.viewport.zoom, 4 / this.viewport.zoom]);
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
ctx.setLineDash([]);
ctx.restore();
if (rect.width > 0 && rect.height > 0) {
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
const textWorldX = rect.x + rect.width / 2;
const textWorldY = rect.y + rect.height + (20 / this.viewport.zoom);
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom;
const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom;
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const textMetrics = ctx.measureText(text);
const bgWidth = textMetrics.width + 10;
const bgHeight = 22;
ctx.fillStyle = "rgba(0, 128, 0, 0.7)";
ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
ctx.fillStyle = "white";
ctx.fillText(text, screenX, screenY);
ctx.restore();
}
}
if (this.selectedLayer) {
this.selectedLayers.forEach(layer => {
if (!layer.image) return;
const currentWidth = Math.round(layer.width);
const currentHeight = Math.round(layer.height);
const rotation = Math.round(layer.rotation % 360);
const text = `${currentWidth}x${currentHeight} | ${rotation}°`;
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 localCorners = [
{x: -halfW, y: -halfH},
{x: halfW, y: -halfH},
{x: halfW, y: halfH},
{x: -halfW, y: halfH}
];
const worldCorners = localCorners.map(p => ({
x: centerX + p.x * cos - p.y * sin,
y: centerY + p.x * sin + p.y * cos
}));
let minX = Infinity, maxX = -Infinity, maxY = -Infinity;
worldCorners.forEach(p => {
minX = Math.min(minX, p.x);
maxX = Math.max(maxX, p.x);
maxY = Math.max(maxY, p.y);
});
const padding = 20 / this.viewport.zoom;
const textWorldX = (minX + maxX) / 2;
const textWorldY = maxY + padding;
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom;
const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom;
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const textMetrics = ctx.measureText(text);
const textBgWidth = textMetrics.width + 10;
const textBgHeight = 22;
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(screenX - textBgWidth / 2, screenY - textBgHeight / 2, textBgWidth, textBgHeight);
ctx.fillStyle = "white";
ctx.fillText(text, screenX, screenY);
ctx.restore();
});
}
ctx.restore();
if (this.canvas.width !== this.offscreenCanvas.width || this.canvas.height !== this.offscreenCanvas.height) {
this.canvas.width = this.offscreenCanvas.width;
this.canvas.height = this.offscreenCanvas.height;
}
this.ctx.drawImage(this.offscreenCanvas, 0, 0);
}
drawGrid(ctx) {
const gridSize = 64;
const lineWidth = 0.5 / this.viewport.zoom;
const viewLeft = this.viewport.x;
const viewTop = this.viewport.y;
const viewRight = this.viewport.x + this.offscreenCanvas.width / this.viewport.zoom;
const viewBottom = this.viewport.y + this.offscreenCanvas.height / this.viewport.zoom;
ctx.beginPath();
ctx.strokeStyle = '#707070';
ctx.lineWidth = lineWidth;
for (let x = Math.floor(viewLeft / gridSize) * gridSize; x < viewRight; x += gridSize) {
ctx.moveTo(x, viewTop);
ctx.lineTo(x, viewBottom);
}
for (let y = Math.floor(viewTop / gridSize) * gridSize; y < viewBottom; y += gridSize) {
ctx.moveTo(viewLeft, y);
ctx.lineTo(viewRight, y);
}
ctx.stroke();
}
drawCanvasOutline(ctx) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 2 / this.viewport.zoom;
ctx.setLineDash([10 / this.viewport.zoom, 5 / this.viewport.zoom]);
ctx.rect(0, 0, this.width, this.height);
ctx.stroke();
ctx.setLineDash([]);
}
drawSelectionFrame(ctx, layer) {
const lineWidth = 2 / this.viewport.zoom;
const handleRadius = 5 / this.viewport.zoom;
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.rect(-layer.width / 2, -layer.height / 2, layer.width, layer.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, -layer.height / 2);
ctx.lineTo(0, -layer.height / 2 - 20 / this.viewport.zoom);
ctx.stroke();
const handles = this.getHandles(layer);
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1 / this.viewport.zoom;
for (const key in handles) {
const point = handles[key];
ctx.beginPath();
const localX = point.x - (layer.x + layer.width / 2);
const localY = point.y - (layer.y + layer.height / 2);
const rad = -layer.rotation * Math.PI / 180;
const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad);
const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad);
ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
}
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;
}
getHandleAtPosition(worldX, worldY) {
if (!this.selectedLayer) return null;
const handles = this.getHandles(this.selectedLayer);
const handleRadius = 8 / this.viewport.zoom;
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 key;
}
}
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
};
}
async saveToServer(fileName) {
return new Promise((resolve) => {
const tempCanvas = document.createElement('canvas');
const maskCanvas = document.createElement('canvas');
tempCanvas.width = this.width;
tempCanvas.height = this.height;
maskCanvas.width = this.width;
maskCanvas.height = this.height;
const tempCtx = tempCanvas.getContext('2d');
const maskCtx = maskCanvas.getContext('2d');
tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, this.width, this.height);
maskCtx.fillStyle = '#000000';
maskCtx.fillRect(0, 0, this.width, this.height);
this.layers.sort((a, b) => a.zIndex - b.zIndex).forEach(layer => {
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
tempCtx.translate(layer.x + layer.width / 2, layer.y + 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();
maskCtx.save();
maskCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
maskCtx.rotate(layer.rotation * Math.PI / 180);
maskCtx.globalCompositeOperation = 'lighter';
if (layer.mask) {
maskCtx.drawImage(layer.mask, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
} else {
const layerCanvas = document.createElement('canvas');
layerCanvas.width = layer.width;
layerCanvas.height = layer.height;
const layerCtx = layerCanvas.getContext('2d');
layerCtx.drawImage(layer.image, 0, 0, layer.width, layer.height);
const imageData = layerCtx.getImageData(0, 0, layer.width, layer.height);
const alphaCanvas = document.createElement('canvas');
alphaCanvas.width = layer.width;
alphaCanvas.height = layer.height;
const alphaCtx = alphaCanvas.getContext('2d');
const alphaData = alphaCtx.createImageData(layer.width, layer.height);
for (let i = 0; i < imageData.data.length; i += 4) {
const alpha = imageData.data[i + 3] * (layer.opacity !== undefined ? layer.opacity : 1);
alphaData.data[i] = alphaData.data[i + 1] = alphaData.data[i + 2] = alpha;
alphaData.data[i + 3] = 255;
}
alphaCtx.putImageData(alphaData, 0, 0);
maskCtx.drawImage(alphaCanvas, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
}
maskCtx.restore();
});
const finalMaskData = maskCtx.getImageData(0, 0, this.width, this.height);
for (let i = 0; i < finalMaskData.data.length; i += 4) {
finalMaskData.data[i] =
finalMaskData.data[i + 1] =
finalMaskData.data[i + 2] = 255 - finalMaskData.data[i];
finalMaskData.data[i + 3] = 255;
}
maskCtx.putImageData(finalMaskData, 0, 0);
tempCanvas.toBlob(async (blob) => {
const formData = new FormData();
formData.append("image", blob, fileName);
formData.append("overwrite", "true");
try {
const resp = await fetch("/upload/image", {
method: "POST",
body: formData,
});
if (resp.status === 200) {
maskCanvas.toBlob(async (maskBlob) => {
const maskFormData = new FormData();
const maskFileName = fileName.replace('.png', '_mask.png');
maskFormData.append("image", maskBlob, maskFileName);
maskFormData.append("overwrite", "true");
try {
const maskResp = await fetch("/upload/image", {
method: "POST",
body: maskFormData,
});
if (maskResp.status === 200) {
const data = await resp.json();
this.widget.value = data.name;
resolve(true);
} else {
console.error("Error saving mask: " + maskResp.status);
resolve(false);
}
} catch (error) {
console.error("Error saving mask:", error);
resolve(false);
}
}, "image/png");
} else {
console.error(resp.status + " - " + resp.statusText);
resolve(false);
}
} catch (error) {
console.error(error);
resolve(false);
}
}, "image/png");
});
}
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');
});
}
moveLayerUp() {
if (!this.selectedLayer) return;
const index = this.layers.indexOf(this.selectedLayer);
if (index < this.layers.length - 1) {
const temp = this.layers[index].zIndex;
this.layers[index].zIndex = this.layers[index + 1].zIndex;
this.layers[index + 1].zIndex = temp;
[this.layers[index], this.layers[index + 1]] = [this.layers[index + 1], this.layers[index]];
this.render();
}
}
moveLayerDown() {
if (!this.selectedLayer) return;
const index = this.layers.indexOf(this.selectedLayer);
if (index > 0) {
const temp = this.layers[index].zIndex;
this.layers[index].zIndex = this.layers[index - 1].zIndex;
this.layers[index - 1].zIndex = temp;
[this.layers[index], this.layers[index - 1]] = [this.layers[index - 1], this.layers[index]];
this.render();
}
}
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) {
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;
}
mirrorHorizontal() {
if (!this.selectedLayer) return;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = this.selectedLayer.image.width;
tempCanvas.height = this.selectedLayer.image.height;
tempCtx.translate(tempCanvas.width, 0);
tempCtx.scale(-1, 1);
tempCtx.drawImage(this.selectedLayer.image, 0, 0);
const newImage = new Image();
newImage.onload = () => {
this.selectedLayer.image = newImage;
this.render();
};
newImage.src = tempCanvas.toDataURL();
}
mirrorVertical() {
if (!this.selectedLayer) return;
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = this.selectedLayer.image.width;
tempCanvas.height = this.selectedLayer.image.height;
tempCtx.translate(0, tempCanvas.height);
tempCtx.scale(1, -1);
tempCtx.drawImage(this.selectedLayer.image, 0, 0);
const newImage = new Image();
newImage.onload = () => {
this.selectedLayer.image = newImage;
this.render();
};
newImage.src = tempCanvas.toDataURL();
}
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) {
console.error("Error getting layer image data:", error);
throw error;
}
}
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();
}
processInputData(nodeData) {
if (nodeData.input_image) {
this.addInputImage(nodeData.input_image);
}
if (nodeData.input_mask) {
this.addInputMask(nodeData.input_mask);
}
}
addInputImage(imageData) {
const layer = new ImageLayer(imageData);
this.layers.push(layer);
this.updateCanvas();
}
addInputMask(maskData) {
if (this.inputImage) {
const mask = new MaskLayer(maskData);
mask.linkToLayer(this.inputImage);
this.masks.push(mask);
this.updateCanvas();
}
}
async addInputToCanvas(inputImage, inputMask) {
try {
console.log("Adding input to canvas:", {inputImage});
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = inputImage.width;
tempCanvas.height = inputImage.height;
const imgData = new ImageData(
inputImage.data,
inputImage.width,
inputImage.height
);
tempCtx.putImageData(imgData, 0, 0);
const image = new Image();
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = tempCanvas.toDataURL();
});
const scale = Math.min(
this.width / inputImage.width * 0.8,
this.height / inputImage.height * 0.8
);
const layer = {
image: image,
x: (this.width - inputImage.width * scale) / 2,
y: (this.height - inputImage.height * scale) / 2,
width: inputImage.width * scale,
height: inputImage.height * scale,
rotation: 0,
zIndex: this.layers.length
};
if (inputMask) {
layer.mask = inputMask.data;
}
this.layers.push(layer);
this.selectedLayer = layer;
this.render();
console.log("Layer added successfully");
return true;
} catch (error) {
console.error("Error in addInputToCanvas:", error);
throw error;
}
}
async convertTensorToImage(tensor) {
try {
console.log("Converting tensor to image:", tensor);
if (!tensor || !tensor.data || !tensor.width || !tensor.height) {
throw new Error("Invalid tensor data");
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = tensor.width;
canvas.height = tensor.height;
const imageData = new ImageData(
new Uint8ClampedArray(tensor.data),
tensor.width,
tensor.height
);
ctx.putImageData(imageData, 0, 0);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (e) => reject(new Error("Failed to load image: " + e));
img.src = canvas.toDataURL();
});
} catch (error) {
console.error("Error converting tensor to image:", error);
throw error;
}
}
async convertTensorToMask(tensor) {
if (!tensor || !tensor.data) {
throw new Error("Invalid mask tensor");
}
try {
return new Float32Array(tensor.data);
} catch (error) {
throw new Error(`Mask conversion failed: ${error.message}`);
}
}
async initNodeData() {
try {
console.log("Starting node data initialization...");
if (!this.node || !this.node.inputs) {
console.log("Node or inputs not ready");
return this.scheduleDataCheck();
}
if (this.node.inputs[0] && this.node.inputs[0].link) {
const imageLinkId = this.node.inputs[0].link;
const imageData = app.nodeOutputs[imageLinkId];
if (imageData) {
console.log("Found image data:", imageData);
await this.processImageData(imageData);
this.dataInitialized = true;
} else {
console.log("Image data not available yet");
return this.scheduleDataCheck();
}
}
if (this.node.inputs[1] && this.node.inputs[1].link) {
const maskLinkId = this.node.inputs[1].link;
const maskData = app.nodeOutputs[maskLinkId];
if (maskData) {
console.log("Found mask data:", maskData);
await this.processMaskData(maskData);
}
}
} catch (error) {
console.error("Error in initNodeData:", error);
return this.scheduleDataCheck();
}
}
scheduleDataCheck() {
if (this.pendingDataCheck) {
clearTimeout(this.pendingDataCheck);
}
this.pendingDataCheck = setTimeout(() => {
this.pendingDataCheck = null;
if (!this.dataInitialized) {
this.initNodeData();
}
}, 1000);
}
async processImageData(imageData) {
try {
if (!imageData) return;
console.log("Processing image data:", {
type: typeof imageData,
isArray: Array.isArray(imageData),
shape: imageData.shape,
hasData: !!imageData.data
});
if (Array.isArray(imageData)) {
imageData = imageData[0];
}
if (!imageData.shape || !imageData.data) {
throw new Error("Invalid image data format");
}
const originalWidth = imageData.shape[2];
const originalHeight = imageData.shape[1];
const scale = Math.min(
this.width / originalWidth * 0.8,
this.height / originalHeight * 0.8
);
const convertedData = this.convertTensorToImageData(imageData);
if (convertedData) {
const image = await this.createImageFromData(convertedData);
this.addScaledLayer(image, scale);
console.log("Image layer added successfully with scale:", scale);
}
} catch (error) {
console.error("Error processing image data:", error);
throw error;
}
}
addScaledLayer(image, scale) {
try {
const scaledWidth = image.width * scale;
const scaledHeight = image.height * scale;
const layer = {
image: image,
x: (this.width - scaledWidth) / 2,
y: (this.height - scaledHeight) / 2,
width: scaledWidth,
height: scaledHeight,
rotation: 0,
zIndex: this.layers.length,
originalWidth: image.width,
originalHeight: image.height
};
this.layers.push(layer);
this.selectedLayer = layer;
this.render();
console.log("Scaled layer added:", {
originalSize: `${image.width}x${image.height}`,
scaledSize: `${scaledWidth}x${scaledHeight}`,
scale: scale
});
} catch (error) {
console.error("Error adding scaled layer:", error);
throw error;
}
}
convertTensorToImageData(tensor) {
try {
const shape = tensor.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
console.log("Converting tensor:", {
shape: shape,
dataRange: {
min: tensor.min_val,
max: tensor.max_val
}
});
const imageData = new ImageData(width, height);
const data = new Uint8ClampedArray(width * height * 4);
const flatData = tensor.data;
const pixelCount = width * height;
for (let i = 0; i < pixelCount; i++) {
const pixelIndex = i * 4;
const tensorIndex = i * channels;
for (let c = 0; c < channels; c++) {
const value = flatData[tensorIndex + c];
const normalizedValue = (value - tensor.min_val) / (tensor.max_val - tensor.min_val);
data[pixelIndex + c] = Math.round(normalizedValue * 255);
}
data[pixelIndex + 3] = 255;
}
imageData.data.set(data);
return imageData;
} catch (error) {
console.error("Error converting tensor:", error);
return null;
}
}
async createImageFromData(imageData) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = canvas.toDataURL();
});
}
async retryDataLoad(maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
await this.initNodeData();
return;
} catch (error) {
console.warn(`Retry ${i + 1}/${maxRetries} failed:`, error);
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
console.error("Failed to load data after", maxRetries, "retries");
}
async processMaskData(maskData) {
try {
if (!maskData) return;
console.log("Processing mask data:", maskData);
if (Array.isArray(maskData)) {
maskData = maskData[0];
}
if (!maskData.shape || !maskData.data) {
throw new Error("Invalid mask data format");
}
if (this.selectedLayer) {
const maskTensor = await this.convertTensorToMask(maskData);
this.selectedLayer.mask = maskTensor;
this.render();
console.log("Mask applied to selected layer");
}
} catch (error) {
console.error("Error processing mask data:", error);
}
}
async loadImageFromCache(base64Data) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = base64Data;
});
}
async importImage(cacheData) {
try {
console.log("Starting image import with cache data");
const img = await this.loadImageFromCache(cacheData.image);
const mask = cacheData.mask ? await this.loadImageFromCache(cacheData.mask) : null;
const scale = Math.min(
this.width / img.width * 0.8,
this.height / img.height * 0.8
);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(img, 0, 0);
if (mask) {
const imageData = tempCtx.getImageData(0, 0, img.width, img.height);
const maskCanvas = document.createElement('canvas');
maskCanvas.width = img.width;
maskCanvas.height = img.height;
const maskCtx = maskCanvas.getContext('2d');
maskCtx.drawImage(mask, 0, 0);
const maskData = maskCtx.getImageData(0, 0, img.width, img.height);
for (let i = 0; i < imageData.data.length; i += 4) {
imageData.data[i + 3] = maskData.data[i];
}
tempCtx.putImageData(imageData, 0, 0);
}
const finalImage = new Image();
await new Promise((resolve) => {
finalImage.onload = resolve;
finalImage.src = tempCanvas.toDataURL();
});
const layer = {
image: finalImage,
x: (this.width - img.width * scale) / 2,
y: (this.height - img.height * scale) / 2,
width: img.width * scale,
height: img.height * scale,
rotation: 0,
zIndex: this.layers.length
};
this.layers.push(layer);
this.selectedLayer = layer;
this.render();
} catch (error) {
console.error('Error importing image:', error);
}
}
async importLatestImage() {
try {
console.log("Fetching latest image from server...");
const response = await fetch('/ycnode/get_latest_image');
const result = await response.json();
if (result.success && result.image_data) {
console.log("Latest image received, adding to canvas.");
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = result.image_data;
});
const layer = {
image: img,
x: 0,
y: 0,
width: this.width,
height: this.height,
rotation: 0,
zIndex: this.layers.length,
blendMode: 'normal',
opacity: 1
};
this.layers.push(layer);
this.selectedLayers = [layer];
this.selectedLayer = layer;
this.render();
console.log("Latest image imported and placed on canvas successfully.");
return true;
} else {
throw new Error(result.error || "Failed to fetch the latest image.");
}
} catch (error) {
console.error("Error importing latest image:", error);
alert(`Failed to import latest image: ${error.message}`);
return false;
}
}
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) {
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;
}
}