diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index e28dc57..e1d6fa7 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,9 @@ app.registerExtension({ style: { position: "absolute", maxWidth: "160px", - right: "calc(-1% - 160px)", + zIndex: 9999999, }, - parent: this.root, + parent: document.body, }, [ $el("canvas.preview_img", { @@ -168,6 +174,16 @@ app.registerExtension({ this.root.preview_content_combo = preview_content_combo; } + 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; + } + }; + LiteGraph.pointerListenerAdd(element, "enter", (e) => { if (element?.dataset?.value) { const body_rect = document.body.getBoundingClientRect(); @@ -208,12 +224,12 @@ 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"; } 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`; } ctx.clearRect(0, 0, canvas.width, canvas.height); @@ -268,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/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/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_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..0621b0b --- /dev/null +++ b/PainterNode/css/painter_node_user_fonts.css @@ -0,0 +1,27 @@ +/* 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/painternode/pricedown.otf") format("opentype"); +} + +/* Harry_font__YOURSSINCERELYLARRY_ font */ +@font-face { + font-family: "Harry_font__YOURSSINCERELYLARRY_"; + src: url("./../../fonts/painternode/HarryFontYourssincerelylarry.ttf") + format("truetype"); +} diff --git a/PainterNode/fonts/HarryFontYourssincerelylarry.ttf b/PainterNode/fonts/HarryFontYourssincerelylarry.ttf new file mode 100644 index 0000000..e57cd7d Binary files /dev/null and b/PainterNode/fonts/HarryFontYourssincerelylarry.ttf differ diff --git a/PainterNode/fonts/pricedown.otf b/PainterNode/fonts/pricedown.otf new file mode 100644 index 0000000..f06a2be Binary files /dev/null and b/PainterNode/fonts/pricedown.otf differ 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..37a81e5 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,30 @@ function resizeCanvas(node, sizes) { app.graph.setDirtyCanvas(true, false); }, 1000); } + +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 ================ @@ -110,13 +135,14 @@ class Painter { // 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", + 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" }, + ...FONTS, }; this.bringFrontSelected = true; @@ -372,6 +398,7 @@ class Painter { }); this.painter_bg_setting.appendChild(this.bgImageFile); + this.changePropertyBrush(); this.createBrushesToolbar(); this.bindEvents(); @@ -980,29 +1007,71 @@ 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 in 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); } // Select front event selectFontFamily.onchange = (e) => { - if (this.getActiveStyle("fontFamily") != selectFontFamily.value) - this.setActiveStyle("fontFamily", selectFontFamily.value); + if (this.getActiveStyle("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); + } }; + 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); } @@ -1577,12 +1646,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) => { @@ -1602,6 +1679,8 @@ class Painter { "underline", Boolean(this.getActiveStyle("underline", target)) == false ); + + setProps("fontFamily", this.getActiveStyle("fontFamily", target)); } if ( @@ -1991,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 { @@ -2007,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; @@ -2106,7 +2187,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, }); @@ -2528,6 +2609,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({ @@ -2616,6 +2698,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 () { @@ -2689,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/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/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 f89371c..a164b77 100644 --- a/PoseNode/js/pose_node.js +++ b/PoseNode/js/pose_node.js @@ -2,10 +2,10 @@ * 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 * 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"; @@ -78,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; @@ -101,18 +98,26 @@ function resizeCanvas(node, sizes) { class OpenPose { constructor(node, canvasElement) { - this.lockMode = false; this.visibleEyes = true; this.flipped = false; this.node = node; - 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.maxSizeNodeW = 1024; + this.maxSizehNodeH = 200; + + 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, { @@ -120,20 +125,13 @@ class OpenPose { height: new_height, }); - 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"] = {}; - - 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) { @@ -248,6 +246,9 @@ class OpenPose { this.canvas = new fabric.Canvas(this.canvas, { backgroundColor: "#000", preserveObjectStacking: true, + containerClass: "canvas_container_openpose", + width: this.settings.currentCanvasSize.width, + height: this.settings.currentCanvasSize.height, }); const updateLines = (target) => { @@ -354,68 +355,68 @@ 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 (reset_size) { + if (confirm("Clear undo/redo?")) { + this.settings.undo_history = []; + this.settings.redo_history = []; + } + + if (confirm("Reset canvas size?")) { + this.setCanvasSize(512, 512); + + const backgroundImg = this.canvas.backgroundImage; + this.canvas.clear(); + this.canvas.backgroundColor = "#000"; + this.addPose(); + + 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) { @@ -431,15 +432,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) => { @@ -456,44 +448,33 @@ class OpenPose { hideShowControls(false); - const uploadFile = async (blobFile) => { - try { - const resp = await fetch("/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; - this.updateHistoryData(); - - 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(jsonData.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 @@ -510,26 +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, - }); - this.canvas.setBackgroundImage( - img, - this.canvas.renderAll.bind(this.canvas), - { - scaleX: this.canvas.width / img.width, - scaleY: this.canvas.height / img.height, + + const formData = new FormData(); + formData.append("image", file); + formData.append("overwrite", "true"); + formData.append("type", "input"); + + 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() { @@ -563,84 +598,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`, - }); - } - - element.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; + 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", [ @@ -710,8 +681,8 @@ function createOpenPose(node, inputName, inputData, app) { }, }), $el("button.posenode_cnavas_size", { - innerHTML: ` - + innerHTML: ` + `, style: { margin: 0, padding: 0 }, title: "Change canvas size", @@ -741,6 +712,24 @@ function createOpenPose(node, inputName, inputData, app) { }), ]); + node.openPose = new OpenPose(node, canvasOpenPose); + const widget = node.addDOMWidget( + "widget_openpose", + "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", @@ -754,53 +743,27 @@ function createOpenPose(node, inputName, inputData, app) { node.openPose.backgroundInput ); - document.body.appendChild(widget.openpose); + openPoseWrapper.appendChild(widget.openpose); - // Add buttons add, reset, undo, redo poses - node.addWidget("button", "Add pose", "add_pose", () => { - node.openPose.addPose(); - }); + node.onResize = function () { + let [w, h] = this.size; - node.addWidget("button", "Reset pose", "reset_pose", () => { - node.openPose.resetCanvas(true); - }); + let aspect_ratio = 1; - // Add customWidget to node - node.addCustomWidget(widget); + aspect_ratio = node.openPose.canvas.height / node.openPose.canvas.width; - node.onRemoved = () => { - if (Object.hasOwn(LS_Poses, node.name)) { - delete LS_Poses[node.name]; - LS_Save(); - } + const buffer = 140; - // 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(); - } - } + if (w > 1024) w = w - (w - 1024); + if (w < 200) w = 200; + + h = w * aspect_ratio + buffer; + + if (h < 200) h = 200 + h / 2; + + this.size = [w, h]; }; - widget.onRemove = () => { - widget.openpose?.remove(); - }; - - 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 }; } @@ -810,80 +773,87 @@ 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; - 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}`); - 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]); + this.openPose.uploadPoseFile(nodeNamePNG); + + // 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 } + ); + + 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; + + this.openPose.canvas.renderAll(); + this.openPose.uploadPoseFile(this.name); + } + }, 0); + }; } }, }); diff --git a/__init__.py b/__init__.py index ce60250..28508db 100644 --- a/__init__.py +++ b/__init__.py @@ -285,7 +285,7 @@ def install_node(nodeElement): # -- Copy tree folders 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): diff --git a/pyproject.toml b/pyproject.toml index ff61740..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.37" +version = "1.0.45" license = { file = "LICENSE" } [project.urls]