Files
Comfyui-LayerForge/js/Canvas_view.js
2024-11-25 01:28:58 +00:00

889 lines
32 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.

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { $el } from "../../scripts/ui.js";
import { Canvas } from "./Canvas.js";
async function createCanvasWidget(node, widget, app) {
const canvas = new Canvas(node, widget);
// 添加全局样式
const style = document.createElement('style');
style.textContent = `
.painter-button {
background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
border: 1px solid #2a2a2a;
border-radius: 4px;
color: #ffffff;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px;
text-align: center;
margin: 2px;
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
}
.painter-button:hover {
background: linear-gradient(to bottom, #5a5a5a, #4a4a4a);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.painter-button:active {
background: linear-gradient(to bottom, #3a3a3a, #4a4a4a);
transform: translateY(1px);
}
.painter-button.primary {
background: linear-gradient(to bottom, #4a6cd4, #3a5cc4);
border-color: #2a4cb4;
}
.painter-button.primary:hover {
background: linear-gradient(to bottom, #5a7ce4, #4a6cd4);
}
.painter-controls {
background: linear-gradient(to bottom, #404040, #383838);
border-bottom: 1px solid #2a2a2a;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 8px;
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.painter-container {
background: #607080; /* 带蓝色的灰色背景 */
border: 1px solid #4a5a6a;
border-radius: 6px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
}
.painter-dialog {
background: #404040;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
padding: 20px;
color: #ffffff;
}
.painter-dialog input {
background: #303030;
border: 1px solid #505050;
border-radius: 4px;
color: #ffffff;
padding: 4px 8px;
margin: 4px;
width: 80px;
}
.painter-dialog button {
background: #505050;
border: 1px solid #606060;
border-radius: 4px;
color: #ffffff;
padding: 4px 12px;
margin: 4px;
cursor: pointer;
}
.painter-dialog button:hover {
background: #606060;
}
.blend-opacity-slider {
width: 100%;
margin: 5px 0;
display: none;
}
.blend-mode-active .blend-opacity-slider {
display: block;
}
.blend-mode-item {
padding: 5px;
cursor: pointer;
position: relative;
}
.blend-mode-item.active {
background-color: rgba(0,0,0,0.1);
}
`;
document.head.appendChild(style);
// 修改控制面板,使其高度自适应
const controlPanel = $el("div.painterControlPanel", {}, [
$el("div.controls.painter-controls", {
style: {
position: "absolute",
top: "0",
left: "0",
right: "0",
minHeight: "50px", // 改为最小高度
zIndex: "10",
background: "linear-gradient(to bottom, #404040, #383838)",
borderBottom: "1px solid #2a2a2a",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
padding: "8px",
display: "flex",
gap: "6px",
flexWrap: "wrap",
alignItems: "center"
},
// 添加监听器来动态整画布容器的位置
onresize: (entries) => {
const controlsHeight = entries[0].target.offsetHeight;
canvasContainer.style.top = (controlsHeight + 10) + "px";
}
}, [
$el("button.painter-button.primary", {
textContent: "Add Image",
onclick: () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.multiple = true;
input.onchange = async (e) => {
for (const file of e.target.files) {
// 创建图片对象
const img = new Image();
img.onload = async () => {
// 计算适当的缩放比例
const scale = Math.min(
canvas.width / img.width * 0.8,
canvas.height / img.height * 0.8
);
// 创建新图层
const layer = {
image: img,
x: (canvas.width - img.width * scale) / 2,
y: (canvas.height - img.height * scale) / 2,
width: img.width * scale,
height: img.height * scale,
rotation: 0,
zIndex: canvas.layers.length
};
// 添加图层并选中
canvas.layers.push(layer);
canvas.selectedLayer = layer;
// 渲染画布
canvas.render();
// 立即保存并触发输出更新
await canvas.saveToServer(widget.value);
// 触发节点更新
app.graph.runStep();
};
img.src = URL.createObjectURL(file);
}
};
input.click();
}
}),
$el("button.painter-button.primary", {
textContent: "Import Input",
onclick: async () => {
try {
console.log("Import Input clicked");
console.log("Node ID:", node.id);
const response = await fetch(`/ycnode/get_canvas_data/${node.id}`);
console.log("Response status:", response.status);
const result = await response.json();
console.log("Full response data:", result);
if (result.success && result.data) {
if (result.data.image) {
console.log("Found image data, importing...");
await canvas.importImage({
image: result.data.image,
mask: result.data.mask
});
await canvas.saveToServer(widget.value);
app.graph.runStep();
} else {
throw new Error("No image data found in cache");
}
} else {
throw new Error("Invalid response format");
}
} catch (error) {
console.error("Error importing input:", error);
alert(`Failed to import input: ${error.message}`);
}
}
}),
$el("button.painter-button", {
textContent: "Canvas Size",
onclick: () => {
const dialog = $el("div.painter-dialog", {
style: {
position: 'fixed',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '1000'
}
}, [
$el("div", {
style: {
color: "white",
marginBottom: "10px"
}
}, [
$el("label", {
style: {
marginRight: "5px"
}
}, [
$el("span", {}, ["Width: "])
]),
$el("input", {
type: "number",
id: "canvas-width",
value: canvas.width,
min: "1",
max: "4096"
})
]),
$el("div", {
style: {
color: "white",
marginBottom: "10px"
}
}, [
$el("label", {
style: {
marginRight: "5px"
}
}, [
$el("span", {}, ["Height: "])
]),
$el("input", {
type: "number",
id: "canvas-height",
value: canvas.height,
min: "1",
max: "4096"
})
]),
$el("div", {
style: {
textAlign: "right"
}
}, [
$el("button", {
id: "cancel-size",
textContent: "Cancel"
}),
$el("button", {
id: "confirm-size",
textContent: "OK"
})
])
]);
document.body.appendChild(dialog);
document.getElementById('confirm-size').onclick = () => {
const width = parseInt(document.getElementById('canvas-width').value) || canvas.width;
const height = parseInt(document.getElementById('canvas-height').value) || canvas.height;
canvas.updateCanvasSize(width, height);
document.body.removeChild(dialog);
};
document.getElementById('cancel-size').onclick = () => {
document.body.removeChild(dialog);
};
}
}),
$el("button.painter-button", {
textContent: "Remove Layer",
onclick: () => {
const index = canvas.layers.indexOf(canvas.selectedLayer);
canvas.removeLayer(index);
}
}),
$el("button.painter-button", {
textContent: "Rotate +90°",
onclick: () => canvas.rotateLayer(90)
}),
$el("button.painter-button", {
textContent: "Scale +5%",
onclick: () => canvas.resizeLayer(1.05)
}),
$el("button.painter-button", {
textContent: "Scale -5%",
onclick: () => canvas.resizeLayer(0.95)
}),
$el("button.painter-button", {
textContent: "Layer Up",
onclick: async () => {
canvas.moveLayerUp();
await canvas.saveToServer(widget.value);
app.graph.runStep();
}
}),
$el("button.painter-button", {
textContent: "Layer Down",
onclick: async () => {
canvas.moveLayerDown();
await canvas.saveToServer(widget.value);
app.graph.runStep();
}
}),
// 添加水平镜像按钮
$el("button.painter-button", {
textContent: "Mirror H",
onclick: () => {
canvas.mirrorHorizontal();
}
}),
// 添加垂直镜像按钮
$el("button.painter-button", {
textContent: "Mirror V",
onclick: () => {
canvas.mirrorVertical();
}
}),
// 在控制面板中添加抠图按钮
$el("button.painter-button", {
textContent: "Matting",
onclick: async () => {
try {
if (!canvas.selectedLayer) {
throw new Error("Please select an image first");
}
// 获取或创建状态指示器
const statusIndicator = MattingStatusIndicator.getInstance(controlPanel.querySelector('.controls'));
// 添加状态监听
const updateStatus = (event) => {
const {status} = event.detail;
statusIndicator.setStatus(status);
};
api.addEventListener("matting_status", updateStatus);
try {
// 获取图像据
const imageData = await canvas.getLayerImageData(canvas.selectedLayer);
console.log("Sending image to server...");
// 发送请求
const response = await fetch("/matting", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
image: imageData,
threshold: 0.5,
refinement: 1
})
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const result = await response.json();
console.log("Creating new layer with matting result...");
// 创建新图层
const mattedImage = new Image();
mattedImage.onload = async () => {
// 创建临时画布来处理透明度
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = canvas.selectedLayer.width;
tempCanvas.height = canvas.selectedLayer.height;
// 绘制原始图像
tempCtx.drawImage(
mattedImage,
0, 0,
tempCanvas.width, tempCanvas.height
);
// 创建新图层
const newImage = new Image();
newImage.onload = async () => {
const newLayer = {
image: newImage,
x: canvas.selectedLayer.x,
y: canvas.selectedLayer.y,
width: canvas.selectedLayer.width,
height: canvas.selectedLayer.height,
rotation: canvas.selectedLayer.rotation,
zIndex: canvas.layers.length + 1
};
canvas.layers.push(newLayer);
canvas.selectedLayer = newLayer;
canvas.render();
// 保存并更新
await canvas.saveToServer(widget.value);
app.graph.runStep();
};
// 转换为PNG并保持透明度
newImage.src = tempCanvas.toDataURL('image/png');
};
mattedImage.src = result.matted_image;
console.log("Matting result applied successfully");
} finally {
api.removeEventListener("matting_status", updateStatus);
}
} catch (error) {
console.error("Matting error:", error);
alert(`Error during matting process: ${error.message}`);
}
}
})
])
]);
// 创建ResizeObserver来监控控制面板的高度变化
const resizeObserver = new ResizeObserver((entries) => {
const controlsHeight = entries[0].target.offsetHeight;
canvasContainer.style.top = (controlsHeight + 10) + "px";
});
// 监控控制面板的大小变化
resizeObserver.observe(controlPanel.querySelector('.controls'));
// 获取触发器widget
const triggerWidget = node.widgets.find(w => w.name === "trigger");
// 创建更新函数
const updateOutput = async () => {
// 保存画布
await canvas.saveToServer(widget.value);
// 更新触发器值
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
// 触发节点更新
app.graph.runStep();
};
// 修改所有可能触发更新的操作
const addUpdateToButton = (button) => {
const origClick = button.onclick;
button.onclick = async (...args) => {
await origClick?.(...args);
await updateOutput();
};
};
// 为所有按钮添加更新逻辑
controlPanel.querySelectorAll('button').forEach(addUpdateToButton);
// 修改画布容器样式使用动态top值
const canvasContainer = $el("div.painterCanvasContainer.painter-container", {
style: {
position: "absolute",
top: "60px", // 初始值
left: "10px",
right: "10px",
bottom: "10px",
display: "flex",
justifyContent: "center",
alignItems: "center",
overflow: "hidden"
}
}, [canvas.canvas]);
// 修改节点大小调整逻辑
node.onResize = function() {
const minSize = 300;
const controlsElement = controlPanel.querySelector('.controls');
const controlPanelHeight = controlsElement.offsetHeight; // 取实际高
const padding = 20;
// 保持节点宽度,高度根据画布比例调整
const width = Math.max(this.size[0], minSize);
const height = Math.max(
width * (canvas.height / canvas.width) + controlPanelHeight + padding * 2,
minSize + controlPanelHeight
);
this.size[0] = width;
this.size[1] = height;
// 计算画布的实际可用空间
const availableWidth = width - padding * 2;
const availableHeight = height - controlPanelHeight - padding * 2;
// 更新画布尺寸,保持比例
const scale = Math.min(
availableWidth / canvas.width,
availableHeight / canvas.height
);
canvas.canvas.style.width = (canvas.width * scale) + "px";
canvas.canvas.style.height = (canvas.height * scale) + "px";
// 强制重新渲染
canvas.render();
};
// 添加拖拽事件监听
canvas.canvas.addEventListener('mouseup', updateOutput);
canvas.canvas.addEventListener('mouseleave', updateOutput);
// 创建一个包含控制面板和画布的容器
const mainContainer = $el("div.painterMainContainer", {
style: {
position: "relative",
width: "100%",
height: "100%"
}
}, [controlPanel, canvasContainer]);
// 将主容器添加到节点
const mainWidget = node.addDOMWidget("mainContainer", "widget", mainContainer);
// 设置节点的默认大小
node.size = [500, 500]; // 设置初始大小为正方形
// 在执行开始时保存数据
api.addEventListener("execution_start", async () => {
// 保存画布
await canvas.saveToServer(widget.value);
// 保存当前节点的输入数据
if (node.inputs[0].link) {
const linkId = node.inputs[0].link;
const inputData = app.nodeOutputs[linkId];
if (inputData) {
ImageCache.set(linkId, inputData);
}
}
});
// 移除原来在 saveToServer 中的缓存清理
const originalSaveToServer = canvas.saveToServer;
canvas.saveToServer = async function(fileName) {
const result = await originalSaveToServer.call(this, fileName);
// 移除这里的缓存清理
// ImageCache.clear();
return result;
};
return {
canvas: canvas,
panel: controlPanel
};
}
// 修改状态指示器类,确保单例模式
class MattingStatusIndicator {
static instance = null;
static getInstance(container) {
if (!MattingStatusIndicator.instance) {
MattingStatusIndicator.instance = new MattingStatusIndicator(container);
}
return MattingStatusIndicator.instance;
}
constructor(container) {
this.indicator = document.createElement('div');
this.indicator.style.cssText = `
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #808080;
margin-left: 10px;
display: inline-block;
transition: background-color 0.3s;
`;
const style = document.createElement('style');
style.textContent = `
.processing {
background-color: #2196F3;
animation: blink 1s infinite;
}
.completed {
background-color: #4CAF50;
}
.error {
background-color: #f44336;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
`;
document.head.appendChild(style);
container.appendChild(this.indicator);
}
setStatus(status) {
this.indicator.className = ''; // 清除所有状态
if (status) {
this.indicator.classList.add(status);
}
if (status === 'completed') {
setTimeout(() => {
this.indicator.classList.remove('completed');
}, 2000);
}
}
}
// 验证 ComfyUI 的图像数据格式
function validateImageData(data) {
// 打印完整的输入数据结构
console.log("Validating data structure:", {
hasData: !!data,
type: typeof data,
isArray: Array.isArray(data),
keys: data ? Object.keys(data) : null,
shape: data?.shape,
dataType: data?.data ? data.data.constructor.name : null,
fullData: data // 打印完整数据
});
// 检查是否为空
if (!data) {
console.log("Data is null or undefined");
return false;
}
// 如果是数组,获取第一个元素
if (Array.isArray(data)) {
console.log("Data is array, getting first element");
data = data[0];
}
// 检查数据结构
if (!data || typeof data !== 'object') {
console.log("Invalid data type");
return false;
}
// 检查是否有数据属性
if (!data.data) {
console.log("Missing data property");
return false;
}
// 检查数据类型
if (!(data.data instanceof Float32Array)) {
// 如果不是 Float32Array尝试转换
try {
data.data = new Float32Array(data.data);
} catch (e) {
console.log("Failed to convert data to Float32Array:", e);
return false;
}
}
return true;
}
// 转换 ComfyUI 图像数据为画布可用格式
function convertImageData(data) {
console.log("Converting image data:", data);
// 如果是数组,获取第一个元素
if (Array.isArray(data)) {
data = data[0];
}
// 获取维度信息 [batch, height, width, channels]
const shape = data.shape;
const height = shape[1]; // 1393
const width = shape[2]; // 1393
const channels = shape[3]; // 3
const floatData = new Float32Array(data.data);
console.log("Processing dimensions:", { height, width, channels });
// 创建画布格式的数据 (RGBA)
const rgbaData = new Uint8ClampedArray(width * height * 4);
// 转换数据格式 [batch, height, width, channels] -> RGBA
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const tensorIndex = (h * width + w) * channels;
// 复制 RGB 通道并转换值范围 (0-1 -> 0-255)
for (let c = 0; c < channels; c++) {
const value = floatData[tensorIndex + c];
rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255)));
}
// 设置 alpha 通道为完全不透明
rgbaData[pixelIndex + 3] = 255;
}
}
// 返回画布可用的格式
return {
data: rgbaData, // Uint8ClampedArray 格式的 RGBA 数据
width: width, // 图像宽度
height: height // 图像高度
};
}
// 处理遮罩数据
function applyMaskToImageData(imageData, maskData) {
console.log("Applying mask to image data");
const rgbaData = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
// 获取遮罩数据 [batch, height, width]
const maskShape = maskData.shape;
const maskFloatData = new Float32Array(maskData.data);
console.log(`Applying mask of shape: ${maskShape}`);
// 将遮罩数据应用到 alpha 通道
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const maskIndex = h * width + w;
// 使遮罩值作为 alpha 值,转换值范围从 0-1 到 0-255
const alpha = maskFloatData[maskIndex];
rgbaData[pixelIndex + 3] = Math.max(0, Math.min(255, Math.round(alpha * 255)));
}
}
console.log("Mask application completed");
return {
data: rgbaData,
width: width,
height: height
};
}
// 修改缓存管理
const ImageCache = {
cache: new Map(),
// 存储图像数据
set(key, imageData) {
console.log("Caching image data for key:", key);
this.cache.set(key, imageData);
},
// 获取图像数据
get(key) {
const data = this.cache.get(key);
console.log("Retrieved cached data for key:", key, !!data);
return data;
},
// 检查是否存在
has(key) {
return this.cache.has(key);
},
// 清除缓存
clear() {
console.log("Clearing image cache");
this.cache.clear();
}
};
// 改进数据准备函数
function prepareImageForCanvas(inputImage) {
console.log("Preparing image for canvas:", inputImage);
try {
// 如果是数组,获取第一个元素
if (Array.isArray(inputImage)) {
inputImage = inputImage[0];
}
if (!inputImage || !inputImage.shape || !inputImage.data) {
throw new Error("Invalid input image format");
}
// 获取维度信息 [batch, height, width, channels]
const shape = inputImage.shape;
const height = shape[1];
const width = shape[2];
const channels = shape[3];
const floatData = new Float32Array(inputImage.data);
console.log("Image dimensions:", { height, width, channels });
// 创建 RGBA 格式数据
const rgbaData = new Uint8ClampedArray(width * height * 4);
// 转换数据格式 [batch, height, width, channels] -> RGBA
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = (h * width + w) * 4;
const tensorIndex = (h * width + w) * channels;
// 转换 RGB 通道 (0-1 -> 0-255)
for (let c = 0; c < channels; c++) {
const value = floatData[tensorIndex + c];
rgbaData[pixelIndex + c] = Math.max(0, Math.min(255, Math.round(value * 255)));
}
// 设置 alpha 通道
rgbaData[pixelIndex + 3] = 255;
}
}
// 返回画布需要的格式
return {
data: rgbaData,
width: width,
height: height
};
} catch (error) {
console.error("Error preparing image:", error);
throw new Error(`Failed to prepare image: ${error.message}`);
}
}
app.registerExtension({
name: "Comfy.CanvasNode",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "CanvasNode") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function() {
const r = onNodeCreated?.apply(this, arguments);
const widget = this.widgets.find(w => w.name === "canvas_image");
await createCanvasWidget(this, widget, app);
return r;
};
}
}
});
async function handleImportInput(data) {
if (data && data.image) {
const imageData = data.image;
await importImage(imageData);
}
}