From 87bbf5d265f4e3d5311e1accd64c05309631a0cc Mon Sep 17 00:00:00 2001 From: AlekPet Date: Mon, 30 Dec 2024 14:13:39 +0300 Subject: [PATCH 01/11] initial users customs fonts --- PainterNode/css/painter_node_fonts.css | 5 ++++ PainterNode/css/painter_node_user_fonts.css | 26 ++++++++++++++++++ .../fonts/HarryFontYourssincerelylarry.ttf | Bin 0 -> 18332 bytes PainterNode/fonts/pricedown.otf | Bin 0 -> 58640 bytes PainterNode/fonts/put files fonts here | 0 PainterNode/js/painter_node.js | 25 ++++++++++++++++- __init__.py | 2 +- 7 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 PainterNode/css/painter_node_fonts.css create mode 100644 PainterNode/css/painter_node_user_fonts.css create mode 100644 PainterNode/fonts/HarryFontYourssincerelylarry.ttf create mode 100644 PainterNode/fonts/pricedown.otf create mode 100644 PainterNode/fonts/put files fonts here diff --git a/PainterNode/css/painter_node_fonts.css b/PainterNode/css/painter_node_fonts.css new file mode 100644 index 0000000..f9b6b74 --- /dev/null +++ b/PainterNode/css/painter_node_fonts.css @@ -0,0 +1,5 @@ +/* Default styles */ +@import url("https://fonts.googleapis.com/css2?family=Jacquard+12&display=swap"); + +/* User fonts */ +@import url("./painter_node_user_fonts.css"); diff --git a/PainterNode/css/painter_node_user_fonts.css b/PainterNode/css/painter_node_user_fonts.css new file mode 100644 index 0000000..33e091c --- /dev/null +++ b/PainterNode/css/painter_node_user_fonts.css @@ -0,0 +1,26 @@ +/* Example add font file for different formats, and supports old browsers: */ +/* +@font-face { + font-family: 'MyNameFont'; + src: url('webfont.eot'); + src: url('webfont.eot?#iefix') format('embedded-opentype'), + url('webfont.woff') format('woff'), + url('webfont.ttf') format('truetype'), + url("webfont.otf") format("opentype"); + url('webfont.svg#svgFontName') format('svg'); +} +*/ + +/* User fonts files */ + +/* Pricedown font */ +@font-face { + font-family: "Pricedown"; + src: url("../../fonts/pricedown.otf") format("opentype"); +} + +/* Harry_font__YOURSSINCERELYLARRY_ font */ +@font-face { + font-family: "Harry_font__YOURSSINCERELYLARRY_"; + src: url("../../fonts/HarryFontYourssincerelylarry.ttf") format("truetype"); +} diff --git a/PainterNode/fonts/HarryFontYourssincerelylarry.ttf b/PainterNode/fonts/HarryFontYourssincerelylarry.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e57cd7d55ae83fdd3e730f227216fd68ec2d55ac GIT binary patch literal 18332 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 zΜ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 literal 0 HcmV?d00001 diff --git a/PainterNode/fonts/pricedown.otf b/PainterNode/fonts/pricedown.otf new file mode 100644 index 0000000000000000000000000000000000000000..f06a2bee78b58b4a4e7f7eeb14cc6213119d804c GIT binary patch literal 58640 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
?4QJOT332{hy)VXu$Y? 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 literal 0 HcmV?d00001 diff --git a/PainterNode/fonts/put files fonts here b/PainterNode/fonts/put files fonts here new file mode 100644 index 0000000..e69de29 diff --git a/PainterNode/js/painter_node.js b/PainterNode/js/painter_node.js index 835b987..4b1b0d2 100644 --- a/PainterNode/js/painter_node.js +++ b/PainterNode/js/painter_node.js @@ -372,11 +372,31 @@ class Painter { }); this.painter_bg_setting.appendChild(this.bgImageFile); + + this.getLoadedFonts(); + this.changePropertyBrush(); this.createBrushesToolbar(); this.bindEvents(); } + async getLoadedFonts() { + const getListFonts = async () => { + const fontFaces = await document.fonts.ready; + const loadedFonts = []; + + document.fonts.forEach((font) => { + loadedFonts.push(font.family); + }); + console.log("Loaded fonts"); + return [...new Set(loadedFonts)]; + }; + + await getListFonts().then((fonts) => { + fonts.forEach((font) => (this.fonts[font] = font)); + }); + } + setValueElementsLS() { this.painter_wrapper_settings.remove(); this.mainSettings(); @@ -993,8 +1013,10 @@ class Painter { // Select front event selectFontFamily.onchange = (e) => { - if (this.getActiveStyle("fontFamily") != selectFontFamily.value) + if (this.getActiveStyle("fontFamily") != selectFontFamily.value) { this.setActiveStyle("fontFamily", selectFontFamily.value); + this.uploadPaintFile(this.node.name); + } }; property_textbox.append( @@ -2528,6 +2550,7 @@ app.registerExtension({ async init(app) { // Add styles addStylesheet("css/painternode/painter_node_styles.css", import.meta.url); + addStylesheet("css/painternode/painter_node_fonts.css", import.meta.url); // Add settings params painter node app.ui.settings.addSetting({ diff --git a/__init__.py b/__init__.py index 0e5169f..e3606f3 100644 --- a/__init__.py +++ b/__init__.py @@ -204,7 +204,7 @@ def install_node(nodeElement): log(f"* Node <{nodeElement}> is found, installing...") web_extensions_dir = os.path.join(extension_folder, extension_dirs[0]) - extensions_dirs_copy = ["js", "css", "assets", "lib"] + extensions_dirs_copy = ["js", "css", "assets", "lib", "fonts"] for dir_name in extensions_dirs_copy: folder_curr = os.path.join(extension_folder, nodeElement, dir_name) if os.path.exists(folder_curr): From ad3fc80453ba496e896761b3a127e5781f5acd5a Mon Sep 17 00:00:00 2001 From: AlekPet Date: Wed, 1 Jan 2025 15:51:10 +0300 Subject: [PATCH 02/11] updated the code fixes PoseNode, PainterNode saves images Add user custom fonts to PainterNode (may not work quite correctly, test version). --- PainterNode/README.md | 21 ++++++ PainterNode/css/painter_node_user_fonts.css | 5 +- PainterNode/js/painter_node.js | 49 +++++++----- PoseNode/js/pose_node.js | 84 ++++++++++++++++++--- pyproject.toml | 2 +- 5 files changed, 127 insertions(+), 34 deletions(-) diff --git a/PainterNode/README.md b/PainterNode/README.md index cd8a64a..9b2d168 100644 --- a/PainterNode/README.md +++ b/PainterNode/README.md @@ -113,3 +113,24 @@ npm run brushes - Update image (not update the images of the node with piping in image (**Note:** you must enable this option so that the mask does not change)) ![Screenshot Piping](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/raw/master/PainterNode/pipingImage.jpg) + +# User custom fonts + +> To add custom fonts, they must be written in the css file: `PainterNode\css\painter_node_user_fonts.css` + +### Two variants: + +**Variant 1**: Path to fonts, fonts puts in the `PainterNode\fonts`, adn add to the code in the css. + +```css +@font-face { + font-family: "Pricedown"; + src: url("./../../fonts/painternode/pricedown.otf") format("opentype"); +} +``` + +**Variant 2**: Add url to font in the `painter_node_user_fonts.css`, example used link [google fonts.](https://fonts.google.com/) + +```css +@import url("https://fonts.googleapis.com/css2?family=Jacquard+12&display=swap"); +``` diff --git a/PainterNode/css/painter_node_user_fonts.css b/PainterNode/css/painter_node_user_fonts.css index 33e091c..0621b0b 100644 --- a/PainterNode/css/painter_node_user_fonts.css +++ b/PainterNode/css/painter_node_user_fonts.css @@ -16,11 +16,12 @@ /* Pricedown font */ @font-face { font-family: "Pricedown"; - src: url("../../fonts/pricedown.otf") format("opentype"); + src: url("./../../fonts/painternode/pricedown.otf") format("opentype"); } /* Harry_font__YOURSSINCERELYLARRY_ font */ @font-face { font-family: "Harry_font__YOURSSINCERELYLARRY_"; - src: url("../../fonts/HarryFontYourssincerelylarry.ttf") format("truetype"); + src: url("./../../fonts/painternode/HarryFontYourssincerelylarry.ttf") + format("truetype"); } diff --git a/PainterNode/js/painter_node.js b/PainterNode/js/painter_node.js index 4b1b0d2..4775129 100644 --- a/PainterNode/js/painter_node.js +++ b/PainterNode/js/painter_node.js @@ -109,15 +109,15 @@ class Painter { // this.undo_history = this.node.LS_Cls.LS_Painters.undo_history || []; // this.redo_history = this.node.LS_Cls.LS_Painters.redo_history || []; - this.fonts = { - Arial: "arial", - "Times New Roman": "Times New Roman", - Verdana: "verdana", - Georgia: "georgia", - Courier: "courier", - "Comic Sans MS": "comic sans ms", - Impact: "impact", - }; + this.fonts = [ + "Arial", + "Times New Roman", + "Verdana", + "Georgia", + "Courier", + "Comic Sans MS", + "Impact", + ]; this.bringFrontSelected = true; @@ -373,7 +373,7 @@ class Painter { this.painter_bg_setting.appendChild(this.bgImageFile); - this.getLoadedFonts(); + this.getLoadedFonts(); // dev users fonts this.changePropertyBrush(); this.createBrushesToolbar(); @@ -393,7 +393,7 @@ class Painter { }; await getListFonts().then((fonts) => { - fonts.forEach((font) => (this.fonts[font] = font)); + this.fonts = this.fonts.concat(fonts); }); } @@ -1000,14 +1000,15 @@ class Painter { }); const separator = makeElement("div", { class: ["separator"] }); const selectFontFamily = makeElement("select", { + dataset: { prop: "prop_fontFamily" }, class: ["font_family_select"], }); - for (let f in this.fonts) { + for (let font of this.fonts) { const option = makeElement("option"); - if (f === "Arial") option.setAttribute("selected", true); - option.value = this.fonts[f]; - option.textContent = f; + if (font === "Arial") option.setAttribute("selected", true); + option.value = font; + option.textContent = font; selectFontFamily.appendChild(option); } @@ -1599,12 +1600,20 @@ class Painter { if (!targets || targets.length == 0) return; // Selected tools - const setProps = (style, check) => { + const setProps = (style, value) => { const propEl = this.painter_drawning_box_property.querySelector( - `#prop_${style}` + `[data-prop=prop_${style}]` ); - if (propEl) propEl.classList[check ? "remove" : "add"]("active"); + if (propEl) { + switch (propEl.dataset.prop) { + case "prop_fontFamily": { + propEl.value = value; + } + default: + propEl.classList[value ? "remove" : "add"]("active"); + } + } }; targets.forEach((target) => { @@ -1624,6 +1633,8 @@ class Painter { "underline", Boolean(this.getActiveStyle("underline", target)) == false ); + + setProps("fontFamily", this.getActiveStyle("fontFamily", target)); } if ( @@ -2128,7 +2139,7 @@ class Painter { await new Promise((res) => { const uploadFile = async (blobFile) => { try { - const resp = await fetch("/upload/image", { + const resp = await api.fetchApi("/upload/image", { method: "POST", body: blobFile, }); diff --git a/PoseNode/js/pose_node.js b/PoseNode/js/pose_node.js index f89371c..6524d6d 100644 --- a/PoseNode/js/pose_node.js +++ b/PoseNode/js/pose_node.js @@ -2,10 +2,11 @@ * Title: Set Poses in ComflyUI from ControlNet * Author: AlekPet * Description: I rewrote the main.js file as a class, from fkunn1326's openpose-editor (https://github.com/fkunn1326/openpose-editor/blob/master/javascript/main.js) - * Version: 2024.10.13 + * Version: 2025.01.01 * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet */ +import { api } from "../../scripts/api.js"; import { app } from "../../scripts/app.js"; import { $el } from "../../scripts/ui.js"; import { addStylesheet } from "../../scripts/utils.js"; @@ -105,8 +106,13 @@ class OpenPose { this.visibleEyes = true; this.flipped = false; this.node = node; + + this.maxSizeNodeW = 1024; + this.maxSizehNodeH = 200; + this.undo_history = LS_Poses[node.name].undo_history || []; this.redo_history = LS_Poses[node.name].redo_history || []; + this.history_change = false; this.currentCanvasSize = { width: 512, height: 512 }; @@ -123,8 +129,11 @@ class OpenPose { this.currentCanvasSize = { width: new_width, height: new_height }; // Check isset local storage key currentCanvasSize - if (!LS_Poses[this.node.name].hasOwnProperty("currentCanvasSize")) - LS_Poses[this.node.name]["currentCanvasSize"] = {}; + if (!Object.hasOwn(LS_Poses, this.node.name)) { + LS_Poses[this.node.name] = { + currentCanvasSize: {}, + }; + } LS_Poses[this.node.name]["currentCanvasSize"] = this.currentCanvasSize; @@ -248,6 +257,7 @@ class OpenPose { this.canvas = new fabric.Canvas(this.canvas, { backgroundColor: "#000", preserveObjectStacking: true, + containerClass: "canvas-container-pose", }); const updateLines = (target) => { @@ -458,7 +468,7 @@ class OpenPose { const uploadFile = async (blobFile) => { try { - const resp = await fetch("/upload/image", { + const resp = await api.fetchApi("/upload/image", { method: "POST", body: blobFile, }); @@ -623,9 +633,9 @@ function createOpenPose(node, inputName, inputData, app) { height: `${20.0 * transform.d}px`, }); } - - element.hidden = !visible; }); + + this.openpose.hidden = !visible; }, }; @@ -641,6 +651,7 @@ function createOpenPose(node, inputName, inputData, app) { widget.openpose = node.openPose.canvas.wrapperEl; widget.parent = node; + widget.openpose.hidden = true; // Create elements undo, redo, clear history let panelButtons = $el("div.pose_panelButtons.comfy-menu-btns", [ @@ -786,6 +797,54 @@ function createOpenPose(node, inputName, inputData, app) { widget.openpose?.remove(); }; + node.onResize = function () { + let [w, h] = this.size; + let aspect_ratio = 1; + + if (node?.imgs && typeof this.imgs !== undefined) { + aspect_ratio = this.imgs[0].naturalHeight / this.imgs[0].naturalWidth; + } + let buffer = 90; + + if (w > this.openPose.maxSizeNodeW) + w = w - (w - this.openPose.maxSizeNodeW); + if (w < 200) w = 200; + + h = w * aspect_ratio + buffer; + + if (h < this.openPose.maxSizeNodeH) h = this.openPose.maxSizeNodeH + h / 2; + + this.size = [w, h]; + }; + + node.onDrawBackground = function (ctx) { + if (!this.flags.collapsed) { + node.openPose.canvas.wrapperEl.hidden = false; + if (this.imgs && this.imgs.length) { + if (app.canvas.ds.scale > 0.8) { + let [dw, dh] = this.size; + + let w = this.imgs[0].naturalWidth; + let h = this.imgs[0].naturalHeight; + + const scaleX = dw / w; + const scaleY = dh / h; + const scale = Math.min(scaleX, scaleY, 1); + + w *= scale / 8; + h *= scale / 8; + + let x = 5; + let y = dh - h - 5; + + ctx.drawImage(this.imgs[0], x, y, w, h); + } + } + } else { + node.openPose.canvas.wrapperEl.hidden = true; + } + }; + app.canvas.onDrawBackground = function () { // Draw node isnt fired once the node is off the screen // if it goes off screen quickly, the input may not be removed @@ -832,16 +891,16 @@ app.registerExtension({ if (nodeData.name === "PoseNode") { const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { + nodeType.prototype.onNodeCreated = async function () { const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; - let openPoseNode = app.graph._nodes.filter( - (wi) => wi.type == "PoseNode" - ), - nodeName = `Pose_${openPoseNode.length}`, - nodeNamePNG = `${nodeName}.png`; + const node_title = await this.getTitle(); + const node_id = this.id; + + const nodeName = `Pose_${node_id}`; + const nodeNamePNG = `${nodeName}.png`; console.log(`Create PoseNode: ${nodeNamePNG}`); @@ -859,6 +918,7 @@ app.registerExtension({ redo_history: [], currentCanvasSize: { width: 512, height: 512 }, }; + LS_Save(); } diff --git a/pyproject.toml b/pyproject.toml index ff61740..d808ba8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.37" +version = "1.0.38" license = { file = "LICENSE" } [project.urls] From 8562214047e53f7281b8bb6f6fc9785ae542d23d Mon Sep 17 00:00:00 2001 From: AlekPet Date: Wed, 1 Jan 2025 16:46:00 +0300 Subject: [PATCH 03/11] edit applying fonts to textbox --- PainterNode/js/painter_node.js | 55 ++++++++++++++++++++++------- PainterNode/lib/fontfaceobserver.js | 8 +++++ pyproject.toml | 2 +- 3 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 PainterNode/lib/fontfaceobserver.js diff --git a/PainterNode/js/painter_node.js b/PainterNode/js/painter_node.js index 4775129..049f190 100644 --- a/PainterNode/js/painter_node.js +++ b/PainterNode/js/painter_node.js @@ -22,6 +22,7 @@ import { isEmptyObject, THEMES_MODAL_WINDOW, } from "./utils.js"; +import "./lib/painternode/fontfaceobserver.js"; import { MyPaintManager } from "./lib/painternode/manager_mypaint.js"; // ================= FUNCTIONS ================ @@ -80,6 +81,17 @@ function resizeCanvas(node, sizes) { app.graph.setDirtyCanvas(true, false); }, 1000); } + +const getListFonts = async () => { + const fontFaces = await document.fonts.ready; + const loadedFonts = []; + + document.fonts.forEach((font) => { + loadedFonts.push(font.family); + }); + console.log("PainterNode: Loading fonts completed"); + return [...new Set(loadedFonts)]; +}; // ================= END FUNCTIONS ================ // ================= CLASS PAINTER ================ @@ -109,15 +121,15 @@ class Painter { // this.undo_history = this.node.LS_Cls.LS_Painters.undo_history || []; // this.redo_history = this.node.LS_Cls.LS_Painters.redo_history || []; - this.fonts = [ - "Arial", - "Times New Roman", - "Verdana", - "Georgia", - "Courier", - "Comic Sans MS", - "Impact", - ]; + this.fonts = { + Arial: { type: "default" }, + "Times New Roman": { type: "default" }, + Verdana: { type: "default" }, + Georgia: { type: "default" }, + Courier: { type: "default" }, + "Comic Sans MS": { type: "default" }, + Impact: { type: "default" }, + }; this.bringFrontSelected = true; @@ -388,12 +400,14 @@ class Painter { document.fonts.forEach((font) => { loadedFonts.push(font.family); }); - console.log("Loaded fonts"); + console.log("PainterNode: Loading fonts completed"); return [...new Set(loadedFonts)]; }; await getListFonts().then((fonts) => { - this.fonts = this.fonts.concat(fonts); + fonts.forEach((font) => { + this.fonts[font] = { type: "custom" }; + }); }); } @@ -1004,7 +1018,7 @@ class Painter { class: ["font_family_select"], }); - for (let font of this.fonts) { + for (let font in this.fonts) { const option = makeElement("option"); if (font === "Arial") option.setAttribute("selected", true); option.value = font; @@ -1015,7 +1029,22 @@ class Painter { // Select front event selectFontFamily.onchange = (e) => { if (this.getActiveStyle("fontFamily") != selectFontFamily.value) { - this.setActiveStyle("fontFamily", selectFontFamily.value); + if (this.fonts[selectFontFamily.value].type == "default") { + this.setActiveStyle("fontFamily", selectFontFamily.value); + return; + } + + const font = new FontFaceObserver(selectFontFamily.value); + font.load().then( + () => { + // console.log("Font is available"); + this.setActiveStyle("fontFamily", selectFontFamily.value); + }, + () => { + // console.log("Font not is available"); + } + ); + this.uploadPaintFile(this.node.name); } }; diff --git a/PainterNode/lib/fontfaceobserver.js b/PainterNode/lib/fontfaceobserver.js new file mode 100644 index 0000000..3177ec6 --- /dev/null +++ b/PainterNode/lib/fontfaceobserver.js @@ -0,0 +1,8 @@ +/* Font Face Observer v2.3.0 - © Bram Stein. License: BSD-3-Clause */(function(){function p(a,c){document.addEventListener?a.addEventListener("scroll",c,!1):a.attachEvent("scroll",c)}function u(a){document.body?a():document.addEventListener?document.addEventListener("DOMContentLoaded",function b(){document.removeEventListener("DOMContentLoaded",b);a()}):document.attachEvent("onreadystatechange",function g(){if("interactive"==document.readyState||"complete"==document.readyState)document.detachEvent("onreadystatechange",g),a()})};function w(a){this.g=document.createElement("div");this.g.setAttribute("aria-hidden","true");this.g.appendChild(document.createTextNode(a));this.h=document.createElement("span");this.i=document.createElement("span");this.m=document.createElement("span");this.j=document.createElement("span");this.l=-1;this.h.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.i.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;"; +this.j.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.m.style.cssText="display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;";this.h.appendChild(this.m);this.i.appendChild(this.j);this.g.appendChild(this.h);this.g.appendChild(this.i)} +function x(a,c){a.g.style.cssText="max-width:none;min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;white-space:nowrap;font-synthesis:none;font:"+c+";"}function B(a){var c=a.g.offsetWidth,b=c+100;a.j.style.width=b+"px";a.i.scrollLeft=b;a.h.scrollLeft=a.h.scrollWidth+100;return a.l!==c?(a.l=c,!0):!1}function C(a,c){function b(){var e=g;B(e)&&null!==e.g.parentNode&&c(e.l)}var g=a;p(a.h,b);p(a.i,b);B(a)};function D(a,c,b){c=c||{};b=b||window;this.family=a;this.style=c.style||"normal";this.weight=c.weight||"normal";this.stretch=c.stretch||"normal";this.context=b}var E=null,F=null,G=null,H=null;function I(a){null===F&&(M(a)&&/Apple/.test(window.navigator.vendor)?(a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(window.navigator.userAgent),F=!!a&&603>parseInt(a[1],10)):F=!1);return F}function M(a){null===H&&(H=!!a.document.fonts);return H} +function N(a,c){var b=a.style,g=a.weight;if(null===G){var e=document.createElement("div");try{e.style.font="condensed 100px sans-serif"}catch(q){}G=""!==e.style.font}return[b,g,G?a.stretch:"","100px",c].join(" ")} +D.prototype.load=function(a,c){var b=this,g=a||"BESbswy",e=0,q=c||3E3,J=(new Date).getTime();return new Promise(function(K,L){if(M(b.context)&&!I(b.context)){var O=new Promise(function(r,t){function h(){(new Date).getTime()-J>=q?t(Error(""+q+"ms timeout exceeded")):b.context.document.fonts.load(N(b,'"'+b.family+'"'),g).then(function(n){1<=n.length?r():setTimeout(h,25)},t)}h()}),P=new Promise(function(r,t){e=setTimeout(function(){t(Error(""+q+"ms timeout exceeded"))},q)});Promise.race([P,O]).then(function(){clearTimeout(e); +K(b)},L)}else u(function(){function r(){var d;if(d=-1!=k&&-1!=l||-1!=k&&-1!=m||-1!=l&&-1!=m)(d=k!=l&&k!=m&&l!=m)||(null===E&&(d=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),E=!!d&&(536>parseInt(d[1],10)||536===parseInt(d[1],10)&&11>=parseInt(d[2],10))),d=E&&(k==y&&l==y&&m==y||k==z&&l==z&&m==z||k==A&&l==A&&m==A)),d=!d;d&&(null!==f.parentNode&&f.parentNode.removeChild(f),clearTimeout(e),K(b))}function t(){if((new Date).getTime()-J>=q)null!==f.parentNode&&f.parentNode.removeChild(f), +L(Error(""+q+"ms timeout exceeded"));else{var d=b.context.document.hidden;if(!0===d||void 0===d)k=h.g.offsetWidth,l=n.g.offsetWidth,m=v.g.offsetWidth,r();e=setTimeout(t,50)}}var h=new w(g),n=new w(g),v=new w(g),k=-1,l=-1,m=-1,y=-1,z=-1,A=-1,f=document.createElement("div");f.dir="ltr";x(h,N(b,"sans-serif"));x(n,N(b,"serif"));x(v,N(b,"monospace"));f.appendChild(h.g);f.appendChild(n.g);f.appendChild(v.g);b.context.document.body.appendChild(f);y=h.g.offsetWidth;z=n.g.offsetWidth;A=v.g.offsetWidth;t(); +C(h,function(d){k=d;r()});x(h,N(b,'"'+b.family+'",sans-serif'));C(n,function(d){l=d;r()});x(n,N(b,'"'+b.family+'",serif'));C(v,function(d){m=d;r()});x(v,N(b,'"'+b.family+'",monospace'))})})};"object"===typeof module?module.exports=D:(window.FontFaceObserver=D,window.FontFaceObserver.prototype.load=D.prototype.load);}()); diff --git a/pyproject.toml b/pyproject.toml index d808ba8..a218018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.38" +version = "1.0.39" license = { file = "LICENSE" } [project.urls] From 77c20c1e278f796241b8587028f18f15b0aa5002 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Sun, 12 Jan 2025 10:16:23 +0300 Subject: [PATCH 04/11] fix contextmenu preview image, video and audio --- ExtrasNode/js/extras_node.js | 51 +++++++++++++++++++++++++++++------- pyproject.toml | 2 +- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index e28dc57..b73c1ef 100644 --- a/ExtrasNode/js/extras_node.js +++ b/ExtrasNode/js/extras_node.js @@ -72,7 +72,7 @@ let PreviewImageSize = PreviewImageSizeLS const SUPPORTS_FORMAT = { image: ["jpg", "jpeg", "bmp", "png", "gif", "tiff", "avif"], video: ["mp4", "webm"], - audio: ["ogg", "wav", "mp3"], + audio: ["ogg", "wav", "mp3", "webm"], }; const loadingContent = (src) => { @@ -86,15 +86,21 @@ const loadingContent = (src) => { img.onload = (e) => res({ raw: img, type: "image", src }); img.onerror = (err) => rej(err); img.src = src; - } else if (SUPPORTS_FORMAT.video.includes(ext)) { + } + + if (SUPPORTS_FORMAT.video.includes(ext)) { const video = document.querySelector(".preview_vid"); video.onerror = (err) => rej(err); video.addEventListener("canplay", (e) => { - res({ raw: video, type: "video", src }); + if (video.videoWidth !== 0) { + res({ raw: video, type: "video", src }); + } }); video.src = src; - } else if (SUPPORTS_FORMAT.audio.includes(ext)) { + } + + if (SUPPORTS_FORMAT.audio.includes(ext)) { const audio = document.querySelector(".preview_audio"); audio.onerror = (err) => rej(err); @@ -139,9 +145,10 @@ app.registerExtension({ style: { position: "absolute", maxWidth: "160px", - right: "calc(-1% - 160px)", + // right: "calc(-1% - 160px)", + zIndex: 9999999, }, - parent: this.root, + parent: document.body, }, [ $el("canvas.preview_img", { @@ -168,6 +175,28 @@ app.registerExtension({ this.root.preview_content_combo = preview_content_combo; } + const closeAllContextMenus = LiteGraph.closeAllContextMenus; + LiteGraph.closeAllContextMenus = function () { + closeAllContextMenus?.apply(this, arguments); + + if (document.querySelector(".preview_content_combo")) + document.querySelector(".preview_content_combo").remove(); + }; + + const closeItem = LiteGraph.ContextMenu.prototype.close; + LiteGraph.ContextMenu.prototype.close = function () { + closeItem?.apply(this, arguments); + + if (this.root?.preview_content_combo) { + this.root.preview_content_combo.remove(); + this.root.preview_content_combo = null; + } + }; + + this.root.addEventListener("blur", () => { + console.log("ok"); + }); + LiteGraph.pointerListenerAdd(element, "enter", (e) => { if (element?.dataset?.value) { const body_rect = document.body.getBoundingClientRect(); @@ -208,12 +237,14 @@ app.registerExtension({ root_rect.right + previewWidth > body_rect.width ) { this.root.preview_content_combo.style.left = `${ - -previewWidth - 10 + root_rect.left - previewWidth - 10 }px`; - this.root.preview_content_combo.style.right = "auto"; + // this.root.preview_content_combo.style.right = "auto"; } else { - this.root.preview_content_combo.style.left = "auto"; - this.root.preview_content_combo.style.right = `calc(-1% - ${this.root.preview_content_combo.style.maxWidth})`; + this.root.preview_content_combo.style.left = `${ + root_rect.left + root_rect.width + 10 + }px`; + // this.root.preview_content_combo.style.right = `calc(-1% - ${this.root.preview_content_combo.style.maxWidth})`; } ctx.clearRect(0, 0, canvas.width, canvas.height); diff --git a/pyproject.toml b/pyproject.toml index a218018..22877c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.39" +version = "1.0.40" license = { file = "LICENSE" } [project.urls] From db9667beb64b582fce3f6a46e820f3806ad68d03 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Sun, 12 Jan 2025 22:06:06 +0300 Subject: [PATCH 05/11] update fonts loaded painter node, extras_node clear code --- ExtrasNode/js/extras_node.js | 7 ---- PainterNode/js/painter_node.js | 60 +++++++++++++++++----------------- pyproject.toml | 2 +- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index b73c1ef..e84d060 100644 --- a/ExtrasNode/js/extras_node.js +++ b/ExtrasNode/js/extras_node.js @@ -145,7 +145,6 @@ app.registerExtension({ style: { position: "absolute", maxWidth: "160px", - // right: "calc(-1% - 160px)", zIndex: 9999999, }, parent: document.body, @@ -193,10 +192,6 @@ app.registerExtension({ } }; - this.root.addEventListener("blur", () => { - console.log("ok"); - }); - LiteGraph.pointerListenerAdd(element, "enter", (e) => { if (element?.dataset?.value) { const body_rect = document.body.getBoundingClientRect(); @@ -239,12 +234,10 @@ app.registerExtension({ this.root.preview_content_combo.style.left = `${ root_rect.left - previewWidth - 10 }px`; - // this.root.preview_content_combo.style.right = "auto"; } else { this.root.preview_content_combo.style.left = `${ root_rect.left + root_rect.width + 10 }px`; - // this.root.preview_content_combo.style.right = `calc(-1% - ${this.root.preview_content_combo.style.maxWidth})`; } ctx.clearRect(0, 0, canvas.width, canvas.height); diff --git a/PainterNode/js/painter_node.js b/PainterNode/js/painter_node.js index 049f190..93cfae7 100644 --- a/PainterNode/js/painter_node.js +++ b/PainterNode/js/painter_node.js @@ -82,16 +82,29 @@ function resizeCanvas(node, sizes) { }, 1000); } -const getListFonts = async () => { - const fontFaces = await document.fonts.ready; - const loadedFonts = []; - - document.fonts.forEach((font) => { - loadedFonts.push(font.family); - }); - console.log("PainterNode: Loading fonts completed"); - return [...new Set(loadedFonts)]; +const FONTS = {}; +const STATES = { + fontsLoaded: false, }; + +async function getLoadedFonts() { + const getListFonts = async () => { + const fontFaces = await document.fonts.ready; + const loadedFonts = []; + + document.fonts.forEach((font) => { + loadedFonts.push(font.family); + }); + + return [...new Set(loadedFonts)]; + }; + + await getListFonts().then((fonts) => { + fonts.forEach((font) => { + FONTS[font] = { type: "custom" }; + }); + }); +} // ================= END FUNCTIONS ================ // ================= CLASS PAINTER ================ @@ -129,6 +142,7 @@ class Painter { Courier: { type: "default" }, "Comic Sans MS": { type: "default" }, Impact: { type: "default" }, + ...FONTS, }; this.bringFrontSelected = true; @@ -385,32 +399,11 @@ class Painter { this.painter_bg_setting.appendChild(this.bgImageFile); - this.getLoadedFonts(); // dev users fonts - this.changePropertyBrush(); this.createBrushesToolbar(); this.bindEvents(); } - async getLoadedFonts() { - const getListFonts = async () => { - const fontFaces = await document.fonts.ready; - const loadedFonts = []; - - document.fonts.forEach((font) => { - loadedFonts.push(font.family); - }); - console.log("PainterNode: Loading fonts completed"); - return [...new Set(loadedFonts)]; - }; - - await getListFonts().then((fonts) => { - fonts.forEach((font) => { - this.fonts[font] = { type: "custom" }; - }); - }); - } - setValueElementsLS() { this.painter_wrapper_settings.remove(); this.mainSettings(); @@ -2679,6 +2672,13 @@ app.registerExtension({ }, async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "PainterNode") { + if (!STATES.fontsLoaded) { + await getLoadedFonts().then(() => { + STATES.fontsLoaded = true; + console.log("PainterNode: Loading fonts completed"); + }); + } + // Create node const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = async function () { diff --git a/pyproject.toml b/pyproject.toml index 22877c1..289e65a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.40" +version = "1.0.41" license = { file = "LICENSE" } [project.urls] From a16cf4b0092471536be7815b6fb90f959b45c2d3 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Sun, 12 Jan 2025 22:38:11 +0300 Subject: [PATCH 06/11] update fonts painter node and utils.js fixed --- ExtrasNode/js/utils.js | 20 +++++++++++++++----- PainterNode/js/painter_node.js | 26 +++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/ExtrasNode/js/utils.js b/ExtrasNode/js/utils.js index 3c0d92b..1db8a7b 100644 --- a/ExtrasNode/js/utils.js +++ b/ExtrasNode/js/utils.js @@ -118,6 +118,13 @@ function animateTransitionProps( preStyles = { display: "block" } ) { Object.assign(el.style, preStyles); + + el.style.transition = + !el.style.transition || + !window.getComputedStyle(el).getPropertyValue("transition") + ? "all .8s" + : el.style.transition; + return new Promise((res) => { setTimeout(() => { Object.assign(el.style, props); @@ -145,9 +152,12 @@ function animateClick(target, params = {}) { const hide = +target.style.opacity === 0; return animateTransitionProps(target, { opacity: hide ? opacityVal : 0, - }) - .then(() => showHide({ elements: [target], hide: !hide })) - .then(() => callback()); + }).then((el) => { + const isHide = hide || el.style.display === "none"; + showHide({ elements: [target], hide: !hide }); + callback(); + return isHide; + }); } function showHide({ elements = [], hide = null, displayProp = "block" } = {}) { @@ -442,8 +452,8 @@ function createWindowModal({ switch (typeof text) { case "string": - if (/^\<*.\/?\>$/.test(text)) { - parent.innerHTML(text); + if (/^\<.*\/?\>$/.test(text)) { + parent.innerHTML = text; } else { parent.textContent = text; } diff --git a/PainterNode/js/painter_node.js b/PainterNode/js/painter_node.js index 93cfae7..c96d44f 100644 --- a/PainterNode/js/painter_node.js +++ b/PainterNode/js/painter_node.js @@ -1042,12 +1042,36 @@ class Painter { } }; + const infoFontsButton = makeElement("button", { + style: {}, + textContent: "?", + title: "Info for fonts", + onclick: (e) => + createWindowModal({ + textTitle: "NOTE", + textBody: + "If fonts not loaded in the canvas, refresh page browser!😅", + ...THEMES_MODAL_WINDOW.warning, + options: { + auto: { autohide: true, autoshow: true, autoremove: true }, + parent: this.canvas.wrapperEl, + overlay: { + overlay_enabled: true, + overlayStyles: { + position: "absolute", + }, + }, + }, + }), + }); + property_textbox.append( buttonItalic, buttonBold, buttonUnderline, separator, - selectFontFamily + selectFontFamily, + infoFontsButton ); this.painter_drawning_box_property.append(property_textbox); } From a069a52c3cfb12ec87dd8b533c327b2cb7bbf889 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Sun, 19 Jan 2025 15:46:46 +0300 Subject: [PATCH 07/11] fix closeAllContextMenus in extras_nodes --- ExtrasNode/js/extras_node.js | 10 ++++++---- pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index e84d060..6a3838d 100644 --- a/ExtrasNode/js/extras_node.js +++ b/ExtrasNode/js/extras_node.js @@ -174,12 +174,14 @@ app.registerExtension({ this.root.preview_content_combo = preview_content_combo; } - const closeAllContextMenus = LiteGraph.closeAllContextMenus; + const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus; LiteGraph.closeAllContextMenus = function () { - closeAllContextMenus?.apply(this, arguments); + originalCloseAllContextMenus?.apply(this, arguments); - if (document.querySelector(".preview_content_combo")) - document.querySelector(".preview_content_combo").remove(); + const previewCombo = document.querySelector(".preview_content_combo"); + if (previewCombo) { + previewCombo.remove(); + } }; const closeItem = LiteGraph.ContextMenu.prototype.close; diff --git a/pyproject.toml b/pyproject.toml index 289e65a..8adf1ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.41" +version = "1.0.42" license = { file = "LICENSE" } [project.urls] From bcf99fca5a88f98774dfc221a186637feffd22c6 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Thu, 23 Jan 2025 10:26:26 +0300 Subject: [PATCH 08/11] fix closeAllContextMenus, fix PainterNode past as background --- ExtrasNode/js/extras_node.js | 20 +++++++-------- PainterNode/js/painter_node.js | 47 +++++++++++++++++----------------- pyproject.toml | 2 +- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index 6a3838d..e1d6fa7 100644 --- a/ExtrasNode/js/extras_node.js +++ b/ExtrasNode/js/extras_node.js @@ -174,16 +174,6 @@ app.registerExtension({ this.root.preview_content_combo = preview_content_combo; } - const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus; - LiteGraph.closeAllContextMenus = function () { - originalCloseAllContextMenus?.apply(this, arguments); - - const previewCombo = document.querySelector(".preview_content_combo"); - if (previewCombo) { - previewCombo.remove(); - } - }; - const closeItem = LiteGraph.ContextMenu.prototype.close; LiteGraph.ContextMenu.prototype.close = function () { closeItem?.apply(this, arguments); @@ -294,6 +284,16 @@ app.registerExtension({ }); }; + const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus; + LiteGraph.closeAllContextMenus = function () { + originalCloseAllContextMenus?.apply(this, arguments); + + const previewCombo = document.querySelector(".preview_content_combo"); + if (previewCombo) { + previewCombo.remove(); + } + }; + // PreviewImage settings ui app.ui.settings.addSetting({ id: `${idExt}.PreviewImage`, diff --git a/PainterNode/js/painter_node.js b/PainterNode/js/painter_node.js index c96d44f..37a81e5 100644 --- a/PainterNode/js/painter_node.js +++ b/PainterNode/js/painter_node.js @@ -2070,6 +2070,29 @@ class Painter { ); } + getImageByName(image_name) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = (e) => resolve(new Error("Image not load!")); + + let name = image_name; + let folder_separator = name.lastIndexOf("/"); + let subfolder = ""; + + if (folder_separator > -1) { + subfolder = name.substring(0, folder_separator); + name = name.substring(folder_separator + 1); + } + + img.src = api.apiURL( + `/view?filename=${encodeURIComponent( + name + )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` + ); + }); + } + async addImageToCanvas(image, options = {}) { async function uploadFile(file) { try { @@ -2086,28 +2109,7 @@ class Painter { let path = data.name; if (data.subfolder) path = data.subfolder + "/" + path; - const img = await new Promise((resolve, reject) => { - let img = new Image(); - img.onload = () => resolve(img); - img.onerror = (e) => resolve(new Error("Image not load!")); - - let name = path; - let folder_separator = name.lastIndexOf("/"); - let subfolder = ""; - - if (folder_separator > -1) { - subfolder = name.substring(0, folder_separator); - name = name.substring(folder_separator + 1); - } - - img.src = api.apiURL( - `/view?filename=${encodeURIComponent( - name - )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` - ); - }); - - return img; + return await getImageByName(path); } else { console.log(`${resp.status} - ${resp.statusText}`); return resp.statusText; @@ -2776,7 +2778,6 @@ app.registerExtension({ const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = async function (_, options) { getExtraMenuOptions?.apply(this, arguments); - await this.getTitle(); const past_index = options.findIndex( (m) => m?.content === "Paste (Clipspace)" diff --git a/pyproject.toml b/pyproject.toml index 8adf1ac..ab1f371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.42" +version = "1.0.43" license = { file = "LICENSE" } [project.urls] From 37d8fb474ebcfed20bac979e8f8cf65d36a6f5a5 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Fri, 24 Jan 2025 13:47:13 +0300 Subject: [PATCH 09/11] update PoseNode, code cleaning and saving method data pose --- PoseNode/css/pose_node_styles.css | 11 + PoseNode/js/pose_node.js | 406 +++++++++--------------------- pyproject.toml | 2 +- 3 files changed, 137 insertions(+), 282 deletions(-) diff --git a/PoseNode/css/pose_node_styles.css b/PoseNode/css/pose_node_styles.css index 2cb5f77..e344ecd 100644 --- a/PoseNode/css/pose_node_styles.css +++ b/PoseNode/css/pose_node_styles.css @@ -1,3 +1,11 @@ +.canvas_container_openpose { + width: auto !important; + height: auto !important; +} +.canvas_container_openpose canvas { + width: 100% !important; + height: auto !important; +} .pose_panelButtons { position: absolute; padding: 4px; @@ -6,6 +14,9 @@ flex-direction: column; width: fit-content; } +.pose_panelButtons button { + width: 32px; +} .pose_panelButtons .clear_history { border-color: var(--error-text); color: var(--error-text) !important; diff --git a/PoseNode/js/pose_node.js b/PoseNode/js/pose_node.js index 6524d6d..956fb0c 100644 --- a/PoseNode/js/pose_node.js +++ b/PoseNode/js/pose_node.js @@ -2,7 +2,6 @@ * Title: Set Poses in ComflyUI from ControlNet * Author: AlekPet * Description: I rewrote the main.js file as a class, from fkunn1326's openpose-editor (https://github.com/fkunn1326/openpose-editor/blob/master/javascript/main.js) - * Version: 2025.01.01 * Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet */ @@ -79,19 +78,16 @@ const default_keypoints = [ [260, 72], ]; -window.LS_Poses = {}; - -function LS_Save() { - localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses)); -} - function resizeCanvas(node, sizes) { - const { width, height } = sizes ?? node.openPose.currentCanvasSize; + const { width, height } = sizes ?? node.openPose.settings.currentCanvasSize; - node.openPose.canvas.setDimensions({ - width: width, - height: height, - }); + node.openPose.canvas.setDimensions( + { + width: width, + height: height, + }, + { cssOnly: false, backstoreOnly: true } + ); node.openPose.canvas.getElement().width = width; node.openPose.canvas.getElement().height = height; @@ -102,7 +98,6 @@ function resizeCanvas(node, sizes) { class OpenPose { constructor(node, canvasElement) { - this.lockMode = false; this.visibleEyes = true; this.flipped = false; this.node = node; @@ -110,15 +105,19 @@ class OpenPose { this.maxSizeNodeW = 1024; this.maxSizehNodeH = 200; - this.undo_history = LS_Poses[node.name].undo_history || []; - this.redo_history = LS_Poses[node.name].redo_history || []; - - this.history_change = false; - this.currentCanvasSize = { width: 512, height: 512 }; + this.settings = { + undo_history: [], + redo_history: [], + currentCanvasSize: { width: 512, height: 512 }, + background: null, + }; this.canvas = this.initCanvas(canvasElement); this.image = node.widgets.find((w) => w.name === "image"); } + getSettings() { + return JSON.parse(JSON.stringify(this.settings)); + } setCanvasSize(new_width, new_height, resetPose = false) { resizeCanvas(this.node, { @@ -126,23 +125,13 @@ class OpenPose { height: new_height, }); - this.currentCanvasSize = { width: new_width, height: new_height }; - - // Check isset local storage key currentCanvasSize - if (!Object.hasOwn(LS_Poses, this.node.name)) { - LS_Poses[this.node.name] = { - currentCanvasSize: {}, - }; - } - - LS_Poses[this.node.name]["currentCanvasSize"] = this.currentCanvasSize; + this.settings.currentCanvasSize = { width: new_width, height: new_height }; this.node.title = `${this.node.type} - ${new_width}x${new_height}`; resetPose && this.resetCanvas(); this.canvas.renderAll(); app.graph.setDirtyCanvas(true, false); this.node.onResize(); - LS_Save(); } setPose(keypoints) { @@ -257,7 +246,9 @@ class OpenPose { this.canvas = new fabric.Canvas(this.canvas, { backgroundColor: "#000", preserveObjectStacking: true, - containerClass: "canvas-container-pose", + containerClass: "canvas_container_openpose", + width: this.settings.currentCanvasSize.width, + height: this.settings.currentCanvasSize.height, }); const updateLines = (target) => { @@ -364,68 +355,66 @@ class OpenPose { }); this.canvas.on("object:modified", (e) => { - if ( - this.lockMode || - this.canvas.getActiveObject().type == "activeSelection" - ) { + if (this.canvas.getActiveObject().type == "activeSelection") { this.uploadPoseFile(this.node.name); return; } - this.undo_history.push(this.getJSON()); - this.redo_history.length = 0; - this.history_change = true; + this.settings.undo_history.push(this.getJSON()); + this.settings.redo_history.length = 0; this.uploadPoseFile(this.node.name); }); - if (!LS_Poses[this.node.name].undo_history.length) { + if (!this.settings.undo_history.length) { this.setPose(default_keypoints); - this.undo_history.push(this.getJSON()); + this.settings.undo_history.push(this.getJSON()); } return this.canvas; } undo() { - if (this.undo_history.length > 0) { - this.lockMode = true; - if (this.undo_history.length > 1) - this.redo_history.push(this.undo_history.pop()); + if (this.settings.undo_history.length > 0) { + if (this.settings.undo_history.length > 1) + this.settings.redo_history.push(this.settings.undo_history.pop()); - const content = this.undo_history[this.undo_history.length - 1]; + const content = + this.settings.undo_history[this.settings.undo_history.length - 1]; this.loadPreset(content); this.canvas.renderAll(); - this.lockMode = false; - this.history_change = true; + this.uploadPoseFile(this.node.name); } } redo() { - if (this.redo_history.length > 0) { - this.lockMode = true; - const content = this.redo_history.pop(); - this.undo_history.push(content); + if (this.settings.redo_history.length > 0) { + const content = this.settings.redo_history.pop(); + this.settings.undo_history.push(content); this.loadPreset(content); this.canvas.renderAll(); - this.lockMode = false; - this.history_change = true; + this.uploadPoseFile(this.node.name); } } resetCanvas(reset_size = false) { - if (reset_size && confirm("Reset canvas size?")) { - this.setCanvasSize(512, 512); + if (confirm("Clear undo/redo?")) { + this.settings.undo_history = []; + this.settings.redo_history = []; } - const backgroundImg = this.canvas.backgroundImage; - this.canvas.clear(); - this.canvas.backgroundColor = "#000"; - this.addPose(); + if (reset_size && confirm("Reset canvas size?")) { + this.setCanvasSize(512, 512); - this.setBackground(backgroundImg); - this.uploadPoseFile(this.node.name); + const backgroundImg = this.canvas.backgroundImage; + this.canvas.clear(); + this.canvas.backgroundColor = "#000"; + this.addPose(); + + this.setBackground(backgroundImg); + this.uploadPoseFile(this.node.name); + } } setBackground(backgroundImg) { @@ -441,15 +430,6 @@ class OpenPose { } } - updateHistoryData() { - if (this.history_change) { - LS_Poses[this.node.name].undo_history = this.undo_history; - LS_Poses[this.node.name].redo_history = this.redo_history; - LS_Save(); - this.history_change = false; - } - } - uploadPoseFile(fileName) { // Upload pose to temp folder ComfyUI const hideShowControls = (show) => { @@ -481,7 +461,6 @@ class OpenPose { } this.image.value = data.name; - this.updateHistoryData(); hideShowControls(true); } else { @@ -523,6 +502,7 @@ class OpenPose { onLoadBackground(e) { try { const file = this.backgroundInput.files[0]; + const url = URL.createObjectURL(file); fabric.Image.fromURL(url, (img) => { img.set({ @@ -530,6 +510,7 @@ class OpenPose { originY: "top", opacity: 0.5, }); + this.canvas.setBackgroundImage( img, this.canvas.renderAll.bind(this.canvas), @@ -573,85 +554,20 @@ class OpenPose { // Create OpenPose widget function createOpenPose(node, inputName, inputData, app) { node.name = inputName; - const widget = { - type: "openpose", - name: `w${inputName}`, - - draw: function (ctx, _, widgetWidth, y, widgetHeight) { - const margin = 10, - visible = app.canvas.ds.scale > 0.5 && this.type === "openpose", - clientRectBound = ctx.canvas.getBoundingClientRect(), - transform = new DOMMatrix() - .scaleSelf( - clientRectBound.width / ctx.canvas.width, - clientRectBound.height / ctx.canvas.height - ) - .multiplySelf(ctx.getTransform()) - .translateSelf(margin, margin + y), - w = (widgetWidth - margin * 2 - 3) * transform.a; - - let aspect_ratio = 1; - if (node.openPose.canvas.width !== node.openPose.canvas.height) { - aspect_ratio = node.openPose.canvas.width / node.openPose.canvas.height; - } - - Object.assign(this.openpose.style, { - left: `${transform.a * margin + transform.e + clientRectBound.left}px`, - top: `${transform.d + transform.f + clientRectBound.top}px`, - width: w + "px", - height: w / aspect_ratio + "px", - position: "absolute", - zIndex: app.graph._nodes.indexOf(node), - }); - - Object.assign(this.openpose.children[0].style, { - width: w + "px", - height: w / aspect_ratio + "px", - }); - - Object.assign(this.openpose.children[1].style, { - width: w + "px", - height: w / aspect_ratio + "px", - }); - - Array.from(this.openpose.children[2].children).forEach((element) => { - Object.assign(element.style, { - width: `${28.0 * transform.a}px`, - height: `${22.0 * transform.d}px`, - fontSize: `${transform.d * 10.0}px`, - }); - - if (element.classList.contains("posenode_cnavas_size")) { - const svg = element.children[0]; - - Object.assign(element.style, { - height: `${28.0 * transform.d}px`, - }); - - Object.assign(svg.style, { - width: `${20.0 * transform.a}px`, - height: `${20.0 * transform.d}px`, - }); - } - }); - - this.openpose.hidden = !visible; - }, - }; // Fabric canvas - let canvasOpenPose = document.createElement("canvas"); - node.openPose = new OpenPose(node, canvasOpenPose); + const openPoseWrapper = $el("div.openPoseWrapper"); + const canvasOpenPose = $el("canvas"); + node.openPoseWrapper = openPoseWrapper; - node.openPose.canvas.setWidth(512); - node.openPose.canvas.setHeight(512); + // Add buttons add, reset, undo, redo poses + node.addWidget("button", "Add pose", "add_pose", () => { + node.openPose.addPose(); + }); - let widgetCombo = node.widgets.filter((w) => w.type === "combo"); - widgetCombo[0].value = node.name; - - widget.openpose = node.openPose.canvas.wrapperEl; - widget.parent = node; - widget.openpose.hidden = true; + node.addWidget("button", "Reset pose", "reset_pose", () => { + node.openPose.resetCanvas(true); + }); // Create elements undo, redo, clear history let panelButtons = $el("div.pose_panelButtons.comfy-menu-btns", [ @@ -721,8 +637,8 @@ function createOpenPose(node, inputName, inputData, app) { }, }), $el("button.posenode_cnavas_size", { - innerHTML: ` - + innerHTML: ` + `, style: { margin: 0, padding: 0 }, title: "Change canvas size", @@ -752,6 +668,24 @@ function createOpenPose(node, inputName, inputData, app) { }), ]); + node.openPose = new OpenPose(node, canvasOpenPose); + const widget = node.addDOMWidget( + `w${inputName}`, + "openpose", + openPoseWrapper, + { + getValue() { + return node.openPose.getSettings(); + }, + } + ); + + let widgetCombo = node.widgets.filter((w) => w.type === "combo"); + widgetCombo[0].value = node.name; + + widget.openpose = node.openPose.canvas.wrapperEl; + widget.parent = node; + // Background image node.openPose.backgroundInput = $el("input", { type: "file", @@ -765,101 +699,27 @@ function createOpenPose(node, inputName, inputData, app) { node.openPose.backgroundInput ); - document.body.appendChild(widget.openpose); - - // Add buttons add, reset, undo, redo poses - node.addWidget("button", "Add pose", "add_pose", () => { - node.openPose.addPose(); - }); - - node.addWidget("button", "Reset pose", "reset_pose", () => { - node.openPose.resetCanvas(true); - }); - - // Add customWidget to node - node.addCustomWidget(widget); - - node.onRemoved = () => { - if (Object.hasOwn(LS_Poses, node.name)) { - delete LS_Poses[node.name]; - LS_Save(); - } - - // When removing this node we need to remove the input from the DOM - for (let y in node.widgets) { - if (node.widgets[y].openpose) { - node.widgets[y].openpose.remove(); - } - } - }; - - widget.onRemove = () => { - widget.openpose?.remove(); - }; + openPoseWrapper.appendChild(widget.openpose); node.onResize = function () { let [w, h] = this.size; + let aspect_ratio = 1; - if (node?.imgs && typeof this.imgs !== undefined) { - aspect_ratio = this.imgs[0].naturalHeight / this.imgs[0].naturalWidth; - } - let buffer = 90; + aspect_ratio = node.openPose.canvas.height / node.openPose.canvas.width; - if (w > this.openPose.maxSizeNodeW) - w = w - (w - this.openPose.maxSizeNodeW); + const buffer = 140; + + if (w > 1024) w = w - (w - 1024); if (w < 200) w = 200; h = w * aspect_ratio + buffer; - if (h < this.openPose.maxSizeNodeH) h = this.openPose.maxSizeNodeH + h / 2; + if (h < 200) h = 200 + h / 2; this.size = [w, h]; }; - node.onDrawBackground = function (ctx) { - if (!this.flags.collapsed) { - node.openPose.canvas.wrapperEl.hidden = false; - if (this.imgs && this.imgs.length) { - if (app.canvas.ds.scale > 0.8) { - let [dw, dh] = this.size; - - let w = this.imgs[0].naturalWidth; - let h = this.imgs[0].naturalHeight; - - const scaleX = dw / w; - const scaleY = dh / h; - const scale = Math.min(scaleX, scaleY, 1); - - w *= scale / 8; - h *= scale / 8; - - let x = 5; - let y = dh - h - 5; - - ctx.drawImage(this.imgs[0], x, y, w, h); - } - } - } else { - node.openPose.canvas.wrapperEl.hidden = true; - } - }; - - app.canvas.onDrawBackground = function () { - // Draw node isnt fired once the node is off the screen - // if it goes off screen quickly, the input may not be removed - // this shifts it off screen so it can be moved back if the node is visible. - for (let n in app.graph._nodes) { - const currNode = app.graph._nodes[n]; - for (let w in currNode.widgets) { - let wid = currNode.widgets[w]; - if (Object.hasOwn(wid, "openpose")) { - wid.openpose.style.left = -8000 + "px"; - wid.openpose.style.position = "absolute"; - } - } - } - }; return { widget: widget }; } @@ -869,24 +729,7 @@ app.registerExtension({ async init(app) { addStylesheet("css/posenode/pose_node_styles.css", import.meta.url); }, - async setup(app) { - let openPoseNode = app.graph._nodes.filter((wi) => wi.type == "PoseNode"); - - if (openPoseNode.length) { - openPoseNode.map((n) => { - console.log(`Setup PoseNode: ${n.name}`); - let widgetImage = n.widgets.find((w) => w.name == "image"); - if (widgetImage && Object.hasOwn(LS_Poses, n.name)) { - let pose_ls = LS_Poses[n.name].undo_history; - n.openPose.loadPreset( - pose_ls.length > 0 - ? pose_ls[pose_ls.length - 1] - : { keypoints: default_keypoints } - ); - } - }); - } - }, + async setup(app) {}, async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "PoseNode") { const onNodeCreated = nodeType.prototype.onNodeCreated; @@ -904,46 +747,47 @@ app.registerExtension({ console.log(`Create PoseNode: ${nodeNamePNG}`); - LS_Poses = - localStorage.getItem("ComfyUI_Poses") && - JSON.parse(localStorage.getItem("ComfyUI_Poses")); - if (!LS_Poses) { - localStorage.setItem("ComfyUI_Poses", JSON.stringify({})); - LS_Poses = JSON.parse(localStorage.getItem("ComfyUI_Poses")); - } - - if (!Object.hasOwn(LS_Poses, nodeNamePNG)) { - LS_Poses[nodeNamePNG] = { - undo_history: [], - redo_history: [], - currentCanvasSize: { width: 512, height: 512 }, - }; - - LS_Save(); - } - createOpenPose.apply(this, [this, nodeNamePNG, {}, app]); - setTimeout(() => { - if (LS_Poses[nodeNamePNG]?.currentCanvasSize) { - this.openPose.currentCanvasSize = - LS_Poses[nodeNamePNG]?.currentCanvasSize; - - this.openPose.setCanvasSize( - this.openPose.currentCanvasSize.width, - this.openPose.currentCanvasSize.height - ); - } - - this.openPose.uploadPoseFile(nodeNamePNG); - // Resize window - window.addEventListener("resize", (e) => resizeCanvas(this), false); - }, 1); - this.setSize([530, 620]); - + // Resize window + window.addEventListener("resize", (e) => resizeCanvas(this), false); return r; }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = async function (widget) { + onConfigure?.apply(this, arguments); + + setTimeout(() => { + const data = widget.widgets_values[3]; + + if (data) { + const { + currentCanvasSize, + background, + undo_history, + redo_history, + } = data; + if (currentCanvasSize) { + const { width, height } = currentCanvasSize; + this.openPose.setCanvasSize(width, height); + } + + this.openPose.loadPreset( + undo_history.length + ? undo_history[undo_history.length - 1] + : { keypoints: default_keypoints } + ); + + this.openPose.settings = data; + this.openPose.value = data; + + this.openPose.canvas.renderAll(); + this.openPose.uploadPoseFile(this.name); + } + }, 0); + }; } }, }); diff --git a/pyproject.toml b/pyproject.toml index ab1f371..2912e30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.43" +version = "1.0.44" license = { file = "LICENSE" } [project.urls] From 6f38ea6d9c2d419991ff2df88179b238b63dd81d Mon Sep 17 00:00:00 2001 From: AlekPet Date: Fri, 24 Jan 2025 16:27:24 +0300 Subject: [PATCH 10/11] PoseNode small fix --- PoseNode/js/pose_node.js | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PoseNode/js/pose_node.js b/PoseNode/js/pose_node.js index 956fb0c..e88837f 100644 --- a/PoseNode/js/pose_node.js +++ b/PoseNode/js/pose_node.js @@ -670,7 +670,7 @@ function createOpenPose(node, inputName, inputData, app) { node.openPose = new OpenPose(node, canvasOpenPose); const widget = node.addDOMWidget( - `w${inputName}`, + "widget_openpose", "openpose", openPoseWrapper, { diff --git a/pyproject.toml b/pyproject.toml index 2912e30..94b2a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.44" +version = "1.0.45" license = { file = "LICENSE" } [project.urls] From 2f8a34509b33ccf18241bc443c44dc92c65c93bb Mon Sep 17 00:00:00 2001 From: AlekPet Date: Fri, 24 Jan 2025 16:27:24 +0300 Subject: [PATCH 11/11] PoseNode small fix --- PoseNode/js/pose_node.js | 182 ++++++++++++++++++++++++++------------- pyproject.toml | 2 +- 2 files changed, 125 insertions(+), 59 deletions(-) diff --git a/PoseNode/js/pose_node.js b/PoseNode/js/pose_node.js index 956fb0c..f2dbc4b 100644 --- a/PoseNode/js/pose_node.js +++ b/PoseNode/js/pose_node.js @@ -399,21 +399,23 @@ class OpenPose { } resetCanvas(reset_size = false) { - if (confirm("Clear undo/redo?")) { - this.settings.undo_history = []; - this.settings.redo_history = []; - } + if (reset_size) { + if (confirm("Clear undo/redo?")) { + this.settings.undo_history = []; + this.settings.redo_history = []; + } - if (reset_size && confirm("Reset canvas size?")) { - this.setCanvasSize(512, 512); + if (confirm("Reset canvas size?")) { + this.setCanvasSize(512, 512); - const backgroundImg = this.canvas.backgroundImage; - this.canvas.clear(); - this.canvas.backgroundColor = "#000"; - this.addPose(); + const backgroundImg = this.canvas.backgroundImage; + this.canvas.clear(); + this.canvas.backgroundColor = "#000"; + this.addPose(); - this.setBackground(backgroundImg); - this.uploadPoseFile(this.node.name); + this.setBackground(backgroundImg); + this.uploadPoseFile(this.node.name); + } } } @@ -446,43 +448,33 @@ class OpenPose { hideShowControls(false); - const uploadFile = async (blobFile) => { - try { - const resp = await api.fetchApi("/upload/image", { - method: "POST", - body: blobFile, - }); - - if (resp.status === 200) { - const data = await resp.json(); - - if (!this.image.options.values.includes(data.name)) { - this.image.options.values.push(data.name); - } - - this.image.value = data.name; - - hideShowControls(true); - } else { - alert(resp.status + " - " + resp.statusText); - } - } catch (error) { - hideShowControls(true); - console.log(error); - } - }; - //Temporarily hide background image let tmp_BackgroundImg = this.canvas.backgroundImage; this.canvas.backgroundImage = null; this.canvas.renderAll(); - this.canvas.lowerCanvasEl.toBlob(function (blob) { - let formData = new FormData(); + this.canvas.lowerCanvasEl.toBlob((blob) => { + const formData = new FormData(); formData.append("image", blob, fileName); formData.append("overwrite", "true"); formData.append("type", "input"); - uploadFile(formData); + + this.uploadFileToServer(formData) + .then((jsonData) => { + if (!jsonData.success) { + throw new Error(data.error); + } + + const { name } = jsonData.data; + + if (!this.image.options.values.includes(name)) { + this.image.options.values.push(name); + } + + this.image.value = name; + hideShowControls(true); + }) + .catch(() => hideShowControls(true)); }, "image/png"); // - end @@ -499,28 +491,80 @@ class OpenPose { }; } + async uploadFileToServer(formData) { + try { + const resp = await api.fetchApi("/upload/image", { + method: "POST", + body: formData, + }); + + if (resp.status !== 200) { + return { success: false, error: `${resp.status} - ${resp.statusText}` }; + } + + return { success: true, data: await resp.json() }; + } catch (error) { + return { success: false, error }; + } + } + + getImageUrlByName(jsonData) { + return { + url: api.apiURL( + `/view?filename=${encodeURIComponent( + jsonData.name + )}&type=input&subfolder=${ + jsonData.subfolder + }${app.getPreviewFormatParam()}${app.getRandParam()}` + ), + name: jsonData.name, + subfolder: jsonData.subfolder, + }; + } + onLoadBackground(e) { try { const file = this.backgroundInput.files[0]; - const url = URL.createObjectURL(file); - fabric.Image.fromURL(url, (img) => { - img.set({ - originX: "left", - originY: "top", - opacity: 0.5, - }); + const formData = new FormData(); + formData.append("image", file); + formData.append("overwrite", "true"); + formData.append("type", "input"); - this.canvas.setBackgroundImage( - img, - this.canvas.renderAll.bind(this.canvas), - { - scaleX: this.canvas.width / img.width, - scaleY: this.canvas.height / img.height, + this.uploadFileToServer(formData) + .then((jsonData) => { + if (!jsonData.success) { + throw new Error(jsonData.error); } - ); - }); - } catch (err) {} + return this.getImageUrlByName(jsonData.data); + }) + .then((image_data) => { + if (!image_data) { + console.log("Invalid data image!"); + return; + } + this.settings.background = image_data.url; + + fabric.Image.fromURL(image_data.url, (img) => { + img.set({ + originX: "left", + originY: "top", + opacity: 0.5, + }); + + this.canvas.setBackgroundImage( + img, + this.canvas.renderAll.bind(this.canvas), + { + scaleX: this.canvas.width / img.width, + scaleY: this.canvas.height / img.height, + } + ); + }); + }); + } catch (err) { + console.error(err); + } } getJSON() { @@ -670,7 +714,7 @@ function createOpenPose(node, inputName, inputData, app) { node.openPose = new OpenPose(node, canvasOpenPose); const widget = node.addDOMWidget( - `w${inputName}`, + "widget_openpose", "openpose", openPoseWrapper, { @@ -750,6 +794,9 @@ app.registerExtension({ createOpenPose.apply(this, [this, nodeNamePNG, {}, app]); this.setSize([530, 620]); + + this.openPose.uploadPoseFile(nodeNamePNG); + // Resize window window.addEventListener("resize", (e) => resizeCanvas(this), false); return r; @@ -780,6 +827,25 @@ app.registerExtension({ : { keypoints: default_keypoints } ); + if (background) { + fabric.Image.fromURL(background, (img) => { + img.set({ + originX: "left", + originY: "top", + opacity: 0.5, + }); + + this.openPose.canvas.setBackgroundImage( + img, + this.openPose.canvas.renderAll.bind(this.openPose.canvas), + { + scaleX: this.openPose.canvas.width / img.width, + scaleY: this.openPose.canvas.height / img.height, + } + ); + }); + } + this.openPose.settings = data; this.openPose.value = data; diff --git a/pyproject.toml b/pyproject.toml index 2912e30..94b2a02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.44" +version = "1.0.45" license = { file = "LICENSE" } [project.urls]