Unverified Commit 5e25c770 authored by pythongosssss's avatar pythongosssss Committed by GitHub
Browse files

Initial refactoring changes

 - Moved to web folder
 - Splitting into individual files
parent 3637e19e
<html>
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="litegraph.css">
<script type="text/javascript" src="litegraph.core.js"></script>
<link rel="stylesheet" type="text/css" href="lib/litegraph.css">
<link rel="stylesheet" type="text/css" href="style.css">
<script type="text/javascript" src="lib/litegraph.core.js"></script>
<script type="module">
import { app } from "/scripts/app.js";
await app.setup();
window.app = app;
window.graph = app.graph;
</script>
</head>
<style>
.customtext_input {
background-color: #FFFFFF;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #202020;
}
.customtext_input {
background-color: #202020;
color: white;
}
}
</style>
<body style='width:100%; height:100%; overflow: hidden;'>
<style>
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 100; /* Sit on top */
left: 20%; /* Center the modal horizontally */
right: 20%; /* Center the modal horizontally */
bottom: 30%; /* Center the modal vertically */
/* transform: translate(-50%, -50%); /* Use this to center the modal */ */
width: 50%; /* Set a width for the modal */
height: auto; /* Set a height for the modal */
padding: 30px;
background-color: #ff0000; /* Modal background */
box-shadow: 0px 0px 20px #888888;
border-radius: 10px;
text-align: center;
}
.close {
color: #aaaaaa;
font-size: 24px; /* Decreased font-size */
font-weight: bold;
position: absolute;
bottom: 10px; /* move the close button up a bit */
left: 50%; /* center the close button horizontally */
transform: translateX(-50%); /* use this to center the close button horizontally */
width: 100%;
text-align: center; /* center the text inside the button */
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
#modal-text {
white-space: pre-line; /* This will respect line breaks */
margin-bottom: 20px; /* Add some margin between the text and the close button*/
}
@media only screen and (max-height: 850px) {
#menu {
margin-top: -70px;
}
}
</style>
<div id="myErrorModal" class="modal">
<div class="modal-content">
<p id="modal-text"></p>
<span class="close">CLOSE</span>
</div>
</div>
<canvas id='mycanvas' width='1000' height='1000' style='width: 100%; height: 100%;'></canvas>
<body>
<script>
var graph = new LGraph();
var canvas = new LGraphCanvas("#mycanvas", graph);
const ccc = document.getElementById("mycanvas");
const ctx = ccc.getContext("2d");
let nodeOutputs = {}
// Resize the canvas to match the size of the canvas element
function resizeCanvas() {
ccc.width = ccc.offsetWidth;
ccc.height = ccc.offsetHeight;
canvas.draw(true, true);
}
// call the function when the page loads
resizeCanvas();
// call the function when the window is resized
window.addEventListener("resize", resizeCanvas);
var default_graph = {"last_node_id":9,"last_link_id":9,"nodes":[{"id":7,"type":"CLIPTextEncode","pos":[413,389],"size":{"0":425.27801513671875,"1":180.6060791015625},"flags":{},"order":3,"mode":0,"inputs":[{"name":"clip","type":"CLIP","link":5}],"outputs":[{"name":"CONDITIONING","type":"CONDITIONING","links":[6],"slot_index":0}],"properties":{},"widgets_values":["bad hands"]},{"id":6,"type":"CLIPTextEncode","pos":[415,186],"size":{"0":422.84503173828125,"1":164.31304931640625},"flags":{},"order":2,"mode":0,"inputs":[{"name":"clip","type":"CLIP","link":3}],"outputs":[{"name":"CONDITIONING","type":"CONDITIONING","links":[4],"slot_index":0}],"properties":{},"widgets_values":["masterpiece best quality girl"]},{"id":5,"type":"EmptyLatentImage","pos":[473,609],"size":{"0":315,"1":106},"flags":{},"order":1,"mode":0,"outputs":[{"name":"LATENT","type":"LATENT","links":[2],"slot_index":0}],"properties":{},"widgets_values":[512,512,1]},{"id":3,"type":"KSampler","pos":[863,186],"size":{"0":315,"1":262},"flags":{},"order":4,"mode":0,"inputs":[{"name":"model","type":"MODEL","link":1},{"name":"positive","type":"CONDITIONING","link":4},{"name":"negative","type":"CONDITIONING","link":6},{"name":"latent_image","type":"LATENT","link":2}],"outputs":[{"name":"LATENT","type":"LATENT","links":[7],"slot_index":0}],"properties":{},"widgets_values":[8566257,true,20,8,"euler","normal",1]},{"id":8,"type":"VAEDecode","pos":[1209,188],"size":{"0":210,"1":46},"flags":{},"order":5,"mode":0,"inputs":[{"name":"samples","type":"LATENT","link":7},{"name":"vae","type":"VAE","link":8}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[9],"slot_index":0}],"properties":{}},{"id":9,"type":"SaveImage","pos":[1451,189],"size":{"0":210,"1":26},"flags":{},"order":6,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":9}],"properties":{}},{"id":4,"type":"CheckpointLoader","pos":[26,474],"size":{"0":315,"1":122},"flags":{},"order":0,"mode":0,"outputs":[{"name":"MODEL","type":"MODEL","links":[1],"slot_index":0},{"name":"CLIP","type":"CLIP","links":[3,5],"slot_index":1},{"name":"VAE","type":"VAE","links":[8],"slot_index":2}],"properties":{},"widgets_values":["v1-inference.yaml","v1-5-pruned-emaonly.ckpt"]}],"links":[[1,4,0,3,0,"MODEL"],[2,5,0,3,3,"LATENT"],[3,4,1,6,0,"CLIP"],[4,6,0,3,1,"CONDITIONING"],[5,4,1,7,0,"CLIP"],[6,7,0,3,2,"CONDITIONING"],[7,3,0,8,0,"LATENT"],[8,4,2,8,1,"VAE"],[9,8,0,9,0,"IMAGE"]],"groups":[],"config":{},"extra":{},"version":0.4}
function loadGraphData(graph, graph_data)
{
graph.configure( graph_data);
for (let n in graph._nodes) {
n = graph._nodes[n];
s = n.computeSize();
s[0] = Math.max(n.size[0], s[0]);
s[1] = Math.max(n.size[1], s[1]);
n.size = s;
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
for (let w in n.widgets) {
let wid = n.widgets[w];
if (n.class_comfy == "KSampler" || n.class_comfy == "KSamplerAdvanced") {
if (wid.name == "sampler_name") {
if (wid.value.startsWith("sample_")) {
wid.value = wid.value.slice(7);
}
}
}
}
}
}
function afterLoadGraph()
{
let workflow = null;
try {
workflow = JSON.parse(localStorage.getItem("workflow"));
loadGraphData(graph, workflow);
} catch(err) {
}
if (!workflow) {
loadGraphData(graph, default_graph);
}
function saveGraph() {
localStorage.setItem("workflow", JSON.stringify(graph.serialize()));
}
setInterval(saveGraph, 1000);
}
function onObjectInfo(json) {
for (let key in json) {
function MyNode()
{
j = MyNode.__json_data;
inp = j['input']['required'];
this.class_comfy = MyNode.class_type_comfy;
this._widgets = []
min_height = 1;
min_width = 1;
for (let x in inp) {
let default_val = min_val = max_val = step_val = multiline = dynamic_prompt = undefined;
if (inp[x].length > 1) {
default_val = inp[x][1]['default'];
min_val = inp[x][1]['min'];
max_val = inp[x][1]['max'];
step_val = inp[x][1]['step'];
multiline = inp[x][1]['multiline'];
dynamic_prompt = inp[x][1]['dynamic_prompt'];
}
let type = inp[x][0];
if (Array.isArray(type)) {
w = this.addWidget("combo", x, type[0], function(v){}, { values: type } );
this._widgets += [w]
} else if (type == "INT") {
if (default_val == undefined) default_val = 0;
if (min_val == undefined) min_val = 0;
if (max_val == undefined) max_val = 2048;
if (step_val == undefined) step_val = 1;
w = this.addWidget("number", x, default_val, function(v){let s = this.options.step / 10;this.value = Math.round( v / s ) * s;}, { min: min_val, max: max_val, step: 10.0 * step_val} );
this._widgets += [w]
if (x == "seed" || x == "noise_seed") {
w1 = this.addWidget("toggle", "Random seed after every gen", true, function(v){}, { on: "enabled", off:"disabled"} );
w1.to_randomize = w;
this._widgets += [w1]
}
} else if (type == "FLOAT") {
if (default_val == undefined) default_val = 0;
if (min_val == undefined) min_val = 0;
if (max_val == undefined) max_val = 2048;
if (step_val == undefined) step_val = 0.5;
// if (min_val == 0.0 && max_val == 1.0) {
// w = this.slider = this.addWidget("slider", x, default_val, function(v){}, { min: min_val, max: max_val} );
// } else {
w = this.addWidget("number", x, default_val, function(v){}, { min: min_val, max: max_val, step: 10.0 * step_val} );
// }
this._widgets += [w]
} else if (type == "STRING") {
if (default_val == undefined) default_val = "";
if (multiline == undefined) multiline = false;
if (dynamic_prompt == undefined) dynamic_prompt = false;
if (multiline) {
var w = {
type: "customtext",
name: x,
dynamic_prompt: dynamic_prompt,
get value() { return this.input_div.value;},
set value(x) { this.input_div.value = x;},
callback: function(v){console.log(v);},
options: {},
draw: function(ctx, node, widget_width, y, H){
var show_text = canvas.ds.scale > 0.5;
// this.input_div.style.top = `${y}px`;
let t = ctx.getTransform();
let margin = 10;
let x_div = t.a * margin * 2 + t.e;
let y_div = t.d * (y + H) + t.f;
let width_div = (widget_width - margin * 2 - 3) * t.a;
let height_div = (this.parent.size[1] - (y + H) - 3)* t.d;
this.input_div.style.left = `${x_div}px`;
this.input_div.style.top = `${y_div}px`;
this.input_div.style.width = width_div;
this.input_div.style.height = height_div;
this.input_div.style.position = 'absolute';
this.input_div.style.zIndex = 1;
this.input_div.style.fontSize = t.d * 10.0;
if (show_text) {
this.input_div.hidden = false;
} else {
this.input_div.hidden = true;
}
ctx.save();
// ctx.fillText(String(this.value).substr(0,30), 0, y + H * 0.7);
ctx.restore();
},
};
w.input_div = document.createElement('textarea');
w.input_div.contentEditable = true;
w.input_div.className = "customtext_input";
w.input_div.style.overflow = 'hidden';
w.input_div.style.overflowY = 'auto';
w.input_div.style.padding = 2;
w.input_div.style.resize = 'none';
w.input_div.style.border = 'none';
w.input_div.value = default_val;
document.addEventListener('click', function(event) {
if (!w.input_div.contains(event.target)) {
w.input_div.blur();
}
});
w.parent = this;
min_height = Math.max(min_height, 200);
min_width = Math.max(min_width, 400);
ccc.parentNode.appendChild(w.input_div);
w = this.addCustomWidget(w);
canvas.onDrawBackground = function() {
for (let n in graph._nodes) {
n = graph._nodes[n];
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, 'input_div')) {
wid.input_div.style.left = -8000;
wid.input_div.style.position = 'absolute';
}
}
}
}
// w = this.addWidget("text", x, "", function(v){}, { multiline:true } );
console.log(w, this);
this._widgets += [w]
this.onRemoved = function() {
for (let y in this.widgets) {
if (this.widgets[y].input_div) {
this.widgets[y].input_div.remove();
}
}
}
} else {
w = this.addWidget("text", x, default_val, function(v){}, { multiline:false } );
w.dynamic_prompt = dynamic_prompt;
this._widgets += [w];
}
} else {
this.addInput(x, type);
}
MyNode.prototype.getExtraMenuOptions = function(canvas, options) {
if(this.imgs) {
let img;
if(this.imageIndex != null) {
img = this.imgs[this.imageIndex];
} else if(this.overIndex != null) {
img = this.imgs[this.overIndex];
}
if(img) {
options.unshift({
content: "Open Image",
callback: () => window.open(img.src, "_blank")
});
}
}
}
MyNode.prototype.onDrawBackground = function(ctx) {
if(!this.flags.collapsed) {
const output = nodeOutputs[this.id + ""];
if(output && output.images) {
if(this.images !== output.images) {
this.images = output.images;
this.imgs = null;
this.imageIndex = null;
Promise.all(output.images.map(src => {
return new Promise(r => {
const img = new Image();
img.onload = () => r(img);
img.onerror = () => r(null);
img.src = "/view/" + src;
});
})).then(imgs => {
if(this.images === output.images) {
this.imgs = imgs.filter(Boolean);
if(this.size[1] < 100) {
this.size[1] = 250;
}
graph.setDirtyCanvas(true);
}
});
}
if(this.imgs) {
const canvas = graph.list_of_graphcanvas[0];
const mouse = canvas.graph_mouse;
if(!canvas.pointer_is_down && this.pointerDown) {
if(mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) {
this.imageIndex = this.pointerDown.index;
}
this.pointerDown = null;
}
let w = this.imgs[0].naturalWidth;
let h = this.imgs[0].naturalHeight;
let imageIndex = this.imageIndex;
const numImages = this.imgs.length;
if(numImages === 1 && !imageIndex) {
this.imageIndex = imageIndex = 0;
}
let shiftY = this.type === "SaveImage" ? 55 : 0;
let dw = this.size[0];
let dh = this.size[1];
dh -= shiftY;
if(imageIndex == null) {
let best = 0;
let cellWidth;
let cellHeight;
let cols = 0;
let shiftX = 0;
for (let c = 1; c <= numImages; c++) {
const rows = Math.ceil(numImages / c);
const cW = dw / c;
const cH = dh / rows;
const scaleX = cW / w;
const scaleY = cH / h;
const scale = Math.min(scaleX, scaleY, 1);
const imageW = w * scale;
const imageH = h * scale;
const area = imageW * imageH * numImages;
if(area > best) {
best = area;
cellWidth = imageW;
cellHeight = imageH;
cols = c;
shiftX = c * ((cW - imageW) / 2);
}
}
let anyHovered = false;
this.imageRects = [];
for (let i = 0; i < numImages; i++) {
const img = this.imgs[i];
const row = Math.floor(i / cols);
const col = i % cols;
const x = col * cellWidth + shiftX;
const y = row * cellHeight + shiftY;
if(!anyHovered) {
anyHovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], cellWidth, cellHeight);
if(anyHovered) {
this.overIndex = i;
let value = 110;
if(canvas.pointer_is_down) {
if(!this.pointerDown || this.pointerDown.index !== i) {
this.pointerDown = {index: i, pos: [...mouse]};
}
value = 125;
}
ctx.filter = `contrast(${value}%) brightness(${value}%)`;
canvas.canvas.style.cursor = "pointer";
}
}
this.imageRects.push([x, y, cellWidth, cellHeight]);
ctx.drawImage(img, x, y, cellWidth, cellHeight);
ctx.filter = "none";
}
if(!anyHovered) {
this.pointerDown = null;
this.overIndex = null;
}
} else {
// Draw individual
const scaleX = dw / w;
const scaleY = dh / h;
const scale = Math.min(scaleX, scaleY, 1);
w *= scale;
h *= scale;
let x = (dw - w) / 2;
let y = (dh - h) / 2 + shiftY;
ctx.drawImage(this.imgs[imageIndex], x, y, w, h);
const drawButton = (x, y, sz, text) => {
const hovered = LiteGraph.isInsideRectangle(mouse[0], mouse[1], x + this.pos[0], y + this.pos[1], sz, sz);
let fill = "#333";
let textFill = "#fff";
let isClicking = false;
if(hovered) {
canvas.canvas.style.cursor = "pointer";
if(canvas.pointer_is_down) {
fill = "#1e90ff";
isClicking = true;
} else {
fill = "#eee";
textFill = "#000";
}
} else {
this.pointerWasDown = null;
}
ctx.fillStyle = fill;
ctx.beginPath();
ctx.roundRect(x, y, sz, sz, [4]);
ctx.fill();
ctx.fillStyle = textFill;
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(text, x + 15, y + 20);
return isClicking;
}
if(numImages > 1) {
if(drawButton(x + w - 35, y + h - 35, 30, `${this.imageIndex+1}/${numImages}`)) {
let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1;
if(!this.pointerDown || !this.pointerDown.index === i) {
this.pointerDown = {index: i, pos: [...mouse]};
}
}
if(drawButton(x + w - 35, y + 5, 30, `x`)) {
if(!this.pointerDown || !this.pointerDown.index === null) {
this.pointerDown = {index: null, pos: [...mouse]};
}
}
}
}
}
}
}
};
}
out = j['output'];
for (let x in out) {
this.addOutput(out[x], out[x]);
}
s = this.computeSize();
s[0] *= 1.5;
s[0] = Math.max(min_width, s[0]);
s[1] = Math.max(min_height, s[1]);
this.size = s;
this.serialize_widgets = true;
}
MyNode.title = json[key]['name'];
MyNode.class_type_comfy = json[key]['name'];
MyNode.__json_data = json[key]
LiteGraph.registerNodeType(key, MyNode);
MyNode.category = json[key]['category'];
};
afterLoadGraph();
// loadGraphData(graph, JSON.parse(base_txt2img_graph));
}
fetch("object_info", {cache: "no-store"})
.then(response => response.json())
.then(json => onObjectInfo(json));
//register in the system
graph.start();
// LiteGraph.registerNodeType("testing", MyAddNode);
graph.onNodeRemoved = function(n) {
for (let y in n.widgets) {
if (n.widgets[y].input_div) {
n.widgets[y].input_div.remove();
}
}
}
function graphToPrompt() {
let s = graph.serialize();
let output = {};
// console.log(s['nodes']);
nodes = s['nodes']
for (let x in nodes) {
let n = graph.getNodeById(nodes[x].id);
let input_ = {};
// dynamic prompts handling
if (n.widgets && n.widgets.length > 0) {
// find widgets declared as supporting dynamic prompting
var supportedWidgets = n.widgets.filter(e => e.dynamic_prompt === true);
if (supportedWidgets.length > 0) {
// resolve dynamic prompts for all widgets supporting it in this node
for (let i in supportedWidgets)
{
var widget = supportedWidgets[i];
// store the unresolved prompt to restore it after sending the resolved prompt to the backend
// use of .innerText which keep \n symbols in order to not break multilines support
widget.value_initial = widget.input_div.value;
// resolve the string
var prompt = widget.input_div.value;
while (prompt.replace("\\{", "").includes('{') && prompt.replace("\\}", "").includes('}')) {
const startIndex = prompt.replace("\\{", "00").indexOf('{');
const endIndex = prompt.replace("\\}", "00").indexOf('}');
const optionsString = prompt.substring(startIndex + 1, endIndex);
const options = optionsString.split('|');
const randomIndex = Math.floor(Math.random() * options.length);
const randomOption = options[randomIndex];
prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1);
}
widget.value = prompt;
}
}
}
for (let y in n.widgets) {
if (!Object.hasOwn(n.widgets[y], 'to_randomize')) { //don't include "Random seed after every gen" in prompt.
if (n.widgets[y].dynamic_prompt && n.widgets[y].dynamic_prompt === true) {
input_[n.widgets[y].name] = n.widgets[y].value.replace("\\{", "{").replace("\\}", "}");
} else {
input_[n.widgets[y].name] = n.widgets[y].value;
}
}
}
for (let y in n.inputs) {
let parent_node = n.getInputNode(y);
if (parent_node) {
for (let z in parent_node.outputs) {
let c_nodes = parent_node.getOutputNodes(z);
// console.log(c_nodes, z);
if (c_nodes) {
for (let zz in c_nodes) {
if (c_nodes[zz].id == n.id && parent_node.outputs[z].links.includes(n.inputs[y].link)) {
input_[n.inputs[y].name] = [String(parent_node.id), parseInt(z)];
break;
}
}
}
}
}
}
let node = {}
node['inputs'] = input_;
node['class_type'] = n.class_comfy;
// inputs = x['inputs']
// inputs['name'], inputs['id']
// console.log(x, n);
// console.log(node);
output[String(n.id)] = node;
}
return output;
}
function closeModal() {
var modal = document.getElementById("myErrorModal");
modal.style.display = "none";
}
function showModal(text) {
var modal = document.getElementById("myErrorModal");
var modalText = document.getElementById("modal-text");
modalText.innerHTML = text;
modal.style.display = "block";
var closeBtn = modal.getElementsByClassName("close")[0];
closeBtn.onclick = function(event) {closeModal();}
}
function promptPosted(data)
{
if (data.status != 200) {
data.text().then(dt => showModal(dt));
return;
}
let s = graph.serialize();
let output = {};
// console.log(s['nodes']);
nodes = s['nodes']
for (let x in nodes) {
let n = graph.getNodeById(nodes[x].id);
for (let w in n.widgets) {
let wid = n.widgets[w];
if (Object.hasOwn(wid, 'to_randomize')) {
if (wid.value) {
wid.to_randomize.value = Math.floor(Math.random() * 1125899906842624);
}
}
}
}
canvas.draw(true, true);
loadQueueIfVisible();
}
return;
function postPrompt(number) {
let prompt = graphToPrompt();
......@@ -676,13 +50,6 @@ function postPrompt(number) {
}
}
function promptToGraph(prompt) {
for (let x in prompt) {
}
}
function prompt_file_load(file)
{
if (file.type === 'image/png') {
......@@ -1017,14 +384,6 @@ function loadItems(type) {
}).catch((response) => {console.log(response)});
}
function loadQueueIfVisible()
{
var queue_div = document.getElementById("queuebutton-content");
if (queue_div.style.display == 'block') {
loadQueue();
}
}
function seeItems(type) {
var queue_div = document.getElementById(type + "button-content");
if (queue_div.style.display == 'block') {
......
class ComfyApi {
async getNodeDefs() {
const resp = await fetch("object_info", { cache: "no-store" });
return await resp.json();
}
async queuePrompt(number, { output, workflow }) {
const body = {
client_id: this.clientId,
prompt: output,
extra_data: { extra_pnginfo: { workflow } },
};
if (number === -1) {
body.front = true;
} else if (number != 0) {
body.number = number;
}
const res = await fetch("/prompt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status !== 200) {
throw {
response: await res.text(),
};
}
}
}
export const api = new ComfyApi();
import { ComfyWidgets } from "./widgets.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
class ComfyDialog {
constructor() {
this.element = document.createElement("div");
this.element.classList.add("comfy-modal");
const content = document.createElement("div");
content.classList.add("comfy-modal-content");
this.textElement = document.createElement("p");
content.append(this.textElement);
const closeBtn = document.createElement("button");
closeBtn.type = "button";
closeBtn.textContent = "CLOSE";
content.append(closeBtn);
closeBtn.onclick = () => this.close();
this.element.append(content);
document.body.append(this.element);
}
close() {
this.element.style.display = "none";
}
show(html) {
this.textElement.innerHTML = html;
this.element.style.display = "flex";
}
}
class ComfyQueue {
constructor() {
this.element = document.createElement("div");
}
async update() {
if (this.element.style.display !== "none") {
await this.load();
}
}
async show() {
this.element.style.display = "block";
await this.load();
}
async load() {
const queue = await api.getQueue();
}
hide() {
this.element.style.display = "none";
}
}
class ComfyUI {
constructor(app) {
this.app = app;
this.menuContainer = document.createElement("div");
this.menuContainer.classList.add("comfy-menu");
document.body.append(this.menuContainer);
this.dialog = new ComfyDialog();
this.queue = new ComfyQueue();
}
}
class ComfyApp {
constructor() {
this.ui = new ComfyUI(this);
this.nodeOutputs = {};
this.extensions = [
{
name: "TestExtension",
init(app) {
console.log("[ext:init]", app);
},
setup(app) {
console.log("[ext:setup]", app);
},
addCustomNodeDefs(defs, app) {
console.log("[ext:addCustomNodeDefs]", defs, app);
},
loadedGraphNode(node, app) {
// console.log("[ext:loadedGraphNode]", node, app);
},
getCustomWidgets(app) {
console.log("[ext:getCustomWidgets]", app);
return {};
},
beforeRegisterNode(nodeType, nodeData, app) {
// console.log("[ext:beforeRegisterNode]", nodeType, nodeData, app);
},
registerCustomNodes(app) {
console.log("[ext:registerCustomNodes]", app);
},
},
];
}
#log(message, ...other) {
console.log("[comfy]", message, ...other);
}
#error(message, ...other) {
console.error("[comfy]", message, ...other);
}
#invokeExtensions(method, ...args) {
let results = [];
for (const ext of this.extensions) {
if (method in ext) {
try {
results.push(ext[method](...args, this));
} catch (error) {
this.#error(
`Error calling extension '${ext.name}' method '${method}'`,
{ error },
{ extension: ext },
{ args }
);
}
}
}
return results;
}
async #invokeExtensionsAsync(method, ...args) {
return await Promise.all(
this.extensions.map(async (ext) => {
if (method in ext) {
try {
return await ext[method](...args, this);
} catch (error) {
this.#error(
`Error calling extension '${ext.name}' method '${method}'`,
{ error },
{ extension: ext },
{ args }
);
}
}
})
);
}
#addNodeContextMenuHandler(node) {
node.prototype.getExtraMenuOptions = function (_, options) {
if (this.imgs) {
// If this node has images then we add an open in new tab item
let img;
if (this.imageIndex != null) {
// An image is selected so select that
img = this.imgs[this.imageIndex];
} else if (this.overIndex != null) {
// No image is selected but one is hovered
img = this.imgs[this.overIndex];
}
if (img) {
options.unshift({
content: "Open Image",
callback: () => window.open(img.src, "_blank"),
});
}
}
};
}
#addDrawBackgroundHandler(node) {
const app = this;
node.prototype.onDrawBackground = function (ctx) {
if (!this.flags.collapsed) {
const output = app.nodeOutputs[this.id + ""];
if (output && output.images) {
if (this.images !== output.images) {
this.images = output.images;
this.imgs = null;
this.imageIndex = null;
Promise.all(
output.images.map((src) => {
return new Promise((r) => {
const img = new Image();
img.onload = () => r(img);
img.onerror = () => r(null);
img.src = "/view/" + src;
});
})
).then((imgs) => {
if (this.images === output.images) {
this.imgs = imgs.filter(Boolean);
if (this.size[1] < 100) {
this.size[1] = 250;
}
app.graph.setDirtyCanvas(true);
}
});
}
if (this.imgs) {
const canvas = graph.list_of_graphcanvas[0];
const mouse = canvas.graph_mouse;
if (!canvas.pointer_is_down && this.pointerDown) {
if (mouse[0] === this.pointerDown.pos[0] && mouse[1] === this.pointerDown.pos[1]) {
this.imageIndex = this.pointerDown.index;
}
this.pointerDown = null;
}
let w = this.imgs[0].naturalWidth;
let h = this.imgs[0].naturalHeight;
let imageIndex = this.imageIndex;
const numImages = this.imgs.length;
if (numImages === 1 && !imageIndex) {
this.imageIndex = imageIndex = 0;
}
let shiftY = this.type === "SaveImage" ? 55 : 0;
let dw = this.size[0];
let dh = this.size[1];
dh -= shiftY;
if (imageIndex == null) {
let best = 0;
let cellWidth;
let cellHeight;
let cols = 0;
let shiftX = 0;
for (let c = 1; c <= numImages; c++) {
const rows = Math.ceil(numImages / c);
const cW = dw / c;
const cH = dh / rows;
const scaleX = cW / w;
const scaleY = cH / h;
const scale = Math.min(scaleX, scaleY, 1);
const imageW = w * scale;
const imageH = h * scale;
const area = imageW * imageH * numImages;
if (area > best) {
best = area;
cellWidth = imageW;
cellHeight = imageH;
cols = c;
shiftX = c * ((cW - imageW) / 2);
}
}
let anyHovered = false;
this.imageRects = [];
for (let i = 0; i < numImages; i++) {
const img = this.imgs[i];
const row = Math.floor(i / cols);
const col = i % cols;
const x = col * cellWidth + shiftX;
const y = row * cellHeight + shiftY;
if (!anyHovered) {
anyHovered = LiteGraph.isInsideRectangle(
mouse[0],
mouse[1],
x + this.pos[0],
y + this.pos[1],
cellWidth,
cellHeight
);
if (anyHovered) {
this.overIndex = i;
let value = 110;
if (canvas.pointer_is_down) {
if (!this.pointerDown || this.pointerDown.index !== i) {
this.pointerDown = { index: i, pos: [...mouse] };
}
value = 125;
}
ctx.filter = `contrast(${value}%) brightness(${value}%)`;
canvas.canvas.style.cursor = "pointer";
}
}
this.imageRects.push([x, y, cellWidth, cellHeight]);
ctx.drawImage(img, x, y, cellWidth, cellHeight);
ctx.filter = "none";
}
if (!anyHovered) {
this.pointerDown = null;
this.overIndex = null;
}
} else {
// Draw individual
const scaleX = dw / w;
const scaleY = dh / h;
const scale = Math.min(scaleX, scaleY, 1);
w *= scale;
h *= scale;
let x = (dw - w) / 2;
let y = (dh - h) / 2 + shiftY;
ctx.drawImage(this.imgs[imageIndex], x, y, w, h);
const drawButton = (x, y, sz, text) => {
const hovered = LiteGraph.isInsideRectangle(
mouse[0],
mouse[1],
x + this.pos[0],
y + this.pos[1],
sz,
sz
);
let fill = "#333";
let textFill = "#fff";
let isClicking = false;
if (hovered) {
canvas.canvas.style.cursor = "pointer";
if (canvas.pointer_is_down) {
fill = "#1e90ff";
isClicking = true;
} else {
fill = "#eee";
textFill = "#000";
}
} else {
this.pointerWasDown = null;
}
ctx.fillStyle = fill;
ctx.beginPath();
ctx.roundRect(x, y, sz, sz, [4]);
ctx.fill();
ctx.fillStyle = textFill;
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(text, x + 15, y + 20);
return isClicking;
};
if (numImages > 1) {
if (drawButton(x + w - 35, y + h - 35, 30, `${this.imageIndex + 1}/${numImages}`)) {
let i = this.imageIndex + 1 >= numImages ? 0 : this.imageIndex + 1;
if (!this.pointerDown || !this.pointerDown.index === i) {
this.pointerDown = { index: i, pos: [...mouse] };
}
}
if (drawButton(x + w - 35, y + 5, 30, `x`)) {
if (!this.pointerDown || !this.pointerDown.index === null) {
this.pointerDown = { index: null, pos: [...mouse] };
}
}
}
}
}
}
}
};
}
/**
* Set up the app on the page
*/
async setup() {
// Create and mount the LiteGraph in the DOM
const canvasEl = Object.assign(document.createElement("canvas"), { id: "graph-canvas" });
document.body.prepend(canvasEl);
this.graph = new LGraph();
const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph));
this.ctx = canvasEl.getContext("2d");
this.graph.start();
function resizeCanvas() {
canvasEl.width = canvasEl.offsetWidth;
canvasEl.height = canvasEl.offsetHeight;
canvas.draw(true, true);
}
// Ensure the canvas fills the window
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
await this.#invokeExtensionsAsync("init");
await this.registerNodes();
// Load previous workflow
let restored = false;
try {
const json = localStorage.getItem("workflow");
if (json) {
const workflow = JSON.parse(json);
this.loadGraphData(workflow);
restored = true;
}
} catch (err) {}
// We failed to restore a workflow so load the default
if (!restored) {
this.loadGraphData(defaultGraph);
}
// Save current workflow automatically
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000);
await this.#invokeExtensionsAsync("setup");
}
async registerNodes() {
const app = this;
// Load node definitions from the backend
const defs = await api.getNodeDefs();
await this.#invokeExtensionsAsync("addCustomNodeDefs", defs);
// Generate list of known widgets
const widgets = Object.assign(
{},
ComfyWidgets,
...(await this.#invokeExtensionsAsync("getCustomWidgets")).filter(Boolean)
);
// Register a node for each definition
for (const nodeId in defs) {
const nodeData = defs[nodeId];
const node = Object.assign(
function ComfyNode() {
const inputs = nodeData["input"]["required"];
const config = { minWidth: 1, minHeight: 1 };
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
if (Array.isArray(type)) {
// Enums e.g. latent rotation
this.addWidget("combo", inputName, type[0], () => {}, { values: type });
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
}
}
const s = this.computeSize();
s[0] = Math.max(config.minWidth, s[0] * 1.5);
s[1] = Math.max(config.minHeight, s[1]);
this.size = s;
this.serialize_widgets = true;
},
{
title: nodeData.name,
comfyClass: nodeData.name,
}
);
node.prototype.comfyClass = nodeData.name;
this.#addNodeContextMenuHandler(node);
this.#addDrawBackgroundHandler(node, app);
await this.#invokeExtensionsAsync("beforeRegisterNode", node, nodeData);
LiteGraph.registerNodeType(nodeId, node);
node.category = nodeData.category;
}
await this.#invokeExtensionsAsync("registerCustomNodes");
}
/**
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
*/
loadGraphData(graphData) {
this.graph.configure(graphData);
for (const node of this.graph._nodes) {
const size = node.computeSize();
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
if (node.widgets) {
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
for (let widget of node.widgets) {
if (node.type == "KSampler" || node.type == "KSamplerAdvanced") {
if (widget.name == "sampler_name") {
if (widget.value.startsWith("sample_")) {
wid.value = widget.value.slice(7);
}
}
}
}
}
this.#invokeExtensions("loadedGraphNode", node);
}
}
graphToPrompt() {
// TODO: Implement dynamic prompts
const workflow = this.graph.serialize();
const output = {};
for (const n of workflow.nodes) {
const inputs = {};
const node = this.graph.getNodeById(n.id);
const widgets = node.widgets;
// Store all widget values
if (widgets) {
for (const widget of widgets) {
if (widget.options.serialize !== false) {
inputs[widget.name] = widget.value;
}
}
}
// Store all node links
for (let i in node.inputs) {
const link = node.getInputLink(i);
if (link) {
inputs[node.inputs[i].name] = [String(link.origin_id), parseInt(link.origin_slot)];
}
}
output[String(node.id)] = {
inputs,
class_type: node.comfyClass,
};
}
return { workflow, output };
}
async queuePrompt(number) {
const p = this.graphToPrompt();
try {
await api.queuePrompt(number, p);
} catch (error) {
this.ui.dialog.show(error.response || error.toString());
return;
}
for (const n of p.workflow.nodes) {
const node = graph.getNodeById(n.id);
if (node.widgets) {
for (const widget of node.widgets) {
// Allow widgets to run callbacks after a prompt has been queued
// e.g. random seed after every gen
if (widget.afterQueued) {
widget.afterQueued();
}
}
}
}
this.canvas.draw(true, true);
await this.ui.queue.update();
}
}
export const app = new ComfyApp();
export const defaultGraph = {
last_node_id: 9,
last_link_id: 9,
nodes: [
{
id: 7,
type: "CLIPTextEncode",
pos: [413, 389],
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
flags: {},
order: 3,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
properties: {},
widgets_values: ["bad hands"],
},
{
id: 6,
type: "CLIPTextEncode",
pos: [415, 186],
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
flags: {},
order: 2,
mode: 0,
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
properties: {},
widgets_values: ["masterpiece best quality girl"],
},
{
id: 5,
type: "EmptyLatentImage",
pos: [473, 609],
size: { 0: 315, 1: 106 },
flags: {},
order: 1,
mode: 0,
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
properties: {},
widgets_values: [512, 512, 1],
},
{
id: 3,
type: "KSampler",
pos: [863, 186],
size: { 0: 315, 1: 262 },
flags: {},
order: 4,
mode: 0,
inputs: [
{ name: "model", type: "MODEL", link: 1 },
{ name: "positive", type: "CONDITIONING", link: 4 },
{ name: "negative", type: "CONDITIONING", link: 6 },
{ name: "latent_image", type: "LATENT", link: 2 },
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
widgets_values: [8566257, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,
type: "VAEDecode",
pos: [1209, 188],
size: { 0: 210, 1: 46 },
flags: {},
order: 5,
mode: 0,
inputs: [
{ name: "samples", type: "LATENT", link: 7 },
{ name: "vae", type: "VAE", link: 8 },
],
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
properties: {},
},
{
id: 9,
type: "SaveImage",
pos: [1451, 189],
size: { 0: 210, 1: 26 },
flags: {},
order: 6,
mode: 0,
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
properties: {},
},
{
id: 4,
type: "CheckpointLoader",
pos: [26, 474],
size: { 0: 315, 1: 122 },
flags: {},
order: 0,
mode: 0,
outputs: [
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
],
properties: {},
widgets_values: ["v1-inference.yaml", "v1-5-pruned-emaonly.ckpt"],
},
],
links: [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"],
],
groups: [],
config: {},
extra: {},
version: 0.4,
};
function getNumberDefaults(inputData, defaultStep) {
let defaultVal = inputData[1]["default"];
let { min, max, step } = inputData[1];
if (defaultVal == undefined) defaultVal = 0;
if (min == undefined) min = 0;
if (max == undefined) max = 2048;
if (step == undefined) step = defaultStep;
return { val: defaultVal, config: { min, max, step: 10.0 * step } };
}
function seedWidget(node, inputName, inputData) {
const seed = ComfyWidgets.INT(node, inputName, inputData);
const randomize = node.addWidget("toggle", "Random seed after every gen", true, function (v) {}, {
on: "enabled",
off: "disabled",
serialize: false, // Don't include this in prompt.
});
randomize.afterQueued = () => {
if (randomize.value) {
seed.widget.value = Math.floor(Math.random() * 1125899906842624);
}
};
return { widget: seed, randomize };
}
function addMultilineWidget(node, name, defaultVal, dynamicPrompt, app) {
const widget = {
type: "customtext",
name,
get value() {
return this.inputEl.value;
},
set value(x) {
this.inputEl.value = x;
},
options: {
dynamicPrompt,
},
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
const visible = app.canvas.ds.scale > 0.5;
const t = ctx.getTransform();
const margin = 10;
console.log("back you go")
Object.assign(this.inputEl.style, {
left: `${t.a * margin + t.e}px`,
top: `${t.d * (y + widgetHeight - margin) + t.f}px`,
width: `${(widgetWidth - margin * 2 - 3) * t.a}px`,
height: `${(this.parent.size[1] - (y + widgetHeight) - 3) * t.d}px`,
position: "absolute",
zIndex: 1,
fontSize: `${t.d * 10.0}px`,
});
this.inputEl.hidden = !visible;
},
};
widget.inputEl = document.createElement("textarea");
widget.inputEl.className = "comfy-multiline-input";
widget.inputEl.value = defaultVal;
document.addEventListener("click", function (event) {
if (!widget.inputEl.contains(event.target)) {
widget.inputEl.blur();
}
});
widget.parent = node;
document.body.appendChild(widget.inputEl);
node.addCustomWidget(widget);
node.onRemoved = function () {
// When removing this node we need to remove the input from the DOM
for (let y in this.widgets) {
if (this.widgets[y].inputEl) {
this.widgets[y].inputEl.remove();
}
}
};
return { minWidth: 400, minHeight: 200, widget };
}
export const ComfyWidgets = {
"INT:seed": seedWidget,
"INT:noise_seed": seedWidget,
FLOAT(node, inputName, inputData) {
const { val, config } = getNumberDefaults(inputData, 0.5);
return { widget: node.addWidget("number", inputName, val, () => {}, config) };
},
INT(node, inputName, inputData) {
const { val, config } = getNumberDefaults(inputData, 1);
return {
widget: node.addWidget(
"number",
inputName,
val,
function (v) {
const s = this.options.step / 10;
this.value = Math.round(v / s) * s;
},
config
),
};
},
STRING(node, inputName, inputData, app) {
const defaultVal = inputData[1].default || "";
const multiline = !!inputData[1].multiline;
const dynamicPrompt = !!inputData[1].dynamic_prompt;
if (multiline) {
return addMultilineWidget(node, inputName, defaultVal, dynamicPrompt, app);
} else {
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, { dynamicPrompt }) };
}
},
};
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#graph-canvas {
width: 100%;
height: 100%;
}
.comfy-multiline-input {
background-color: #ffffff;
overflow: hidden;
overflow-y: auto;
padding: 2px;
resize: none;
border: none;
}
.comfy-modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 100; /* Sit on top */
padding: 30px 30px 10px 30px;
background-color: #ff0000; /* Modal background */
box-shadow: 0px 0px 20px #888888;
border-radius: 10px;
text-align: center;
top: 50%;
left: 50%;
max-width: 80vw;
max-height: 80vh;
transform: translate(-50%, -50%);
overflow: hidden;
}
.comfy-modal-content {
display: flex;
flex-direction: column;
}
.comfy-modal p {
overflow: auto;
white-space: pre-line; /* This will respect line breaks */
margin-bottom: 20px; /* Add some margin between the text and the close button*/
}
.comfy-modal button {
cursor: pointer;
color: #aaaaaa;
border: none;
background-color: transparent;
font-size: 24px;
font-weight: bold;
width: 100%;
}
.comfy-modal button:hover,
.comfy-modal button:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #202020;
}
.comfy-multiline-input {
background-color: #202020;
color: white;
}
}
@media only screen and (max-height: 850px) {
#menu {
margin-top: -70px;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment