Files
Comfyui-LayerForge/js/Canvas.js
2024-11-24 10:31:41 +08:00

1514 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.isRotating = false;
this.rotationStartAngle = 0;
this.rotationCenter = { x: 0, y: 0 };
this.selectedLayers = [];
this.isCtrlPressed = false;
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
alpha: false
});
this.gridCache = document.createElement('canvas');
this.gridCacheCtx = this.gridCache.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: 'normal', label: '正常' },
{ name: 'multiply', label: '正片叠底' },
{ name: 'screen', label: '滤色' },
{ name: 'overlay', label: '叠加' },
{ name: 'darken', label: '变暗' },
{ name: 'lighten', label: '变亮' },
{ name: 'color-dodge', label: '颜色减淡' },
{ name: 'color-burn', label: '颜色加深' },
{ name: 'hard-light', label: '强光' },
{ name: 'soft-light', label: '柔光' },
{ name: 'difference', label: '差值' },
{ name: 'exclusion', label: '排除' }
];
}
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';
}
setupEventListeners() {
let isDragging = false;
let lastX = 0;
let lastY = 0;
let isRotating = false;
let isResizing = false;
let resizeHandle = null;
let lastClickTime = 0;
let isAltPressed = false;
let dragStartX = 0;
let dragStartY = 0;
let originalWidth = 0;
let originalHeight = 0;
document.addEventListener('keydown', (e) => {
if (e.key === 'Control') {
this.isCtrlPressed = true;
}
if (e.key === 'Alt') {
isAltPressed = true;
e.preventDefault();
}
if (e.key === 'Delete' && this.selectedLayer) {
const index = this.layers.indexOf(this.selectedLayer);
this.removeLayer(index);
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Control') {
this.isCtrlPressed = false;
}
if (e.key === 'Alt') {
isAltPressed = false;
}
});
this.canvas.addEventListener('mousedown', (e) => {
const currentTime = new Date().getTime();
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (currentTime - lastClickTime < 300) {
this.selectedLayers = [];
this.selectedLayer = null;
this.render();
return;
}
lastClickTime = currentTime;
const result = this.getLayerAtPosition(mouseX, mouseY);
if (result) {
const clickedLayer = result.layer;
dragStartX = mouseX;
dragStartY = mouseY;
if (clickedLayer) {
originalWidth = clickedLayer.width;
originalHeight = clickedLayer.height;
}
if (this.isCtrlPressed) {
const index = this.selectedLayers.indexOf(clickedLayer);
if (index === -1) {
this.selectedLayers.push(clickedLayer);
this.selectedLayer = clickedLayer;
} else {
this.selectedLayers.splice(index, 1);
this.selectedLayer = this.selectedLayers[this.selectedLayers.length - 1] || null;
}
} else {
if (!this.selectedLayers.includes(clickedLayer)) {
this.selectedLayers = [clickedLayer];
this.selectedLayer = clickedLayer;
}
}
if (this.isRotationHandle(mouseX, mouseY)) {
isRotating = true;
this.rotationCenter.x = this.selectedLayer.x + this.selectedLayer.width/2;
this.rotationCenter.y = this.selectedLayer.y + this.selectedLayer.height/2;
this.rotationStartAngle = Math.atan2(
mouseY - this.rotationCenter.y,
mouseX - this.rotationCenter.x
);
} else {
isDragging = true;
lastX = mouseX;
lastY = mouseY;
}
} else {
if (!this.isCtrlPressed) {
this.selectedLayers = [];
this.selectedLayer = null;
}
}
this.render();
});
this.canvas.addEventListener('mousemove', (e) => {
if (!this.selectedLayer) return;
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (isDragging && isAltPressed) {
const dx = mouseX - dragStartX;
const dy = mouseY - dragStartY;
if (Math.abs(dx) > Math.abs(dy)) {
this.selectedLayer.width = Math.max(20, originalWidth + dx);
} else {
this.selectedLayer.height = Math.max(20, originalHeight + dy);
}
this.render();
} else if (isDragging && !isAltPressed) {
const dx = mouseX - lastX;
const dy = mouseY - lastY;
this.selectedLayers.forEach(layer => {
layer.x += dx;
layer.y += dy;
});
lastX = mouseX;
lastY = mouseY;
this.render();
}
const cursor = isAltPressed && isDragging
? (Math.abs(mouseX - dragStartX) > Math.abs(mouseY - dragStartY) ? 'ew-resize' : 'ns-resize')
: this.getResizeHandle(mouseX, mouseY)
? 'nw-resize'
: this.isRotationHandle(mouseX, mouseY)
? 'grab'
: isDragging ? 'move' : 'default';
this.canvas.style.cursor = cursor;
});
this.canvas.addEventListener('mouseup', () => {
isDragging = false;
isRotating = false;
});
this.canvas.addEventListener('mouseleave', () => {
isDragging = false;
isRotating = false;
});
// 添加鼠标滚轮缩放功能
this.canvas.addEventListener('wheel', (e) => {
if (!this.selectedLayer) return;
e.preventDefault();
const scaleFactor = e.deltaY > 0 ? 0.95 : 1.05;
// 如果按住Shift键则进行旋转而不是缩放
if (e.shiftKey) {
const rotateAngle = e.deltaY > 0 ? -5 : 5;
this.selectedLayers.forEach(layer => {
layer.rotation = (layer.rotation + rotateAngle) % 360;
});
} else {
// 从鼠标位置为中心进行缩放
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
this.selectedLayers.forEach(layer => {
const centerX = layer.x + layer.width/2;
const centerY = layer.y + layer.height/2;
// 计算鼠标相对于图中心的位置
const relativeX = mouseX - centerX;
const relativeY = mouseY - centerY;
// 更新尺寸
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;
});
}
this.render();
});
// 优化旋转控制逻辑
let initialRotation = 0;
let initialAngle = 0;
this.canvas.addEventListener('mousemove', (e) => {
// ... 其他代码保持不变 ...
if (isRotating) {
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const centerX = this.selectedLayer.x + this.selectedLayer.width/2;
const centerY = this.selectedLayer.y + this.selectedLayer.height/2;
// 计算当前角度
const angle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI;
if (e.shiftKey) {
// 按住Shift键时启用15度角度吸附
const snap = 15;
const rotation = Math.round((angle - initialAngle + initialRotation) / snap) * snap;
this.selectedLayers.forEach(layer => {
layer.rotation = rotation;
});
} else {
// 正常旋转
const rotation = angle - initialAngle + initialRotation;
this.selectedLayers.forEach(layer => {
layer.rotation = rotation;
});
}
this.render();
}
});
this.canvas.addEventListener('mousedown', (e) => {
// ... 其他代码保持不变 ...
if (this.isRotationHandle(mouseX, mouseY)) {
isRotating = true;
const centerX = this.selectedLayer.x + this.selectedLayer.width/2;
const centerY = this.selectedLayer.y + this.selectedLayer.height/2;
initialRotation = this.selectedLayer.rotation;
initialAngle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI;
}
});
// 添加键盘快捷键
document.addEventListener('keydown', (e) => {
if (!this.selectedLayer) return;
const step = e.shiftKey ? 1 : 5; // Shift键按下时更精细的控制
switch(e.key) {
case 'ArrowLeft':
this.selectedLayers.forEach(layer => layer.x -= step);
break;
case 'ArrowRight':
this.selectedLayers.forEach(layer => layer.x += step);
break;
case 'ArrowUp':
this.selectedLayers.forEach(layer => layer.y -= step);
break;
case 'ArrowDown':
this.selectedLayers.forEach(layer => layer.y += step);
break;
case '[':
this.selectedLayers.forEach(layer => layer.rotation -= step);
break;
case ']':
this.selectedLayers.forEach(layer => layer.rotation += step);
break;
}
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', '[', ']'].includes(e.key)) {
e.preventDefault();
this.render();
}
});
this.canvas.addEventListener('mousedown', (e) => {
const rect = this.canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (e.shiftKey) {
const result = this.getLayerAtPosition(mouseX, mouseY);
if (result) {
this.selectedLayer = result.layer;
this.showBlendModeMenu(e.clientX, e.clientY);
e.preventDefault(); // 阻止默认行为
return;
}
}
// ... 其余现有的mousedown处理代码 ...
});
}
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
};
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();
}
}
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.layers.forEach(layer => {
const scale = Math.min(
width / layer.image.width * 0.8,
height / layer.image.height * 0.8
);
layer.width = layer.image.width * scale;
layer.height = layer.image.height * scale;
layer.x = (width - layer.width) / 2;
layer.y = (height - layer.height) / 2;
});
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.width ||
this.offscreenCanvas.height !== this.height) {
this.offscreenCanvas.width = this.width;
this.offscreenCanvas.height = this.height;
}
const ctx = this.offscreenCtx;
ctx.fillStyle = '#606060';
ctx.fillRect(0, 0, this.width, this.height);
this.drawCachedGrid();
const sortedLayers = [...this.layers].sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach(layer => {
if (!layer.image) return;
ctx.save();
// 应用混合模式
ctx.globalCompositeOperation = layer.blendMode || 'normal';
const centerX = layer.x + layer.width/2;
const centerY = layer.y + layer.height/2;
const rad = layer.rotation * Math.PI / 180;
// 1. 先设置变换
ctx.setTransform(
Math.cos(rad), Math.sin(rad),
-Math.sin(rad), Math.cos(rad),
centerX, centerY
);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 2. 先绘制原始图像
ctx.drawImage(
layer.image,
-layer.width/2,
-layer.height/2,
layer.width,
layer.height
);
// 3. 再应用遮罩
if (layer.mask) {
try {
console.log("Applying mask to layer");
const maskCanvas = document.createElement('canvas');
const maskCtx = maskCanvas.getContext('2d');
maskCanvas.width = layer.width;
maskCanvas.height = layer.height;
const maskImageData = maskCtx.createImageData(layer.width, layer.height);
const maskData = new Float32Array(layer.mask);
for (let i = 0; i < maskData.length; i++) {
maskImageData.data[i * 4] =
maskImageData.data[i * 4 + 1] =
maskImageData.data[i * 4 + 2] = 255;
maskImageData.data[i * 4 + 3] = maskData[i] * 255;
}
maskCtx.putImageData(maskImageData, 0, 0);
// 使用destination-in混合模式
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(maskCanvas,
-layer.width/2, -layer.height/2,
layer.width, layer.height
);
console.log("Mask applied successfully");
} catch (error) {
console.error("Error applying mask:", error);
}
}
// 4. 最后绘制选择框
if (this.selectedLayers.includes(layer)) {
this.drawSelectionFrame(layer);
}
ctx.restore();
});
this.ctx.drawImage(this.offscreenCanvas, 0, 0);
}
drawCachedGrid() {
if (this.gridCache.width !== this.width ||
this.gridCache.height !== this.height) {
this.gridCache.width = this.width;
this.gridCache.height = this.height;
const ctx = this.gridCacheCtx;
const gridSize = 20;
ctx.beginPath();
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 0.5;
for(let y = 0; y < this.height; y += gridSize) {
ctx.moveTo(0, y);
ctx.lineTo(this.width, y);
}
for(let x = 0; x < this.width; x += gridSize) {
ctx.moveTo(x, 0);
ctx.lineTo(x, this.height);
}
ctx.stroke();
}
this.offscreenCtx.drawImage(this.gridCache, 0, 0);
}
drawSelectionFrame(layer) {
const ctx = this.offscreenCtx;
ctx.beginPath();
ctx.rect(-layer.width/2, -layer.height/2, layer.width, layer.height);
ctx.moveTo(0, -layer.height/2);
ctx.lineTo(0, -layer.height/2 - 20);
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
const points = [
{x: 0, y: -layer.height/2 - 20},
{x: -layer.width/2, y: -layer.height/2},
{x: layer.width/2, y: -layer.height/2},
{x: layer.width/2, y: layer.height/2},
{x: -layer.width/2, y: layer.height/2}
];
points.forEach(point => {
ctx.moveTo(point.x, point.y);
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
});
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.stroke();
}
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);
// 按照zIndex顺序绘制所有图层
this.layers.sort((a, b) => a.zIndex - b.zIndex).forEach(layer => {
// 绘制主图像,包含混合模式
tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
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 {
// 如果没有遮罩使用图层的alpha通道
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);
// 提取alpha通道
for (let i = 0; i < imageData.data.length; i += 4) {
alphaData.data[i] = alphaData.data[i + 1] = alphaData.data[i + 2] = imageData.data[i + 3];
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");
});
}
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(x, y) {
// 获取画布的实际显示尺寸和位置
const rect = this.canvas.getBoundingClientRect();
// 计算画布的缩放比例
const displayWidth = rect.width;
const displayHeight = rect.height;
const scaleX = this.width / displayWidth;
const scaleY = this.height / displayHeight;
// 计算鼠标在画布上的实际位置
const canvasX = (x) * scaleX;
const canvasY = (y) * scaleY;
// 从上层到下层遍历所有图层
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 rad = -layer.rotation * Math.PI / 180;
// 将点击坐标转换到图层的本地坐标系
const dx = canvasX - centerX;
const dy = canvasY - centerY;
const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad) + centerX;
const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad) + centerY;
// 检查点击位置是否在图层范围内
if (rotatedX >= layer.x &&
rotatedX <= layer.x + layer.width &&
rotatedY >= layer.y &&
rotatedY <= layer.y + layer.height) {
// 创建临时画布来检查透明度
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = layer.width;
tempCanvas.height = layer.height;
// 绘制图层到临时画布
tempCtx.save();
tempCtx.clearRect(0, 0, layer.width, layer.height);
tempCtx.drawImage(
layer.image,
0,
0,
layer.width,
layer.height
);
tempCtx.restore();
// 获取点击位置的像素数据
const localX = rotatedX - layer.x;
const localY = rotatedY - layer.y;
try {
const pixel = tempCtx.getImageData(
Math.round(localX),
Math.round(localY),
1, 1
).data;
// 检查像素的alpha值
if (pixel[3] > 10) {
return {
layer: layer,
localX: localX,
localY: localY
};
}
} catch(e) {
console.error("Error checking pixel transparency:", e);
}
}
}
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();
// 获取base64数据
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 {
// 确保数据是Float32Array
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); // 1秒后重试
}
// 修改图像数据处理方法
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;
// 正确处理RGB通道
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);
}
// Alpha通道
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);
// 应用遮罩到alpha通道
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);
}
}
// 添加混合模式菜单方法
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 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})`;
// 高亮当前选中的混合模式
if (this.selectedLayer && this.selectedLayer.blendMode === mode.name) {
option.style.backgroundColor = '#3a3a3a';
}
option.onmouseover = () => {
option.style.backgroundColor = '#3a3a3a';
};
option.onmouseout = () => {
if (!(this.selectedLayer && this.selectedLayer.blendMode === mode.name)) {
option.style.backgroundColor = '';
}
};
option.onclick = () => {
if (this.selectedLayer) {
this.selectedLayer.blendMode = mode.name;
this.render();
}
document.body.removeChild(menu);
};
menu.appendChild(option);
});
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);
}
}