667 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
};
}
},
});