| import { app } from "../../scripts/app.js";
|
| import { api } from "../../scripts/api.js";
|
|
|
| const getRealURL = obj => {
|
| return api.apiURL(`/view?filename=${encodeURIComponent(obj.filename)}&type=${obj.type}&subfolder=${obj.subfolder}&rand=${Math.random()}`)
|
| }
|
| const makeUUID = _ =>{
|
| let dt = new Date().getTime()
|
| const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
| const r = ((dt + Math.random() * 16) % 16) | 0
|
| dt = Math.floor(dt / 16)
|
| return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
| })
|
| return uuid
|
| }
|
| const chainCallback = (object, property, callback) => {
|
| if (object == undefined) {
|
|
|
| console.error("Tried to add callback to non-existant object")
|
| return;
|
| }
|
| if (property in object) {
|
| const callback_orig = object[property]
|
| object[property] = function () {
|
| const r = callback_orig.apply(this, arguments);
|
| callback.apply(this, arguments);
|
| return r
|
| };
|
| } else {
|
| object[property] = callback;
|
| }
|
| }
|
| const getUserSettingsValue = id => id ? app?.ui?.settings?.getSettingValue(id) : null
|
| |
| |
| |
| |
| |
|
|
| const getSetting = (id, storage_key=null) => {
|
| try{
|
| let setting = id ? getUserSettingsValue(id) : null
|
| if(setting === null || setting === undefined) setting = storage_key ? localStorage[storage_key] : (localStorage[id] || null)
|
| return setting
|
| }
|
| catch(e){
|
| console.error(e)
|
| return null
|
| }
|
| }
|
| export const $t = (word) => {
|
| let _locale = getSetting('Comfy.Locale') || 'en'
|
| switch (_locale){
|
| case 'zh-CN':
|
| case 'zh':
|
| return word['zh'] || word['en'] || word
|
| default:
|
| return word['en'] || word
|
| }
|
| }
|
|
|
|
|
| app.registerExtension({
|
| name: "Comfy.EasyUse.FramesEditor",
|
| async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
| const nodeName = nodeData.name;
|
| if (nodeName === "easy framesEditor"){
|
| chainCallback(nodeType.prototype, "onNodeCreated", function() {
|
| const container = document.createElement("div");
|
| container.style.cssText = "position: relative; width: 100%; height: 100%; background: #0f1011; overflow: hidden; box-sizing: border-box;border-radius:4px; margin: 0; padding: 0; display: flex; flex-direction: column;";
|
|
|
|
|
| const toolbar = document.createElement("div");
|
| toolbar.style.cssText = "flex: 0 0 32px; width: 100%; background: #222; display: flex; align-items: center; justify-content: space-between; padding: 0 4px; box-sizing: border-box; border-bottom: 1px solid #333; z-index: 10;";
|
|
|
|
|
| const leftGroup = document.createElement("div");
|
| leftGroup.style.display = "flex";
|
| leftGroup.style.gap = "4px";
|
|
|
| const createBtn = (iconSvg, title, onClick, isActive = false) => {
|
| const btn = document.createElement("div");
|
| btn.style.cssText = `width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 4px; color: ${isActive ? '#fff' : '#ccc'}; background-color: ${isActive ? '#444' : 'transparent'};`;
|
| btn.innerHTML = iconSvg;
|
| btn.title = title;
|
| btn.onmouseover = () => { if (!btn.classList.contains("active")) btn.style.backgroundColor = "#333"; };
|
| btn.onmouseout = () => { if (!btn.classList.contains("active")) btn.style.backgroundColor = "transparent"; };
|
| btn.onclick = onClick;
|
| return btn;
|
| };
|
|
|
| const undoIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>`;
|
| const redoIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>`;
|
| const resetIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`;
|
| const pointIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`;
|
| const boxIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke-dasharray="4 4"/></svg>`;
|
|
|
|
|
| const undoBtn = createBtn(undoIcon, "Undo", () => this.undo());
|
| const redoBtn = createBtn(redoIcon, "Redo", () => this.redo());
|
| const resetBtn = createBtn(resetIcon, "Clear All", () => {
|
| const { positivePoints, negativePoints, bboxes } = this.canvasWidget;
|
| if (positivePoints.length === 0 && negativePoints.length === 0 && bboxes.length === 0) return;
|
| this.canvasWidget.positivePoints = [];
|
| this.canvasWidget.negativePoints = [];
|
| this.canvasWidget.bboxes = [];
|
| this.canvasWidget.history = [];
|
| this.canvasWidget.historyIndex = -1;
|
| this.redrawCanvas();
|
| this.updateUndoRedoUI();
|
| this.updateWidgetValue();
|
| });
|
|
|
| leftGroup.appendChild(undoBtn);
|
| leftGroup.appendChild(redoBtn);
|
| leftGroup.appendChild(resetBtn);
|
|
|
|
|
| const rightGroup = document.createElement("div");
|
| rightGroup.style.display = "flex";
|
| rightGroup.style.gap = "4px";
|
|
|
| let pointBtn, boxBtn;
|
|
|
| const setMode = (mode) => {
|
| this.canvasWidget.mode = mode;
|
| pointBtn.style.backgroundColor = mode === 'point' ? '#444' : 'transparent';
|
| pointBtn.classList.toggle("active", mode === 'point');
|
| pointBtn.style.color = mode === 'point' ? '#fff' : '#ccc';
|
|
|
| boxBtn.style.backgroundColor = mode === 'box' ? '#444' : 'transparent';
|
| boxBtn.classList.toggle("active", mode === 'box');
|
| boxBtn.style.color = mode === 'box' ? '#fff' : '#ccc';
|
| };
|
|
|
| pointBtn = createBtn(pointIcon, "Point Mode", () => setMode('point'), true);
|
| pointBtn.classList.add("active");
|
| boxBtn = createBtn(boxIcon, "Box Mode", () => setMode('box'), false);
|
|
|
| rightGroup.appendChild(pointBtn);
|
| rightGroup.appendChild(boxBtn);
|
|
|
| toolbar.appendChild(leftGroup);
|
| toolbar.appendChild(rightGroup);
|
| container.appendChild(toolbar);
|
|
|
|
|
| const canvasWrapper = document.createElement("div");
|
| canvasWrapper.style.cssText = "flex: 1; width: 100%; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #0f1011;";
|
| container.appendChild(canvasWrapper);
|
|
|
|
|
| const canvas = document.createElement("canvas");
|
| canvas.width = 512;
|
| canvas.height = 512;
|
|
|
| canvas.style.cssText = "display: block; max-width: 100%; max-height: 100%; object-fit: contain; cursor: crosshair; margin: 0 auto;";
|
| canvasWrapper.appendChild(canvas);
|
|
|
| const ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
| const tracker = document.createElement("div");
|
| tracker.style.cssText = "flex: 0 0 32px; width: 100%; background: #222; display: none; align-items: center; justify-content: space-between; padding: 0 8px; box-sizing: border-box; border-top: 1px solid #333; gap: 2px;";
|
|
|
| const frameInfo = document.createElement("div");
|
| frameInfo.style.cssText = "color: #ccc; font-family: monospace; font-size: 12px; min-width: 40px; text-align: center; user-select: none;";
|
| frameInfo.innerText = "0/0";
|
|
|
| const slider = document.createElement("input");
|
| slider.type = "range";
|
| slider.min = "0";
|
| slider.max = "0";
|
| slider.value = "0";
|
| slider.step = "1";
|
| slider.style.cssText = "flex: 1; height: 4px; cursor: pointer;";
|
|
|
| tracker.appendChild(frameInfo);
|
| tracker.appendChild(slider);
|
| container.appendChild(tracker);
|
|
|
| slider.addEventListener("input", (e) => {
|
| const frameIndex = parseInt(e.target.value);
|
| this.canvasWidget.frameIndex = frameIndex;
|
| this.canvasWidget.frameInfo.innerText = `${frameIndex + 1}/${this.canvasWidget.previewFrames.length}`;
|
| this.updateWidgetValue();
|
|
|
| const img = new Image();
|
| img.onload = () => {
|
| this.canvasWidget.image = img;
|
| canvas.width = img.width;
|
| canvas.height = img.height;
|
| this.redrawCanvas();
|
| };
|
| img.src = getRealURL(this.canvasWidget.previewFrames[frameIndex]);
|
| });
|
|
|
|
|
| this.canvasWidget = {
|
| canvas: canvas,
|
| ctx: ctx,
|
| container: container,
|
| tracker: tracker,
|
| slider: slider,
|
| frameInfo: frameInfo,
|
| image: null,
|
| positivePoints: [],
|
| negativePoints: [],
|
| bboxes: [],
|
| hoverBBox: null,
|
| hoveredPoint: null,
|
| mode: 'point',
|
| history: [],
|
| historyIndex: -1,
|
| isDrawingBox: false,
|
| currentBox: null,
|
| frameIndex:0,
|
| previewFrames: [],
|
| };
|
|
|
|
|
| const widget = this.addDOMWidget("canvas", "points_editor", container, );
|
| this.uuid = makeUUID();
|
|
|
| this.canvasWidget.domWidget = widget;
|
|
|
|
|
| const infoWidget = this.widgets.find(w=> w.name == 'info')
|
| setTimeout(_=>{
|
| if(infoWidget && infoWidget.value) {
|
| try {
|
| const info = JSON.parse(infoWidget.value);
|
|
|
| if (Array.isArray(info.positive_coords)) {
|
| this.canvasWidget.positivePoints = info.positive_coords;
|
| }
|
| if (Array.isArray(info.negative_coords)) {
|
| this.canvasWidget.negativePoints = info.negative_coords;
|
| }
|
|
|
| if (Array.isArray(info.bbox)) {
|
| this.canvasWidget.bboxes = info.bbox;
|
| }
|
|
|
| if (typeof info.frame_index === 'number' && this.canvasWidget.slider) {
|
| this.canvasWidget.frameIndex = info.frame_index;
|
| this.canvasWidget.slider.value = info.frame_index;
|
| this.canvasWidget.frameInfo.innerText = `${info.frame_index + 1}/${this.canvasWidget.previewFrames.length}`;
|
| }
|
| this.redrawCanvas();
|
| } catch (e) {
|
|
|
| }
|
| }
|
| },1)
|
|
|
|
|
| widget.computeSize = (width) => {
|
|
|
|
|
| const nodeHeight = this.size ? this.size[1] : 500;
|
| const widgetHeight = Math.max(245, nodeHeight - 150);
|
| return [width, widgetHeight];
|
| };
|
|
|
|
|
|
|
| chainCallback(this, "onResize", function(size) {
|
|
|
|
|
| const containerHeight = Math.max(245, size[1] - 150);
|
| container.style.height = containerHeight + "px";
|
| });
|
|
|
|
|
| chainCallback(this, "onDrawForeground", function(ctx) {
|
|
|
| const containerHeight = Math.max(245, this.size[1] - 150);
|
| if (container.style.height !== containerHeight + "px") {
|
| container.style.height = containerHeight + "px";
|
| }
|
| });
|
|
|
|
|
| chainCallback(this, "onExecuted", function(message) {
|
| if (message.preview && message.preview[0]) {
|
| const {preview_str, is_init} = message.preview[0];
|
| const previewData = JSON.parse(preview_str);
|
| this.canvasWidget.previewFrames = previewData;
|
|
|
| if(is_init){
|
| if(this.canvasWidget.frameIndex>=previewData.length-1){
|
| this.canvasWidget.frameIndex = 0;
|
| this.restoreState({ positivePoints: [], negativePoints: [], bboxes: [] });
|
| this.updateWidgetValue();
|
| this.canvasWidget.history = [];
|
| this.canvasWidget.historyIndex = -1;
|
| this.updateUndoRedoUI();
|
| }
|
| }
|
| if (previewData.length > 1) {
|
| this.canvasWidget.tracker.style.display = "flex";
|
| slider.max = previewData.length - 1;
|
| slider.value = this.canvasWidget.frameIndex;
|
| this.canvasWidget.slider = slider;
|
| this.canvasWidget.frameInfo.innerText = `${this.canvasWidget.frameIndex + 1}/${previewData.length}`;
|
| } else {
|
| this.canvasWidget.tracker.style.display = "none";
|
| }
|
|
|
| const img = new Image();
|
| img.onload = () => {
|
|
|
| this.canvasWidget.image = img;
|
| canvas.width = img.width;
|
| canvas.height = img.height;
|
|
|
| this.redrawCanvas();
|
| };
|
|
|
| if(previewData?.length>0){
|
| if (this.canvasWidget.frameIndex >= previewData.length) {
|
| this.canvasWidget.frameIndex = 0;
|
| slider.value = 0;
|
| this.canvasWidget.slider = slider;
|
| }
|
| img.src = getRealURL(previewData[this.canvasWidget.frameIndex]);
|
| }
|
|
|
| }
|
| });
|
|
|
|
|
| this.addToHistory = () => {
|
| const { positivePoints, negativePoints, bboxes, history, historyIndex } = this.canvasWidget;
|
|
|
| if (historyIndex < history.length - 1) {
|
| this.canvasWidget.history = history.slice(0, historyIndex + 1);
|
| }
|
|
|
| const state = {
|
| positivePoints: JSON.parse(JSON.stringify(positivePoints)),
|
| negativePoints: JSON.parse(JSON.stringify(negativePoints)),
|
| bboxes: JSON.parse(JSON.stringify(bboxes))
|
| };
|
| this.canvasWidget.history.push(state);
|
| this.canvasWidget.historyIndex++;
|
| this.updateUndoRedoUI();
|
| this.updateWidgetValue();
|
| };
|
|
|
| this.undo = () => {
|
| const { history, historyIndex } = this.canvasWidget;
|
| if (historyIndex > 0) {
|
| this.canvasWidget.historyIndex--;
|
| const state = history[this.canvasWidget.historyIndex];
|
| this.restoreState(state);
|
| } else if (historyIndex === 0) {
|
|
|
|
|
|
|
| this.canvasWidget.historyIndex--;
|
| this.restoreState({ positivePoints: [], negativePoints: [], bboxes: [] });
|
| }
|
| this.updateUndoRedoUI();
|
| };
|
|
|
| this.redo = () => {
|
| const { history, historyIndex } = this.canvasWidget;
|
| if (historyIndex < history.length - 1) {
|
| this.canvasWidget.historyIndex++;
|
| const state = history[this.canvasWidget.historyIndex];
|
| this.restoreState(state);
|
| }
|
| this.updateUndoRedoUI();
|
| };
|
|
|
| this.restoreState = (state) => {
|
| this.canvasWidget.positivePoints = JSON.parse(JSON.stringify(state.positivePoints));
|
| this.canvasWidget.negativePoints = JSON.parse(JSON.stringify(state.negativePoints));
|
| this.canvasWidget.bboxes = JSON.parse(JSON.stringify(state.bboxes));
|
| this.redrawCanvas();
|
| this.updateWidgetValue();
|
| };
|
|
|
| this.updateUndoRedoUI = () => {
|
| const { historyIndex, history, positivePoints, negativePoints, bboxes } = this.canvasWidget;
|
| undoBtn.style.color = historyIndex >= 0 ? '#ccc' : '#555';
|
| undoBtn.style.cursor = historyIndex >= 0 ? 'pointer' : 'default';
|
| redoBtn.style.color = historyIndex < history.length - 1 ? '#ccc' : '#555';
|
| redoBtn.style.cursor = historyIndex < history.length - 1 ? 'pointer' : 'default';
|
|
|
| const hasContent = positivePoints.length > 0 || negativePoints.length > 0 || bboxes.length > 0;
|
| resetBtn.style.color = hasContent ? '#ccc' : '#555';
|
| resetBtn.style.cursor = hasContent ? 'pointer' : 'default';
|
| };
|
|
|
| this.updateWidgetValue = () => {
|
| const { positivePoints, negativePoints, bboxes, image, frameIndex } = this.canvasWidget;
|
| const info_widget = this.widgets.find(w=> w.name == 'info')
|
| info_widget.value = image ? JSON.stringify({
|
| positive_coords: positivePoints,
|
| negative_coords: negativePoints,
|
| bbox: bboxes,
|
| frame_index: frameIndex
|
| }) : '';
|
| }
|
|
|
|
|
| const getCoords = (e) => {
|
| const rect = canvas.getBoundingClientRect();
|
| const scaleX = canvas.width / rect.width;
|
| const scaleY = canvas.height / rect.height;
|
| return {
|
| x: (e.clientX - rect.left) * scaleX,
|
| y: (e.clientY - rect.top) * scaleY
|
| };
|
| };
|
|
|
| canvas.addEventListener('mousedown', (e) => {
|
| e.preventDefault();
|
| const coords = getCoords(e);
|
| const { mode, image } = this.canvasWidget;
|
| if(!image) return
|
| if (mode === 'point') {
|
| if (e.button === 0) {
|
| this.canvasWidget.positivePoints.push(coords);
|
| } else if (e.button === 2) {
|
| this.canvasWidget.negativePoints.push(coords);
|
| }
|
| this.addToHistory();
|
| this.redrawCanvas();
|
| } else if (mode === 'box') {
|
| if (e.button === 0) {
|
| this.canvasWidget.isDrawingBox = true;
|
| this.canvasWidget.currentBox = { x: coords.x, y: coords.y, w: 0, h: 0 };
|
| }
|
| }
|
| });
|
|
|
| canvas.addEventListener('mousemove', (e) => {
|
| const coords = getCoords(e);
|
| const { mode, isDrawingBox, currentBox, image } = this.canvasWidget;
|
| if(!image) return
|
| if (mode === 'box' && isDrawingBox && currentBox) {
|
| currentBox.w = coords.x - currentBox.x;
|
| currentBox.h = coords.y - currentBox.y;
|
| this.redrawCanvas();
|
| } else {
|
|
|
|
|
| }
|
| });
|
|
|
| canvas.addEventListener('mouseup', (e) => {
|
| const { mode, isDrawingBox, currentBox, image } = this.canvasWidget;
|
| if(!image) return
|
| if (mode === 'box' && isDrawingBox && currentBox) {
|
|
|
| const box = {
|
| x: Math.min(currentBox.x, currentBox.x + currentBox.w),
|
| y: Math.min(currentBox.y, currentBox.y + currentBox.h),
|
| w: Math.abs(currentBox.w),
|
| h: Math.abs(currentBox.h)
|
| };
|
|
|
| if (box.w > 5 && box.h > 5) {
|
| this.canvasWidget.bboxes.push(box);
|
| this.addToHistory();
|
| }
|
| this.canvasWidget.isDrawingBox = false;
|
| this.canvasWidget.currentBox = null;
|
| this.redrawCanvas();
|
| }
|
| });
|
|
|
| canvas.addEventListener('contextmenu', (e) => {
|
| e.preventDefault();
|
| });
|
|
|
|
|
| this.redrawCanvas();
|
|
|
|
|
| const nodeWidth = Math.max(400, this.size[0] || 400);
|
| const nodeHeight = 530;
|
| this.setSize([nodeWidth, nodeHeight]);
|
|
|
|
|
| container.style.height = "400px";
|
|
|
| this.updateUndoRedoUI();
|
| });
|
|
|
|
|
| nodeType.prototype.redrawCanvas = function() {
|
| const {canvas, ctx, image, positivePoints, negativePoints, bboxes, currentBox, hoveredPoint, mode} = this.canvasWidget;
|
|
|
|
|
| ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
| let pointSize = Math.max(2, Math.min(canvas.width, canvas.height) * 0.008);
|
|
|
|
|
| if (image) {
|
| ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
| } else {
|
| const desc = [
|
| {"en": "Start with your own image or video", "zh": "从您自己的图像或视频开始"},
|
| {"en": "Left Click: Add Positive Point", "zh": "左键点击:添加正面点"},
|
| {"en": "Right Click: Add Negative Point", "zh": "右键点击:添加负面点"},
|
| {"en": "Drag Box: Add Bounding Box", "zh": "拖动框:添加边界框"},
|
| ]
|
|
|
| ctx.fillStyle = "transparent";
|
| ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| ctx.fillStyle = "#ddd";
|
| ctx.font = "34px sans-serif";
|
| ctx.textAlign = "center";
|
| ctx.fillText("🖼️", canvas.width / 2, canvas.height / 2 - 50);
|
| ctx.font = "24px sans-serif";
|
| desc.map((cate,index)=>{
|
| ctx.fillText($t(cate), canvas.width / 2, canvas.height / 2 + (index * 40));
|
| })
|
| }
|
|
|
|
|
| ctx.strokeStyle = "#00f";
|
| ctx.lineWidth = 2;
|
| for (const box of bboxes) {
|
| ctx.strokeRect(box.x, box.y, box.w, box.h);
|
|
|
| ctx.fillStyle = "rgba(0, 0, 255, 0.1)";
|
| ctx.fillRect(box.x, box.y, box.w, box.h);
|
| }
|
|
|
|
|
| if (currentBox) {
|
| ctx.strokeStyle = "#0ff";
|
| ctx.lineWidth = 2;
|
| ctx.setLineDash([5, 5]);
|
| ctx.strokeRect(currentBox.x, currentBox.y, currentBox.w, currentBox.h);
|
| ctx.setLineDash([]);
|
| }
|
|
|
|
|
| ctx.strokeStyle = "#139613";
|
| ctx.fillStyle = "#139613";
|
| for (const point of positivePoints) {
|
| ctx.beginPath();
|
| ctx.arc(point.x, point.y, pointSize, 0, 2 * Math.PI);
|
| ctx.fill();
|
| ctx.lineWidth = 2;
|
| ctx.stroke();
|
| }
|
|
|
|
|
| ctx.strokeStyle = "#8A1616";
|
| ctx.fillStyle = "#8A1616";
|
| for (const point of negativePoints) {
|
| ctx.beginPath();
|
| ctx.arc(point.x, point.y, pointSize, 0, 2 * Math.PI);
|
| ctx.fill();
|
| ctx.lineWidth = 2;
|
| ctx.stroke();
|
| }
|
| };
|
|
|
|
|
| }
|
| }
|
| }) |