diff --git a/web/extensions/core/clipspace.js b/web/extensions/core/clipspace.js new file mode 100644 index 000000000..a9da9dbd5 --- /dev/null +++ b/web/extensions/core/clipspace.js @@ -0,0 +1,281 @@ +import { app } from "/scripts/app.js"; +import { ComfyDialog, $el } from "/scripts/ui.js"; +import { ComfyApp } from "/scripts/app.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 }); +} + +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() { + 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: () => { + const backupCtx = this.backupCanvas.getContext('2d', {transparent: 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); + + 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('type', "temp"); + invalidateImage(item, formData); + this.close(); + } + }), + $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); + }, + }), + ]; + } + + show() { + 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.element.appendChild(imgCanvas); + this.element.appendChild(maskCanvas); + + 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); + } +} + +app.registerExtension({ + name: "Comfy.Clipspace", + init(app) { + app.openClipspace = + function () { + let dlg = new ClipspaceDialog(app); + if(ComfyApp.clipspace) + dlg.show(); + else + app.ui.dialog.show("Clipspace is Empty!"); + }; + } +}); \ No newline at end of file diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 5accc9d86..77517aec1 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -581,6 +581,7 @@ export class ComfyUI { }), $el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }), $el("button", { id: "comfy-refresh-button", textContent: "Refresh", onclick: () => app.refreshComboInNodes() }), + $el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }), $el("button", { id: "comfy-clear-button", textContent: "Clear", onclick: () => { if (!confirmClear.value || confirm("Clear workflow?")) { app.clean();