Spaces:
Configuration error
Configuration error
| import { app } from "../../../scripts/app.js"; | |
| import { fabric } from "../lib/fabric.js"; | |
| fabric.Object.prototype.transparentCorners = false; | |
| fabric.Object.prototype.cornerColor = "#108ce6"; | |
| fabric.Object.prototype.borderColor = "#108ce6"; | |
| fabric.Object.prototype.cornerSize = 10; | |
| let connect_keypoints = [ | |
| [0, 1], | |
| [1, 2], | |
| [2, 3], | |
| [3, 4], | |
| [1, 5], | |
| [5, 6], | |
| [6, 7], | |
| [1, 8], | |
| [8, 9], | |
| [9, 10], | |
| [1, 11], | |
| [11, 12], | |
| [12, 13], | |
| [0, 14], | |
| [14, 16], | |
| [0, 15], | |
| [15, 17], | |
| ]; | |
| let connect_color = [ | |
| [0, 0, 255], | |
| [255, 0, 0], | |
| [255, 170, 0], | |
| [255, 255, 0], | |
| [255, 85, 0], | |
| [170, 255, 0], | |
| [85, 255, 0], | |
| [0, 255, 0], | |
| [0, 255, 85], | |
| [0, 255, 170], | |
| [0, 255, 255], | |
| [0, 170, 255], | |
| [0, 85, 255], | |
| [85, 0, 255], | |
| [170, 0, 255], | |
| [255, 0, 255], | |
| [255, 0, 170], | |
| [255, 0, 85], | |
| ]; | |
| const default_keypoints = [ | |
| [241, 77], | |
| [241, 120], | |
| [191, 118], | |
| [177, 183], | |
| [163, 252], | |
| [298, 118], | |
| [317, 182], | |
| [332, 245], | |
| [225, 241], | |
| [213, 359], | |
| [215, 454], | |
| [270, 240], | |
| [282, 360], | |
| [286, 456], | |
| [232, 59], | |
| [253, 60], | |
| [225, 70], | |
| [260, 72], | |
| ]; | |
| 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.canvas = this.initCanvas(canvasElement); | |
| this.image = node.widgets.find((w) => w.name === "image"); | |
| } | |
| setPose(keypoints) { | |
| this.canvas.clear(); | |
| this.canvas.backgroundColor = "#000"; | |
| const res = []; | |
| for (let i = 0; i < keypoints.length; i += 18) { | |
| const chunk = keypoints.slice(i, i + 18); | |
| res.push(chunk); | |
| } | |
| for (let item of res) { | |
| this.addPose(item); | |
| this.canvas.discardActiveObject(); | |
| } | |
| } | |
| addPose(keypoints = undefined) { | |
| if (keypoints === undefined) { | |
| keypoints = default_keypoints; | |
| } | |
| const group = new fabric.Group(); | |
| const makeCircle = ( | |
| color, | |
| left, | |
| top, | |
| line1, | |
| line2, | |
| line3, | |
| line4, | |
| line5 | |
| ) => { | |
| let c = new fabric.Circle({ | |
| left: left, | |
| top: top, | |
| strokeWidth: 1, | |
| radius: 5, | |
| fill: color, | |
| stroke: color, | |
| }); | |
| c.hasControls = c.hasBorders = false; | |
| c.line1 = line1; | |
| c.line2 = line2; | |
| c.line3 = line3; | |
| c.line4 = line4; | |
| c.line5 = line5; | |
| return c; | |
| }; | |
| const makeLine = (coords, color) => { | |
| return new fabric.Line(coords, { | |
| fill: color, | |
| stroke: color, | |
| strokeWidth: 10, | |
| selectable: false, | |
| evented: false, | |
| }); | |
| }; | |
| const lines = []; | |
| const circles = []; | |
| for (let i = 0; i < connect_keypoints.length; i++) { | |
| // 接続されるidxを指定 [0, 1]なら0と1つなぐ | |
| const item = connect_keypoints[i]; | |
| const line = makeLine( | |
| keypoints[item[0]].concat(keypoints[item[1]]), | |
| `rgba(${connect_color[i].join(", ")}, 0.7)` | |
| ); | |
| lines.push(line); | |
| this.canvas.add(line); | |
| } | |
| for (let i = 0; i < keypoints.length; i++) { | |
| let list = []; | |
| connect_keypoints.filter((item, idx) => { | |
| if (item.includes(i)) { | |
| list.push(lines[idx]); | |
| return idx; | |
| } | |
| }); | |
| const circle = makeCircle( | |
| `rgb(${connect_color[i].join(", ")})`, | |
| keypoints[i][0], | |
| keypoints[i][1], | |
| ...list | |
| ); | |
| circle["id"] = i; | |
| circles.push(circle); | |
| group.addWithUpdate(circle); | |
| } | |
| this.canvas.discardActiveObject(); | |
| this.canvas.setActiveObject(group); | |
| this.canvas.add(group); | |
| group.toActiveSelection(); | |
| this.canvas.requestRenderAll(); | |
| } | |
| initCanvas() { | |
| this.canvas = new fabric.Canvas(this.canvas, { | |
| backgroundColor: "#000", | |
| preserveObjectStacking: true, | |
| }); | |
| const updateLines = (target) => { | |
| if ("_objects" in target) { | |
| const flipX = target.flipX ? -1 : 1; | |
| const flipY = target.flipY ? -1 : 1; | |
| this.flipped = flipX * flipY === -1; | |
| const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes; | |
| if (target.angle === 0) { | |
| const rtop = target.top; | |
| const rleft = target.left; | |
| for (const item of target._objects) { | |
| let p = item; | |
| p.scaleX = 1; | |
| p.scaleY = 1; | |
| const top = | |
| rtop + | |
| p.top * target.scaleY * flipY + | |
| (target.height * target.scaleY) / 2; | |
| const left = | |
| rleft + | |
| p.left * target.scaleX * flipX + | |
| (target.width * target.scaleX) / 2; | |
| p["_top"] = top; | |
| p["_left"] = left; | |
| if (p["id"] === 0) { | |
| p.line1 && p.line1.set({ x1: left, y1: top }); | |
| } else { | |
| p.line1 && p.line1.set({ x2: left, y2: top }); | |
| } | |
| if (p["id"] === 14 || p["id"] === 15) { | |
| p.radius = showEyes ? 5 : 0; | |
| if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0; | |
| if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0; | |
| } | |
| p.line2 && p.line2.set({ x1: left, y1: top }); | |
| p.line3 && p.line3.set({ x1: left, y1: top }); | |
| p.line4 && p.line4.set({ x1: left, y1: top }); | |
| p.line5 && p.line5.set({ x1: left, y1: top }); | |
| } | |
| } else { | |
| const aCoords = target.aCoords; | |
| const center = { | |
| x: (aCoords.tl.x + aCoords.br.x) / 2, | |
| y: (aCoords.tl.y + aCoords.br.y) / 2, | |
| }; | |
| const rad = (target.angle * Math.PI) / 180; | |
| const sin = Math.sin(rad); | |
| const cos = Math.cos(rad); | |
| for (const item of target._objects) { | |
| let p = item; | |
| const p_top = p.top * target.scaleY * flipY; | |
| const p_left = p.left * target.scaleX * flipX; | |
| const left = center.x + p_left * cos - p_top * sin; | |
| const top = center.y + p_left * sin + p_top * cos; | |
| p["_top"] = top; | |
| p["_left"] = left; | |
| if (p["id"] === 0) { | |
| p.line1 && p.line1.set({ x1: left, y1: top }); | |
| } else { | |
| p.line1 && p.line1.set({ x2: left, y2: top }); | |
| } | |
| if (p["id"] === 14 || p["id"] === 15) { | |
| p.radius = showEyes ? 5 : 0.3; | |
| if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0; | |
| if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0; | |
| } | |
| p.line2 && p.line2.set({ x1: left, y1: top }); | |
| p.line3 && p.line3.set({ x1: left, y1: top }); | |
| p.line4 && p.line4.set({ x1: left, y1: top }); | |
| p.line5 && p.line5.set({ x1: left, y1: top }); | |
| } | |
| } | |
| } else { | |
| var p = target; | |
| if (p["id"] === 0) { | |
| p.line1 && p.line1.set({ x1: p.left, y1: p.top }); | |
| } else { | |
| p.line1 && p.line1.set({ x2: p.left, y2: p.top }); | |
| } | |
| p.line2 && p.line2.set({ x1: p.left, y1: p.top }); | |
| p.line3 && p.line3.set({ x1: p.left, y1: p.top }); | |
| p.line4 && p.line4.set({ x1: p.left, y1: p.top }); | |
| p.line5 && p.line5.set({ x1: p.left, y1: p.top }); | |
| } | |
| this.canvas.renderAll(); | |
| }; | |
| this.canvas.on("object:moving", (e) => { | |
| updateLines(e.target); | |
| }); | |
| this.canvas.on("object:scaling", (e) => { | |
| updateLines(e.target); | |
| this.canvas.renderAll(); | |
| }); | |
| this.canvas.on("object:rotating", (e) => { | |
| updateLines(e.target); | |
| this.canvas.renderAll(); | |
| }); | |
| this.canvas.on("object:modified", () => { | |
| if ( | |
| this.lockMode || | |
| this.canvas.getActiveObject().type == "activeSelection" | |
| ) | |
| return; | |
| this.undo_history.push(this.getJSON()); | |
| this.redo_history.length = 0; | |
| this.history_change = true; | |
| this.uploadPoseFile(this.node.name); | |
| }); | |
| if (!LS_Poses[this.node.name].undo_history.length) { | |
| this.setPose(default_keypoints); | |
| this.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()); | |
| const content = this.undo_history[this.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); | |
| this.loadPreset(content); | |
| this.canvas.renderAll(); | |
| this.lockMode = false; | |
| this.history_change = true; | |
| this.uploadPoseFile(this.node.name); | |
| } | |
| } | |
| resetCanvas() { | |
| this.canvas.clear(); | |
| this.canvas.backgroundColor = "#000"; | |
| this.addPose(); | |
| } | |
| 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 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(); | |
| } else { | |
| alert(resp.status + " - " + resp.statusText); | |
| } | |
| } catch (error) { | |
| console.error(error); | |
| } | |
| }; | |
| this.canvas.lowerCanvasEl.toBlob(function (blob) { | |
| let formData = new FormData(); | |
| formData.append("image", blob, fileName); | |
| formData.append("overwrite", "true"); | |
| formData.append("type", "temp"); | |
| uploadFile(formData); | |
| }, "image/png"); | |
| // - end | |
| const callb = this.node.callback, | |
| self = this; | |
| this.image.callback = function () { | |
| this.image.value = self.node.name; | |
| if (callb) { | |
| return callb.apply(this, arguments); | |
| } | |
| }; | |
| } | |
| getJSON() { | |
| const json = { | |
| keypoints: this.canvas | |
| .getObjects() | |
| .filter((item) => { | |
| if (item.type === "circle") return item; | |
| }) | |
| .map((item) => { | |
| return [Math.round(item.left), Math.round(item.top)]; | |
| }), | |
| }; | |
| return json; | |
| } | |
| loadPreset(json) { | |
| try { | |
| if (json["keypoints"].length % 18 === 0) { | |
| this.setPose(json["keypoints"]); | |
| } else { | |
| throw new Error("keypoints is invalid"); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| } | |
| // 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; | |
| Object.assign(this.openpose.style, { | |
| left: `${transform.a * margin + transform.e}px`, | |
| top: `${transform.d + transform.f}px`, | |
| width: w + "px", | |
| height: w + "px", | |
| position: "absolute", | |
| zIndex: app.graph._nodes.indexOf(node), | |
| }); | |
| Object.assign(this.openpose.children[0].style, { | |
| width: w + "px", | |
| height: w + "px", | |
| }); | |
| Object.assign(this.openpose.children[1].style, { | |
| width: w + "px", | |
| height: w + "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`, | |
| }); | |
| element.hidden = !visible; | |
| }); | |
| }, | |
| }; | |
| // Fabric canvas | |
| let canvasOpenPose = document.createElement("canvas"); | |
| node.openPose = new OpenPose(node, canvasOpenPose); | |
| node.openPose.canvas.setWidth(512); | |
| node.openPose.canvas.setHeight(512); | |
| let widgetCombo = node.widgets.filter((w) => w.type === "combo"); | |
| widgetCombo[0].value = node.name; | |
| widget.openpose = node.openPose.canvas.wrapperEl; | |
| widget.parent = node; | |
| // Create elements undo, redo, clear history | |
| let panelButtons = document.createElement("div"), | |
| undoButton = document.createElement("button"), | |
| redoButton = document.createElement("button"), | |
| historyClearButton = document.createElement("button"); | |
| panelButtons.className = "panelButtons comfy-menu-btns"; | |
| undoButton.textContent = "⟲"; | |
| redoButton.textContent = "⟳"; | |
| historyClearButton.textContent = "✖"; | |
| undoButton.title = "Undo"; | |
| redoButton.title = "Redo"; | |
| historyClearButton.title = "Clear History"; | |
| undoButton.addEventListener("click", () => node.openPose.undo()); | |
| redoButton.addEventListener("click", () => node.openPose.redo()); | |
| historyClearButton.addEventListener("click", () => { | |
| if (confirm(`Delete all pose history of a node "${node.name}"?`)) { | |
| node.openPose.undo_history = []; | |
| node.openPose.redo_history = []; | |
| node.openPose.setPose(default_keypoints); | |
| node.openPose.undo_history.push(node.openPose.getJSON()); | |
| node.openPose.history_change = true; | |
| node.openPose.updateHistoryData(); | |
| } | |
| }); | |
| panelButtons.appendChild(undoButton); | |
| panelButtons.appendChild(redoButton); | |
| panelButtons.appendChild(historyClearButton); | |
| node.openPose.canvas.wrapperEl.appendChild(panelButtons); | |
| 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(); | |
| }); | |
| // 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(); | |
| }; | |
| 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) { | |
| n = graph._nodes[n]; | |
| for (let w in n.widgets) { | |
| let wid = n.widgets[w]; | |
| if (Object.hasOwn(wid, "openpose")) { | |
| wid.openpose.style.left = -8000 + "px"; | |
| wid.openpose.style.position = "absolute"; | |
| } | |
| } | |
| } | |
| }; | |
| return { widget: widget }; | |
| } | |
| window.LS_Poses = {}; | |
| function LS_Save() { | |
| ///console.log("Save:", LS_Poses); | |
| localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses)); | |
| } | |
| app.registerExtension({ | |
| name: "comfy.easyuse.poseEditor", | |
| async init(app) { | |
| // Any initial setup to run as soon as the page loads | |
| let style = document.createElement("style"); | |
| style.innerText = `.panelButtons{ | |
| position: absolute; | |
| padding: 4px; | |
| display: flex; | |
| gap: 4px; | |
| flex-direction: column; | |
| width: fit-content; | |
| } | |
| .panelButtons button:last-child{ | |
| border-color: var(--error-text); | |
| color: var(--error-text) !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| }, | |
| async setup(app) { | |
| let openPoseNode = app.graph._nodes.filter((wi) => wi.type == "easy poseEditor"); | |
| 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 beforeRegisterNodeDef(nodeType, nodeData, app) { | |
| if (nodeData.name === "easy poseEditor") { | |
| const onNodeCreated = nodeType.prototype.onNodeCreated; | |
| nodeType.prototype.onNodeCreated = function () { | |
| const r = onNodeCreated | |
| ? onNodeCreated.apply(this, arguments) | |
| : undefined; | |
| let openPoseNode = app.graph._nodes.filter( | |
| (wi) => {wi.type == "easy poseEditor"} | |
| ), | |
| nodeName = `Pose_${openPoseNode.length}`, | |
| nodeNamePNG = `${nodeName}.png`; | |
| console.log(`Create PoseNode: ${nodeName}`); | |
| 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: [], | |
| }; | |
| LS_Save(); | |
| } | |
| createOpenPose.apply(this, [this, nodeNamePNG, {}, app]); | |
| setTimeout(() => { | |
| this.openPose.uploadPoseFile(nodeNamePNG); | |
| }, 1); | |
| this.setSize([530, 620]); | |
| return r; | |
| }; | |
| } | |
| }, | |
| }); | |