mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 05:02:11 -03:00
1514 lines
55 KiB
JavaScript
1514 lines
55 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.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);
|
||
}
|
||
}
|