mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-02-09 21:12:36 +08:00
clipspace feature added
maskeditor feature added
This commit is contained in:
parent
f5af731ffd
commit
588456583d
42
server.py
42
server.py
@ -8,6 +8,7 @@ import uuid
|
|||||||
import json
|
import json
|
||||||
import glob
|
import glob
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@ -112,19 +113,24 @@ class PromptServer():
|
|||||||
files = glob.glob(os.path.join(self.web_root, 'extensions/**/*.js'), recursive=True)
|
files = glob.glob(os.path.join(self.web_root, 'extensions/**/*.js'), recursive=True)
|
||||||
return web.json_response(list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files)))
|
return web.json_response(list(map(lambda f: "/" + os.path.relpath(f, self.web_root).replace("\\", "/"), files)))
|
||||||
|
|
||||||
|
def get_dir_by_type(dir_type):
|
||||||
|
if dir_type is None:
|
||||||
|
type_dir = folder_paths.get_input_directory()
|
||||||
|
elif dir_type == "input":
|
||||||
|
type_dir = folder_paths.get_input_directory()
|
||||||
|
elif dir_type == "temp":
|
||||||
|
type_dir = folder_paths.get_temp_directory()
|
||||||
|
elif dir_type == "output":
|
||||||
|
type_dir = folder_paths.get_output_directory()
|
||||||
|
|
||||||
|
return type_dir
|
||||||
|
|
||||||
@routes.post("/upload/image")
|
@routes.post("/upload/image")
|
||||||
async def upload_image(request):
|
async def upload_image(request):
|
||||||
post = await request.post()
|
post = await request.post()
|
||||||
image = post.get("image")
|
image = post.get("image")
|
||||||
|
|
||||||
if post.get("type") is None:
|
upload_dir = get_dir_by_type(post.get("type"))
|
||||||
upload_dir = folder_paths.get_input_directory()
|
|
||||||
elif post.get("type") == "input":
|
|
||||||
upload_dir = folder_paths.get_input_directory()
|
|
||||||
elif post.get("type") == "temp":
|
|
||||||
upload_dir = folder_paths.get_temp_directory()
|
|
||||||
elif post.get("type") == "output":
|
|
||||||
upload_dir = folder_paths.get_output_directory()
|
|
||||||
|
|
||||||
if not os.path.exists(upload_dir):
|
if not os.path.exists(upload_dir):
|
||||||
os.makedirs(upload_dir)
|
os.makedirs(upload_dir)
|
||||||
@ -155,14 +161,7 @@ class PromptServer():
|
|||||||
image = post.get("image")
|
image = post.get("image")
|
||||||
original_image = post.get("original_image")
|
original_image = post.get("original_image")
|
||||||
|
|
||||||
if post.get("type") is None:
|
upload_dir = get_dir_by_type(post.get("type"))
|
||||||
upload_dir = folder_paths.get_input_directory()
|
|
||||||
elif post.get("type") == "input":
|
|
||||||
upload_dir = folder_paths.get_input_directory()
|
|
||||||
elif post.get("type") == "temp":
|
|
||||||
upload_dir = folder_paths.get_temp_directory()
|
|
||||||
elif post.get("type") == "output":
|
|
||||||
upload_dir = folder_paths.get_output_directory()
|
|
||||||
|
|
||||||
if not os.path.exists(upload_dir):
|
if not os.path.exists(upload_dir):
|
||||||
os.makedirs(upload_dir)
|
os.makedirs(upload_dir)
|
||||||
@ -180,10 +179,13 @@ class PromptServer():
|
|||||||
|
|
||||||
filepath = os.path.join(upload_dir, filename)
|
filepath = os.path.join(upload_dir, filename)
|
||||||
|
|
||||||
original_pil = Image.open(original_image)
|
original_pil = Image.open(original_image.file).convert('RGBA')
|
||||||
mask_pil = Image.open(image.file)
|
mask_pil = Image.open(image.file).convert('RGBA')
|
||||||
alpha_channel = mask_pil.getchannel('A')
|
|
||||||
original_pil.putalpha(alpha_channel, (0, 0))
|
# alpha copy
|
||||||
|
new_alpha = mask_pil.getchannel('A')
|
||||||
|
original_pil.putalpha(new_alpha)
|
||||||
|
|
||||||
original_pil.save(filepath)
|
original_pil.save(filepath)
|
||||||
|
|
||||||
return web.json_response({"name": filename})
|
return web.json_response({"name": filename})
|
||||||
|
|||||||
@ -2,268 +2,54 @@ import { app } from "/scripts/app.js";
|
|||||||
import { ComfyDialog, $el } from "/scripts/ui.js";
|
import { ComfyDialog, $el } from "/scripts/ui.js";
|
||||||
import { ComfyApp } from "/scripts/app.js";
|
import { ComfyApp } from "/scripts/app.js";
|
||||||
|
|
||||||
// Helper function to convert a data URL to a Blob object
|
export class ClipspaceDialog extends ComfyDialog {
|
||||||
function dataURLToBlob(dataURL) {
|
static items = [];
|
||||||
const parts = dataURL.split(';base64,');
|
static is_opened = false; // prevent redundant popup
|
||||||
const contentType = parts[0].split(':')[1];
|
|
||||||
const byteString = atob(parts[1]);
|
static registerButton(name, callback) {
|
||||||
const arrayBuffer = new ArrayBuffer(byteString.length);
|
const item =
|
||||||
const uint8Array = new Uint8Array(arrayBuffer);
|
$el("button", {
|
||||||
for (let i = 0; i < byteString.length; i++) {
|
type: "button",
|
||||||
uint8Array[i] = byteString.charCodeAt(i);
|
textContent: name,
|
||||||
|
onclick: callback
|
||||||
|
})
|
||||||
|
|
||||||
|
ClipspaceDialog.items.push(item);
|
||||||
}
|
}
|
||||||
return new Blob([arrayBuffer], { type: contentType });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invalidateImage(filepath, formData) {
|
|
||||||
await fetch('/upload/image', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
}).then(response => {}).catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
ComfyApp.clipspace.imgs[0] = new Image();
|
|
||||||
ComfyApp.clipspace.imgs[0].src = `view?filename=${filepath.filename}&type=${filepath.type}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ClipspaceDialog extends ComfyDialog {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.element = $el("div.comfy-modal", { parent: document.body },
|
this.element =
|
||||||
[
|
$el("div.comfy-modal", { parent: document.body },
|
||||||
$el("div.comfy-modal-content",
|
[$el("div.comfy-modal-content",[...this.createButtons()]),]
|
||||||
[
|
);
|
||||||
...this.createButtons()]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createButtons() {
|
createButtons() {
|
||||||
return [
|
const buttons = [];
|
||||||
|
|
||||||
|
for(let idx in ClipspaceDialog.items) {
|
||||||
|
const item = ClipspaceDialog.items[idx];
|
||||||
|
buttons.push(ClipspaceDialog.items[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
$el("button", {
|
$el("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Save",
|
textContent: "Close",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
const backupCtx = this.backupCanvas.getContext('2d', {transparent: true});
|
ClipspaceDialog.is_opened = false;
|
||||||
backupCtx.clearRect(0,0,this.backupCanvas.width,this.backupCanvas.height);
|
|
||||||
backupCtx.drawImage(this.maskCanvas,
|
|
||||||
0, 0, this.maskCanvas.width, this.maskCanvas.height,
|
|
||||||
0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
|
||||||
|
|
||||||
// paste mask data into alpha channel
|
|
||||||
const backupData = backupCtx.getImageData(0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
|
||||||
|
|
||||||
for (let i = 0; i < backupData.data.length; i += 4) {
|
|
||||||
if(backupData.data[i+3] == 255)
|
|
||||||
backupData.data[i+3] = 0;
|
|
||||||
else
|
|
||||||
backupData.data[i+3] = 255;
|
|
||||||
|
|
||||||
backupData.data[i] = 0;
|
|
||||||
backupData.data[i+1] = 0;
|
|
||||||
backupData.data[i+2] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
backupCtx.globalCompositeOperation = 'source-over';
|
|
||||||
backupCtx.putImageData(backupData, 0, 0);
|
|
||||||
|
|
||||||
const dataURL = this.backupCanvas.toDataURL();
|
|
||||||
const blob = dataURLToBlob(dataURL);
|
|
||||||
|
|
||||||
/*
|
|
||||||
// copy image data
|
|
||||||
backupCtx.globalCompositeOperation = 'copy';
|
|
||||||
backupCtx.globalAlpha = 1.0;
|
|
||||||
backupCtx.drawImage(this.image, 0, 0);
|
|
||||||
backupCtx.globalCompositeOperation = 'source-over';
|
|
||||||
|
|
||||||
const backupData2 = backupCtx.getImageData(0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
|
||||||
|
|
||||||
// restore alpha channel
|
|
||||||
var cnt_r = 0;
|
|
||||||
for (let i = 0; i < backupData2.data.length; i += 4) {
|
|
||||||
if(backupData2.data[i] == 0) {
|
|
||||||
cnt_r++;
|
|
||||||
}
|
|
||||||
|
|
||||||
backupData2.data[i + 3] = backupData.data[i + 3];
|
|
||||||
}
|
|
||||||
|
|
||||||
// I don't know why RGB channel is effected by this code....
|
|
||||||
backupCtx.putImageData(backupData2, 0, 0);
|
|
||||||
|
|
||||||
const dataURL2 = this.backupCanvas.toDataURL();
|
|
||||||
const blob2 = dataURLToBlob(dataURL2);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
const filename = "clipspace-mask-" + performance.now() + ".png";
|
|
||||||
|
|
||||||
const item =
|
|
||||||
{
|
|
||||||
"filename": filename,
|
|
||||||
"subfolder": "",
|
|
||||||
"type": "temp",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(ComfyApp.clipspace);
|
|
||||||
if(ComfyApp.clipspace.images)
|
|
||||||
ComfyApp.clipspace.images[0] = item;
|
|
||||||
|
|
||||||
if(ComfyApp.clipspace.widgets) {
|
|
||||||
const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
|
|
||||||
console.log(index);
|
|
||||||
ComfyApp.clipspace.widgets[index].value = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
formData.append('image', blob, filename);
|
|
||||||
formData.append('original-imagepath', ComfyApp.clipspace.);
|
|
||||||
formData.append('type', "temp");
|
|
||||||
invalidateImage(item, formData);
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
$el("button", {
|
);
|
||||||
type: "button",
|
|
||||||
textContent: "Cancel",
|
return buttons;
|
||||||
onclick: () => this.close(),
|
|
||||||
}),
|
|
||||||
$el("button", {
|
|
||||||
type: "button",
|
|
||||||
textContent: "Clear",
|
|
||||||
onclick: () => {
|
|
||||||
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
const imgCanvas = document.createElement('canvas');
|
ClipspaceDialog.is_opened = true;
|
||||||
const maskCanvas = document.createElement('canvas');
|
|
||||||
const backupCanvas = document.createElement('canvas');
|
|
||||||
|
|
||||||
imgCanvas.id = "imageCanvas";
|
|
||||||
maskCanvas.id = "maskCanvas";
|
|
||||||
backupCanvas.id = "backupCanvas";
|
|
||||||
|
|
||||||
this.element.appendChild(imgCanvas);
|
|
||||||
this.element.appendChild(maskCanvas);
|
|
||||||
|
|
||||||
this.element.style.display = "block";
|
this.element.style.display = "block";
|
||||||
imgCanvas.style.position = "relative";
|
|
||||||
imgCanvas.style.top = "200";
|
|
||||||
imgCanvas.style.left = "0";
|
|
||||||
|
|
||||||
maskCanvas.style.position = "absolute";
|
|
||||||
|
|
||||||
const imgCtx = imgCanvas.getContext('2d');
|
|
||||||
const maskCtx = maskCanvas.getContext('2d');
|
|
||||||
const backupCtx = backupCanvas.getContext('2d');
|
|
||||||
|
|
||||||
this.maskCanvas = maskCanvas;
|
|
||||||
this.maskCtx = maskCtx;
|
|
||||||
this.backupCanvas = backupCanvas;
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
// repositioning
|
|
||||||
imgCanvas.width = window.innerWidth - 250;
|
|
||||||
imgCanvas.height = window.innerHeight - 300;
|
|
||||||
|
|
||||||
// redraw image
|
|
||||||
let drawWidth = image.width;
|
|
||||||
let drawHeight = image.height;
|
|
||||||
if (image.width > imgCanvas.width) {
|
|
||||||
drawWidth = imgCanvas.width;
|
|
||||||
drawHeight = (drawWidth / image.width) * image.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawHeight > imgCanvas.height) {
|
|
||||||
drawHeight = imgCanvas.height;
|
|
||||||
drawWidth = (drawHeight / image.height) * image.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
imgCtx.drawImage(image, 0, 0, drawWidth, drawHeight);
|
|
||||||
|
|
||||||
// update mask
|
|
||||||
backupCtx.drawImage(maskCanvas, 0, 0, maskCanvas.width, maskCanvas.height, 0, 0, backupCanvas.width, backupCanvas.height);
|
|
||||||
maskCanvas.width = drawWidth;
|
|
||||||
maskCanvas.height = drawHeight;
|
|
||||||
maskCanvas.style.top = imgCanvas.offsetTop + "px";
|
|
||||||
maskCanvas.style.left = imgCanvas.offsetLeft + "px";
|
|
||||||
maskCtx.drawImage(backupCanvas, 0, 0, backupCanvas.width, backupCanvas.height, 0, 0, maskCanvas.width, maskCanvas.height);
|
|
||||||
});
|
|
||||||
|
|
||||||
// image load
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = function() {
|
|
||||||
backupCanvas.width = image.width;
|
|
||||||
backupCanvas.height = image.height;
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const filepath = ComfyApp.clipspace.images;
|
|
||||||
console.log(ComfyApp.clipspace);
|
|
||||||
console.log(ComfyApp.clipspace.imgs[0]);
|
|
||||||
image.src = ComfyApp.clipspace.imgs[0].src;
|
|
||||||
this.image = image;
|
|
||||||
|
|
||||||
// event handler for user drawing ------
|
|
||||||
let brush_size = 10;
|
|
||||||
|
|
||||||
function draw_point(event) {
|
|
||||||
const maskRect = maskCanvas.getBoundingClientRect();
|
|
||||||
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
|
||||||
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
|
||||||
|
|
||||||
maskCtx.beginPath();
|
|
||||||
maskCtx.fillStyle = "rgb(0,0,0)";
|
|
||||||
maskCtx.globalCompositeOperation = "source-over";
|
|
||||||
maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
|
||||||
maskCtx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw_move(event) {
|
|
||||||
if (event.buttons === 1) {
|
|
||||||
event.preventDefault();
|
|
||||||
const maskRect = maskCanvas.getBoundingClientRect();
|
|
||||||
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
|
||||||
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
|
||||||
|
|
||||||
maskCtx.beginPath();
|
|
||||||
maskCtx.fillStyle = "rgb(0,0,0)";
|
|
||||||
maskCtx.globalCompositeOperation = "source-over";
|
|
||||||
maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
|
||||||
maskCtx.fill();
|
|
||||||
}
|
|
||||||
else if(event.buttons === 2) {
|
|
||||||
event.preventDefault();
|
|
||||||
const maskRect = maskCanvas.getBoundingClientRect();
|
|
||||||
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
|
||||||
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
|
||||||
|
|
||||||
maskCtx.beginPath();
|
|
||||||
maskCtx.globalCompositeOperation = "destination-out";
|
|
||||||
maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
|
||||||
maskCtx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleWheelEvent(event) {
|
|
||||||
if(event.deltaY < 0)
|
|
||||||
brush_size = Math.min(brush_size+2, 100);
|
|
||||||
else
|
|
||||||
brush_size = Math.max(brush_size-2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
maskCanvas.addEventListener("contextmenu", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
maskCanvas.addEventListener('wheel', handleWheelEvent);
|
|
||||||
maskCanvas.addEventListener('mousedown', draw_point);
|
|
||||||
maskCanvas.addEventListener('mousemove', draw_move);
|
|
||||||
maskCanvas.addEventListener('touchmove', draw_move);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,11 +58,13 @@ app.registerExtension({
|
|||||||
init(app) {
|
init(app) {
|
||||||
app.openClipspace =
|
app.openClipspace =
|
||||||
function () {
|
function () {
|
||||||
let dlg = new ClipspaceDialog(app);
|
if(!ClipspaceDialog.is_opened) {
|
||||||
if(ComfyApp.clipspace)
|
let dlg = new ClipspaceDialog(app);
|
||||||
dlg.show();
|
if(ComfyApp.clipspace)
|
||||||
else
|
dlg.show();
|
||||||
app.ui.dialog.show("Clipspace is Empty!");
|
else
|
||||||
|
app.ui.dialog.show("Clipspace is Empty!");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
376
web/extensions/core/maskeditor.js
Normal file
376
web/extensions/core/maskeditor.js
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
import { app } from "/scripts/app.js";
|
||||||
|
import { ComfyDialog, $el } from "/scripts/ui.js";
|
||||||
|
import { ComfyApp } from "/scripts/app.js";
|
||||||
|
import { ClipspaceDialog } from "/extensions/core/clipspace.js";
|
||||||
|
|
||||||
|
// Helper function to convert a data URL to a Blob object
|
||||||
|
function dataURLToBlob(dataURL) {
|
||||||
|
const parts = dataURL.split(';base64,');
|
||||||
|
const contentType = parts[0].split(':')[1];
|
||||||
|
const byteString = atob(parts[1]);
|
||||||
|
const arrayBuffer = new ArrayBuffer(byteString.length);
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
|
uint8Array[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new Blob([arrayBuffer], { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadedImageToBlob(image) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
|
||||||
|
const dataURL = canvas.toDataURL('image/png', 1);
|
||||||
|
const blob = dataURLToBlob(dataURL);
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadMask(filepath, formData) {
|
||||||
|
await fetch('/upload/mask', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(response => {}).catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
ComfyApp.clipspace.imgs[0] = new Image();
|
||||||
|
ComfyApp.clipspace.imgs[0].src = `view?filename=${filepath.filename}&type=${filepath.type}`;
|
||||||
|
ComfyApp.clipspace.images = [filepath];
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRGB(image, backupCanvas, backupCtx, maskCtx) {
|
||||||
|
// paste mask data into alpha channel
|
||||||
|
backupCtx.drawImage(image, 0, 0, backupCanvas.width, backupCanvas.height);
|
||||||
|
const backupData = backupCtx.getImageData(0, 0, backupCanvas.width, backupCanvas.height);
|
||||||
|
|
||||||
|
// refine mask image
|
||||||
|
for (let i = 0; i < backupData.data.length; i += 4) {
|
||||||
|
if(backupData.data[i+3] == 255)
|
||||||
|
backupData.data[i+3] = 0;
|
||||||
|
else
|
||||||
|
backupData.data[i+3] = 255;
|
||||||
|
|
||||||
|
backupData.data[i] = 0;
|
||||||
|
backupData.data[i+1] = 0;
|
||||||
|
backupData.data[i+2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
backupCtx.globalCompositeOperation = 'source-over';
|
||||||
|
backupCtx.putImageData(backupData, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaskEditorDialog extends ComfyDialog {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.element = $el("div.comfy-modal", { parent: document.body },
|
||||||
|
[
|
||||||
|
$el("div.comfy-modal-content",
|
||||||
|
[
|
||||||
|
...this.createButtons()]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
createButtons() {
|
||||||
|
return [];
|
||||||
|
// $el("button", {
|
||||||
|
// type: "button",
|
||||||
|
// textContent: "Save",
|
||||||
|
// onclick: () => this.save(),
|
||||||
|
// }),
|
||||||
|
// $el("button", {
|
||||||
|
// type: "button",
|
||||||
|
// textContent: "Cancel",
|
||||||
|
// onclick: () => this.close(),
|
||||||
|
// }),
|
||||||
|
// $el("button", {
|
||||||
|
// type: "button",
|
||||||
|
// textContent: "Clear",
|
||||||
|
// onclick: () => {
|
||||||
|
// this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||||
|
// this.backupCtx.clearRect(0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// ];
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMask(self) {
|
||||||
|
}
|
||||||
|
|
||||||
|
setlayout(imgCanvas, maskCanvas) {
|
||||||
|
const self = this;
|
||||||
|
var bottom_panel = document.createElement("div");
|
||||||
|
bottom_panel.style.position = "fixed";
|
||||||
|
bottom_panel.style.bottom = "0";
|
||||||
|
bottom_panel.style.left = "0";
|
||||||
|
bottom_panel.style.right = "0";
|
||||||
|
bottom_panel.style.height = "50px";
|
||||||
|
|
||||||
|
var clearButton = document.createElement("button");
|
||||||
|
clearButton.innerText = "Clear";
|
||||||
|
clearButton.style.position = "absolute";
|
||||||
|
clearButton.style.left = "20px";
|
||||||
|
clearButton.addEventListener("click", function() {
|
||||||
|
self.maskCtx.clearRect(0, 0, self.maskCanvas.width, self.maskCanvas.height);
|
||||||
|
self.backupCtx.clearRect(0, 0, self.backupCanvas.width, self.backupCanvas.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
var saveButton = document.createElement("button");
|
||||||
|
saveButton.innerText = "Save";
|
||||||
|
saveButton.style.position = "absolute";
|
||||||
|
saveButton.style.right = "110px";
|
||||||
|
saveButton.addEventListener("click", function() { self.save(); });
|
||||||
|
|
||||||
|
var cancelButton = document.createElement("button");
|
||||||
|
cancelButton.innerText = "Cancel";
|
||||||
|
cancelButton.style.position = "absolute";
|
||||||
|
cancelButton.style.right = "20px";
|
||||||
|
cancelButton.addEventListener("click", function() { self.close(); });
|
||||||
|
|
||||||
|
this.element.appendChild(imgCanvas);
|
||||||
|
this.element.appendChild(maskCanvas);
|
||||||
|
this.element.appendChild(bottom_panel);
|
||||||
|
|
||||||
|
bottom_panel.appendChild(clearButton);
|
||||||
|
bottom_panel.appendChild(saveButton);
|
||||||
|
bottom_panel.appendChild(cancelButton);
|
||||||
|
|
||||||
|
this.element.style.display = "block";
|
||||||
|
imgCanvas.style.position = "relative";
|
||||||
|
imgCanvas.style.top = "200";
|
||||||
|
imgCanvas.style.left = "0";
|
||||||
|
|
||||||
|
maskCanvas.style.position = "absolute";
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
// layout
|
||||||
|
const imgCanvas = document.createElement('canvas');
|
||||||
|
const maskCanvas = document.createElement('canvas');
|
||||||
|
const backupCanvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
imgCanvas.id = "imageCanvas";
|
||||||
|
maskCanvas.id = "maskCanvas";
|
||||||
|
backupCanvas.id = "backupCanvas";
|
||||||
|
|
||||||
|
this.setlayout(imgCanvas, maskCanvas);
|
||||||
|
|
||||||
|
// prepare content
|
||||||
|
|
||||||
|
this.maskCanvas = maskCanvas;
|
||||||
|
this.backupCanvas = backupCanvas;
|
||||||
|
this.maskCtx = maskCanvas.getContext('2d');
|
||||||
|
this.backupCtx = backupCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// separate original_imgs and imgs
|
||||||
|
if(ComfyApp.clipspace.imgs[0] === ComfyApp.clipspace.original_imgs[0]) {
|
||||||
|
console.log(ComfyApp.clipspace.imgs[0]);
|
||||||
|
var copiedImage = new Image();
|
||||||
|
copiedImage.src = ComfyApp.clipspace.original_imgs[0].src;
|
||||||
|
ComfyApp.clipspace.imgs = [copiedImage];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setImages(imgCanvas, backupCanvas);
|
||||||
|
this.setEventHandler(maskCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImages(imgCanvas, backupCanvas) {
|
||||||
|
const imgCtx = imgCanvas.getContext('2d');
|
||||||
|
const backupCtx = backupCanvas.getContext('2d');
|
||||||
|
const maskCtx = this.maskCtx;
|
||||||
|
const maskCanvas = this.maskCanvas;
|
||||||
|
|
||||||
|
// image load
|
||||||
|
const orig_image = new Image();
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
// repositioning
|
||||||
|
imgCanvas.width = window.innerWidth - 250;
|
||||||
|
imgCanvas.height = window.innerHeight - 300;
|
||||||
|
|
||||||
|
// redraw image
|
||||||
|
let drawWidth = orig_image.width;
|
||||||
|
let drawHeight = orig_image.height;
|
||||||
|
if (orig_image.width > imgCanvas.width) {
|
||||||
|
drawWidth = imgCanvas.width;
|
||||||
|
drawHeight = (drawWidth / orig_image.width) * orig_image.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawHeight > imgCanvas.height) {
|
||||||
|
drawHeight = imgCanvas.height;
|
||||||
|
drawWidth = (drawHeight / orig_image.height) * orig_image.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
imgCtx.drawImage(orig_image, 0, 0, drawWidth, drawHeight);
|
||||||
|
|
||||||
|
// update mask
|
||||||
|
backupCtx.drawImage(maskCanvas, 0, 0, maskCanvas.width, maskCanvas.height, 0, 0, backupCanvas.width, backupCanvas.height);
|
||||||
|
maskCanvas.width = drawWidth;
|
||||||
|
maskCanvas.height = drawHeight;
|
||||||
|
maskCanvas.style.top = imgCanvas.offsetTop + "px";
|
||||||
|
maskCanvas.style.left = imgCanvas.offsetLeft + "px";
|
||||||
|
maskCtx.drawImage(backupCanvas, 0, 0, backupCanvas.width, backupCanvas.height, 0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filepath = ComfyApp.clipspace.images;
|
||||||
|
|
||||||
|
const touched_image = new Image();
|
||||||
|
|
||||||
|
touched_image.onload = function() {
|
||||||
|
backupCanvas.width = touched_image.width;
|
||||||
|
backupCanvas.height = touched_image.height;
|
||||||
|
|
||||||
|
removeRGB(touched_image, backupCanvas, backupCtx, maskCtx);
|
||||||
|
};
|
||||||
|
|
||||||
|
touched_image.src = ComfyApp.clipspace.imgs[0].src;
|
||||||
|
|
||||||
|
// original image load
|
||||||
|
orig_image.onload = function() {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
};
|
||||||
|
|
||||||
|
orig_image.src = ComfyApp.clipspace.original_imgs[0].src;
|
||||||
|
this.image = orig_image;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventHandler(maskCanvas) {
|
||||||
|
let brush_size = 10;
|
||||||
|
const maskCtx = maskCanvas.getContext('2d');
|
||||||
|
|
||||||
|
function draw_point(event) {
|
||||||
|
console.log(event.button);
|
||||||
|
if (event.button == 0) {
|
||||||
|
const maskRect = maskCanvas.getBoundingClientRect();
|
||||||
|
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
||||||
|
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
||||||
|
|
||||||
|
maskCtx.beginPath();
|
||||||
|
maskCtx.fillStyle = "rgb(0,0,0)";
|
||||||
|
maskCtx.globalCompositeOperation = "source-over";
|
||||||
|
maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
maskCtx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw_move(event) {
|
||||||
|
if (event.buttons === 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
const maskRect = maskCanvas.getBoundingClientRect();
|
||||||
|
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
||||||
|
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
||||||
|
|
||||||
|
maskCtx.beginPath();
|
||||||
|
maskCtx.fillStyle = "rgb(0,0,0)";
|
||||||
|
maskCtx.globalCompositeOperation = "source-over";
|
||||||
|
maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
maskCtx.fill();
|
||||||
|
}
|
||||||
|
else if(event.buttons === 2) {
|
||||||
|
event.preventDefault();
|
||||||
|
const maskRect = maskCanvas.getBoundingClientRect();
|
||||||
|
const x = event.offsetX || event.targetTouches[0].clientX - maskRect.left;
|
||||||
|
const y = event.offsetY || event.targetTouches[0].clientY - maskRect.top;
|
||||||
|
|
||||||
|
maskCtx.beginPath();
|
||||||
|
maskCtx.globalCompositeOperation = "destination-out";
|
||||||
|
maskCtx.arc(x, y, brush_size, 0, Math.PI * 2, false);
|
||||||
|
maskCtx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWheelEvent(event) {
|
||||||
|
if(event.deltaY < 0)
|
||||||
|
brush_size = Math.min(brush_size+2, 100);
|
||||||
|
else
|
||||||
|
brush_size = Math.max(brush_size-2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
maskCanvas.addEventListener("contextmenu", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
maskCanvas.addEventListener('wheel', handleWheelEvent);
|
||||||
|
maskCanvas.addEventListener('mousedown', draw_point);
|
||||||
|
maskCanvas.addEventListener('mousemove', draw_move);
|
||||||
|
maskCanvas.addEventListener('touchmove', draw_move);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const backupCtx = this.backupCanvas.getContext('2d', {willReadFrequently:true});
|
||||||
|
|
||||||
|
backupCtx.clearRect(0,0,this.backupCanvas.width,this.backupCanvas.height);
|
||||||
|
backupCtx.drawImage(this.maskCanvas,
|
||||||
|
0, 0, this.maskCanvas.width, this.maskCanvas.height,
|
||||||
|
0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
||||||
|
|
||||||
|
// paste mask data into alpha channel
|
||||||
|
const backupData = backupCtx.getImageData(0, 0, this.backupCanvas.width, this.backupCanvas.height);
|
||||||
|
|
||||||
|
// refine mask image
|
||||||
|
for (let i = 0; i < backupData.data.length; i += 4) {
|
||||||
|
if(backupData.data[i+3] == 255)
|
||||||
|
backupData.data[i+3] = 0;
|
||||||
|
else
|
||||||
|
backupData.data[i+3] = 255;
|
||||||
|
|
||||||
|
backupData.data[i] = 0;
|
||||||
|
backupData.data[i+1] = 0;
|
||||||
|
backupData.data[i+2] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
backupCtx.globalCompositeOperation = 'source-over';
|
||||||
|
backupCtx.putImageData(backupData, 0, 0);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
const filename = "clipspace-mask-" + performance.now() + ".png";
|
||||||
|
|
||||||
|
const item =
|
||||||
|
{
|
||||||
|
"filename": filename,
|
||||||
|
"subfolder": "",
|
||||||
|
"type": "temp",
|
||||||
|
};
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace.images)
|
||||||
|
ComfyApp.clipspace.images[0] = item;
|
||||||
|
|
||||||
|
if(ComfyApp.clipspace.widgets) {
|
||||||
|
const index = ComfyApp.clipspace.widgets.findIndex(obj => obj.name === 'image');
|
||||||
|
|
||||||
|
if(index >= 0)
|
||||||
|
ComfyApp.clipspace.widgets[index].value = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataURL = this.backupCanvas.toDataURL();
|
||||||
|
const blob = dataURLToBlob(dataURL);
|
||||||
|
|
||||||
|
const original_blob = loadedImageToBlob(ComfyApp.clipspace.original_imgs[0]);
|
||||||
|
|
||||||
|
formData.append('image', blob, filename);
|
||||||
|
formData.append('original_image', original_blob);
|
||||||
|
formData.append('type', "temp");
|
||||||
|
|
||||||
|
uploadMask(item, formData);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.registerExtension({
|
||||||
|
name: "Comfy.MaskEditor",
|
||||||
|
init(app) {
|
||||||
|
const callback =
|
||||||
|
function () {
|
||||||
|
let dlg = new MaskEditorDialog(app);
|
||||||
|
dlg.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
ClipspaceDialog.registerButton("MaskEditor", callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user