From 6abac9e1c2da4a9487e7690a2e052de0fdc4fb50 Mon Sep 17 00:00:00 2001 From: BennyKok Date: Fri, 8 Sep 2023 16:23:25 +0800 Subject: [PATCH] refactor!: extract js scripts --- js/AvatarEditor.js | 31 ++++ js/Container.js | 15 ++ js/ImageEditor.js | 128 ++++++++++++++ js/api.js | 4 + js/app.js | 4 + js/blendshape_editor.js | 368 ---------------------------------------- js/index.js | 183 ++++++++++++++++++++ js/state.js | 33 ++++ js/van.js | 4 + 9 files changed, 402 insertions(+), 368 deletions(-) create mode 100644 js/AvatarEditor.js create mode 100644 js/Container.js create mode 100644 js/ImageEditor.js create mode 100644 js/api.js create mode 100644 js/app.js delete mode 100644 js/blendshape_editor.js create mode 100644 js/index.js create mode 100644 js/state.js create mode 100644 js/van.js diff --git a/js/AvatarEditor.js b/js/AvatarEditor.js new file mode 100644 index 0000000..159fa7d --- /dev/null +++ b/js/AvatarEditor.js @@ -0,0 +1,31 @@ +import { iframeSrc, showEditor } from './state.js'; +import { van } from './van.js'; +const { button, iframe, div, img } = van.tags; + +export function AvatarEditor() { + return div( + { + class: 'w-full h-full', + }, + button( + { + class: () => + 'px-4 py-2 rounded-md absolute left-0 top-0 z-[100] pointer-events-auto ' + + (showEditor.val ? '' : 'hidden'), + onclick: () => { + console.log('close'); + showEditor.val = false; + }, + }, + 'back', + ), + iframe({ + id: 'avatech-editor-iframe', + title: 'avatech-editor-iframe', + name: 'avatech-editor-iframe', + class: () => + 'w-full h-full pointer-events-auto ' + (showEditor.val ? '' : 'hidden'), + src: iframeSrc, + }), + ); +} diff --git a/js/Container.js b/js/Container.js new file mode 100644 index 0000000..3da2ccd --- /dev/null +++ b/js/Container.js @@ -0,0 +1,15 @@ +import { ImageEditor } from './ImageEditor.js'; +import { AvatarEditor } from './AvatarEditor.js'; +import { van } from './van.js'; +const { button, iframe, div, img } = van.tags; + +export function Container() { + return div( + { + class: 'fixed left-0 top-0 w-full h-full z-[200] pointer-events-none', + id: 'avatech-editor', + }, + AvatarEditor(), + ImageEditor() + ); +} diff --git a/js/ImageEditor.js b/js/ImageEditor.js new file mode 100644 index 0000000..5e23966 --- /dev/null +++ b/js/ImageEditor.js @@ -0,0 +1,128 @@ +import { + showImageEditor, + point_label, + imageUrl, + imageContainerSize, + imagePrompts, + targetNode, + imageSize +} from './state.js'; +import { van } from './van.js'; +const { button, iframe, div, img } = van.tags; + +export function ImageEditor() { + return div( + { + class: () => 'absolute flex bg-gray-900 bg-opacity-50 top-0 w-full h-full pointer-events-auto ' + + (showImageEditor.val ? '' : 'hidden'), + }, + button( + { + class: () => 'absolute px-4 py-2 rounded-md left-0 top-0 z-[200] ', + onclick: () => { + console.log('close'); + showImageEditor.val = false; + }, + }, + 'back' + ), + div( + { + class: 'w-full flex justify-center absolute top-0 left-0 right-0 items-center', + }, + button( + { + class: () => ' px-4 py-2 rounded-md left-0 top-0 z-[200] ', + onclick: () => { + point_label.val = 1; + }, + }, + 'Positive' + ), + button( + { + class: () => ' px-4 py-2 rounded-md left-0 top-0 z-[200] ', + onclick: () => { + point_label.val = 0; + }, + }, + 'Negative' + ) + ), + div( + { + class: 'flex items-center justify-center absolute h-full left-0 right-0 bottom-0 top-0 mx-auto my-auto max-h-96 ', + }, + img({ + class: 'w-fit h-full', + src: imageUrl, + onload: (e) => { + imageSize.val = { + width: e.target.naturalWidth, + height: e.target.naturalHeight, + }; + + imageContainerSize.val = { + width: e.target.offsetWidth, + height: e.target.offsetHeight, + }; + }, + onclick: (e) => { + const rect = e.target.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const relativeX = Math.trunc( + (x / e.target.offsetWidth) * imageSize.val.width + ); + const relativeY = Math.trunc( + (y / e.target.offsetHeight) * imageSize.val.height + ); + + imagePrompts.val = [ + ...imagePrompts.val, + { x: relativeX, y: relativeY, label: point_label.val }, + ]; + + // targetWidget.widgets.find((x) => x.name === 'x').value = relativeX; + // targetWidget.widgets.find((x) => x.name === 'y').value = relativeY; + targetNode.val.widgets.find( + (x) => x.name === 'image_prompts_json' + ).value = JSON.stringify(imagePrompts.val); + targetNode.val.graph.change(); + // showImageEditor.val = false; + }, + }), + () => { + return div( + { + class: 'absolute w-full h-full pointer-events-none', + style: () => `width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`, + }, + ...imagePrompts.val.map((point) => { + return div( + { + class: 'absolute', + style: () => `left: ${(point.x / imageSize.val.width) * + imageContainerSize.val.width}px; top: ${(point.y / imageSize.val.height) * + imageContainerSize.val.height}px; transform: translate(-50%, -50%);`, + }, + button({ + class: () => `w-4 h-4 rounded-full pointer-events-auto ${point.label === 1 ? 'bg-green-500' : 'bg-red-500'}`, + onclick: (e) => { + e.preventDefault(); + imagePrompts.val = imagePrompts.val.filter( + (x) => x.x !== point.x && x.y !== point.y + ); + targetNode.val.widgets.find( + (x) => x.name === 'image_prompts_json' + ).value = JSON.stringify(imagePrompts.val); + targetNode.val.graph.change(); + }, + }) + ); + }) + ); + } + ) + ); +} diff --git a/js/api.js b/js/api.js new file mode 100644 index 0000000..f152d28 --- /dev/null +++ b/js/api.js @@ -0,0 +1,4 @@ +/** @typedef {import('../../../web/scripts/api.js').api} API*/ +import { api as _api } from '../../scripts/api.js'; +/** @type {API} */ +export const api = _api; diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..53ccc05 --- /dev/null +++ b/js/app.js @@ -0,0 +1,4 @@ +/** @typedef {import('../../../web/scripts/app.js').ComfyApp} ComfyApp*/ +import { app as _app } from '../../scripts/app.js'; +/** @type {ComfyApp} */ +export const app = _app; diff --git a/js/blendshape_editor.js b/js/blendshape_editor.js deleted file mode 100644 index c2a34c9..0000000 --- a/js/blendshape_editor.js +++ /dev/null @@ -1,368 +0,0 @@ -/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ -/** @typedef {import('../../../web/scripts/app.js').ComfyApp} ComfyApp*/ -/** @typedef {import('../../../web/scripts/api.js').api} API*/ - -import { app as _app } from '../../scripts/app.js'; -import { api as _api } from '../../scripts/api.js'; - -/** @type {ComfyApp} */ -const app = _app; - -/** @type {API} */ -const api = _api; - -import * as _van from 'https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.1.3.min.js'; -/** @type {import('./van-1.1.3.min.js').Van} */ -const van = _van.default; - -function addMenuHandler(nodeType, cb) { - const getOpts = nodeType.prototype.getExtraMenuOptions; - nodeType.prototype.getExtraMenuOptions = function () { - const r = getOpts.apply(this, arguments); - cb.apply(this, arguments); - return r; - }; -} - -const iframeSrc = van.state('https://editor.avatech.ai'); -const showEditor = van.state(false); -const showImageEditor = van.state(false); -const imageUrl = van.state(''); -const point_label = van.state(1); -const imageContainerSize = van.state({ - width: 0, - height: 0, -}); - -/** - * @typedef {Object} Point - * @property {number} x - The x coordinate - * @property {number} y - The y coordinate - * @property {number} label - The label - */ - -/** @type {import('./van-1.1.3.min.js').State} */ -const imagePrompts = van.state([]); -let targetWidget; -let imageSize = { width: 0, height: 0 }; - -function openInAvatechEditor(url, fileName) { - let editor = document.getElementById('avatech-editor-iframe'); - iframeSrc.val = url; - showEditor.val = true; - - editor.contentWindow.postMessage( - { - key: 'key', - value: fileName, - method: 'store', - }, - '*', - ); -} - -/** @type {ComfyExtension} */ -const ext = { - getCustomWidgets(app) { - return { - SAM_PROMPTS(node, inputName, inputData, app) { - const btn = node.addWidget('button', 'Edit prompt','', () => { - showMyImageEditor(node); - }); - btn.serialize = false; - - return { - widget: btn, - }; - }, - }; - }, - name: 'Avatech.Avatar.BlendshapeEditor', - init(app) {}, - - async setup() { - const graphCanvas = document.getElementById('graph-canvas'); - window.addEventListener('keydown', (event) => { - console.log(event.key); - - if (event.key === 'b') { - event.preventDefault(); - const currentGraph = app.graph.list_of_graphcanvas[0]; - if (currentGraph.selected_nodes.length !== 1) { - Object.values(currentGraph.selected_nodes).forEach((targetNode) => { - if (targetNode.mode === 4) targetNode.mode = 0; - else targetNode.mode = 4; - }); - } else { - const targetNode = currentGraph.current_node; - if (targetNode.mode === 4) targetNode.mode = 0; - else targetNode.mode = 4; - } - app.graph.change(); - } - - if (event.key === 'Escape') { - event.preventDefault(); - showImageEditor.val = false; - } - - // if enter - if (event.key === 'Enter') { - event.preventDefault(); - document.getElementById('queue-button').click(); - } - }, { - capture: true, - }); - - const { button, iframe, div, img } = van.tags; - - const AvatarEditor = () => { - return div( - { - class: 'w-full h-full', - }, - button( - { - class: () => - 'px-4 py-2 rounded-md absolute left-0 top-0 z-[100] pointer-events-auto ' + - (showEditor.val ? '' : 'hidden'), - onclick: () => { - console.log('close'); - showEditor.val = false; - }, - }, - 'back', - ), - iframe({ - id: 'avatech-editor-iframe', - title: 'avatech-editor-iframe', - name: 'avatech-editor-iframe', - class: () => - 'w-full h-full pointer-events-auto ' + - (showEditor.val ? '' : 'hidden'), - src: iframeSrc, - }), - ); - }; - - const ImageEditor = () => { - return div( - { - class: () => - 'absolute flex bg-gray-900 bg-opacity-50 top-0 w-full h-full pointer-events-auto ' + - (showImageEditor.val ? '' : 'hidden'), - }, - button( - { - class: () => 'absolute px-4 py-2 rounded-md left-0 top-0 z-[200] ', - onclick: () => { - console.log('close'); - showImageEditor.val = false; - }, - }, - 'back', - ), - div( - { - class: - 'w-full flex justify-center absolute top-0 left-0 right-0 items-center', - }, - button( - { - class: () => ' px-4 py-2 rounded-md left-0 top-0 z-[200] ', - onclick: () => { - point_label.val = 1; - }, - }, - 'Positive', - ), - button( - { - class: () => ' px-4 py-2 rounded-md left-0 top-0 z-[200] ', - onclick: () => { - point_label.val = 0; - }, - }, - 'Negative', - ), - ), - div( - { - class: - 'flex items-center justify-center absolute h-full left-0 right-0 bottom-0 top-0 mx-auto my-auto max-h-96 ', - }, - img({ - class: 'w-fit h-full', - src: imageUrl, - onload: (e) => { - imageSize = { - width: e.target.naturalWidth, - height: e.target.naturalHeight, - }; - - imageContainerSize.val = { - width: e.target.offsetWidth, - height: e.target.offsetHeight, - }; - }, - onclick: (e) => { - const rect = e.target.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - const relativeX = Math.trunc( - (x / e.target.offsetWidth) * imageSize.width, - ); - const relativeY = Math.trunc( - (y / e.target.offsetHeight) * imageSize.height, - ); - - imagePrompts.val = [ - ...imagePrompts.val, - { x: relativeX, y: relativeY, label: point_label.val }, - ]; - - // targetWidget.widgets.find((x) => x.name === 'x').value = relativeX; - // targetWidget.widgets.find((x) => x.name === 'y').value = relativeY; - targetWidget.widgets.find( - (x) => x.name === 'image_prompts_json', - ).value = JSON.stringify(imagePrompts.val); - targetWidget.graph.change(); - // showImageEditor.val = false; - }, - }), - () => { - return div( - { - class: 'absolute w-full h-full pointer-events-none', - style: () => - `width: ${imageContainerSize.val.width}px; height: ${imageContainerSize.val.height}px;`, - }, - ...imagePrompts.val.map((point) => { - return div( - { - class: 'absolute', - style: () => - `left: ${ - (point.x / imageSize.width) * - imageContainerSize.val.width - }px; top: ${ - (point.y / imageSize.height) * - imageContainerSize.val.height - }px; transform: translate(-50%, -50%);`, - }, - button({ - class: () => - `w-4 h-4 rounded-full pointer-events-auto ${ - point.label === 1 ? 'bg-green-500' : 'bg-red-500' - }`, - onclick: (e) => { - e.preventDefault(); - imagePrompts.val = imagePrompts.val.filter( - (x) => x.x !== point.x && x.y !== point.y, - ); - targetWidget.widgets.find( - (x) => x.name === 'image_prompts_json', - ).value = JSON.stringify(imagePrompts.val); - targetWidget.graph.change(); - }, - }), - ); - }), - ); - }, - ), - ); - }; - - const Container = () => { - return div( - { - class: 'fixed left-0 top-0 w-full h-full z-[200] pointer-events-none', - id: 'avatech-editor', - }, - AvatarEditor(), - ImageEditor(), - ); - }; - - van.add(document.body, Container()); - }, - - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === 'ExportGLTF') { - addMenuHandler(nodeType, function (_, options) { - const output = app.nodeOutputs[this.id + '']; - if (!output || !output.gltfFilename) return; - - const gltfFilename = - window.location.protocol + - '//' + - api.api_host + - api.api_base + - `/view?filename=${output.gltfFilename[0]}`; - - options.unshift({ - content: 'Save file', - callback: () => { - const a = document.createElement('a'); - let url = new URL(gltfFilename); - url.searchParams.delete('preview'); - a.href = url; - a.setAttribute( - 'download', - new URLSearchParams(url.search).get('filename'), - ); - document.body.append(a); - a.click(); - requestAnimationFrame(() => a.remove()); - }, - }); - - options.unshift({ - content: 'Open In Avatech Editor (Local)', - callback: () => { - openInAvatechEditor('http://localhost:3006', gltfFilename); - }, - }); - - options.unshift({ - content: 'Open In Avatech Editor', - callback: () => { - openInAvatechEditor('https://editor.avatech.ai', gltfFilename); - }, - }); - }); - } else if (nodeData.name === 'SAM_Prompt_Image') { - nodeData.input.required.sam = ['SAM_PROMPTS']; - nodeData.input.required.upload = ['IMAGEUPLOAD']; - // nodeData.input.required.prompts_points = ["IMAGEUPLOAD"]; - addMenuHandler(nodeType, function (_, options) { - options.unshift({ - content: 'Open In Points Editor (Local)', - callback: () => { - showMyImageEditor(this) - }, - }); - }); - } - }, -}; - -function showMyImageEditor(node) { - showImageEditor.val = true; - - imagePrompts.val = JSON.parse( - node.widgets.find((x) => x.name === 'image_prompts_json').value, - ); - imageUrl.val = api.apiURL( - `/view?filename=${encodeURIComponent( - node.widgets.find((x) => x.name === 'image').value, - )}&type=input&subfolder=`, - ); - targetWidget = node; - // console.log(imageUrl.val, api.api_base); - // console.log(this); -} - -app.registerExtension(ext); diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..6c15cb1 --- /dev/null +++ b/js/index.js @@ -0,0 +1,183 @@ +import { + iframeSrc, + showEditor, + showImageEditor, + imageUrl, + imagePrompts, + targetNode, +} from './state.js'; +import { van } from './van.js'; +import { app } from './app.js'; +import { api } from './api.js'; +import { Container } from './Container.js'; + +/** + * Adds a menu handler to a node type. + * @param {Object} nodeType - The type of the node. + * @param {Function} cb - The callback function to handle the menu. + */ +function addMenuHandler(nodeType, cb) { + const getOpts = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments); + cb.apply(this, arguments); + return r; + }; +} + +function openInAvatechEditor(url, fileName) { + let editor = document.getElementById('avatech-editor-iframe'); + iframeSrc.val = url; + showEditor.val = true; + + editor.contentWindow.postMessage( + { + key: 'key', + value: fileName, + method: 'store', + }, + '*', + ); +} + +/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ +/** @type {ComfyExtension} */ +const ext = { + getCustomWidgets(app) { + return { + SAM_PROMPTS(node, inputName, inputData, app) { + const btn = node.addWidget('button', 'Edit prompt', '', () => { + showMyImageEditor(node); + }); + btn.serialize = false; + + return { + widget: btn, + }; + }, + }; + }, + + name: 'Avatech.Avatar.BlendshapeEditor', + + init(app) {}, + + async setup() { + const graphCanvas = document.getElementById('graph-canvas'); + window.addEventListener( + 'keydown', + (event) => { + if (event.key === 'b') { + event.preventDefault(); + const currentGraph = app.graph.list_of_graphcanvas[0]; + if (currentGraph.selected_nodes.length !== 1) { + Object.values(currentGraph.selected_nodes).forEach((targetNode) => { + if (targetNode.mode === 4) targetNode.mode = 0; + else targetNode.mode = 4; + }); + } else { + const targetNode = currentGraph.current_node; + if (targetNode.mode === 4) targetNode.mode = 0; + else targetNode.mode = 4; + } + app.graph.change(); + } + + if (event.key === 'Escape') { + event.preventDefault(); + showImageEditor.val = false; + } + }, + { + capture: true, + }, + ); + + window.addEventListener('keydown', (event) => { + // if enter + if (event.key === 'Enter') { + event.preventDefault(); + document.getElementById('queue-button').click(); + } + }); + + van.add(document.body, Container()); + }, + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === 'ExportGLTF') { + addMenuHandler(nodeType, function (_, options) { + const output = app.nodeOutputs[this.id + '']; + if (!output || !output.gltfFilename) return; + + const gltfFilename = + window.location.protocol + + '//' + + api.api_host + + api.api_base + + `/view?filename=${output.gltfFilename[0]}`; + + options.unshift({ + content: 'Save file', + callback: () => { + const a = document.createElement('a'); + let url = new URL(gltfFilename); + url.searchParams.delete('preview'); + a.href = url; + a.setAttribute( + 'download', + new URLSearchParams(url.search).get('filename'), + ); + document.body.append(a); + a.click(); + requestAnimationFrame(() => a.remove()); + }, + }); + + options.unshift({ + content: 'Open In Avatech Editor (Local)', + callback: () => { + openInAvatechEditor('http://localhost:3006', gltfFilename); + }, + }); + + options.unshift({ + content: 'Open In Avatech Editor', + callback: () => { + openInAvatechEditor('https://editor.avatech.ai', gltfFilename); + }, + }); + }); + } else if (nodeData.name === 'SAM_Prompt_Image') { + nodeData.input.required.sam = ['SAM_PROMPTS']; + nodeData.input.required.upload = ['IMAGEUPLOAD']; + // nodeData.input.required.prompts_points = ["IMAGEUPLOAD"]; + addMenuHandler(nodeType, function (_, options) { + options.unshift({ + content: 'Open In Points Editor (Local)', + callback: () => { + showMyImageEditor(this); + }, + }); + }); + } + }, +}; + +function showMyImageEditor(node) { + showImageEditor.val = true; + + imagePrompts.val = JSON.parse( + node.widgets.find((x) => x.name === 'image_prompts_json').value, + ); + imageUrl.val = api.apiURL( + `/view?filename=${encodeURIComponent( + node.widgets.find((x) => x.name === 'image').value, + )}&type=input&subfolder=`, + ); + targetNode.val = node; + // console.log(imageUrl.val, api.api_base); + // console.log(this); +} + +app.registerExtension(ext); diff --git a/js/state.js b/js/state.js new file mode 100644 index 0000000..983ea6a --- /dev/null +++ b/js/state.js @@ -0,0 +1,33 @@ +/** + * @typedef {import('./van-1.1.3.min.js').State} State + * @template T + */ + +/** + * @typedef {import('../../../web/types/litegraph.js').LGraphNode} LGraphNode + * + * @typedef {Object} Point + * @property {number} x - The x coordinate + * @property {number} y - The y coordinate + * @property {number} label - The label + */ + +import { van } from './van.js'; + +export const iframeSrc = van.state('https://editor.avatech.ai'); +export const showEditor = van.state(false); +export const showImageEditor = van.state(false); +export const imageUrl = van.state(''); +export const point_label = van.state(1); +export const imageContainerSize = van.state({ + width: 0, + height: 0, +}); + +/** @type {State} */ +export const imagePrompts = van.state([]); + +/** @type {State} */ +export const targetNode = van.state(); + +export const imageSize = van.state({ width: 0, height: 0 }); diff --git a/js/van.js b/js/van.js new file mode 100644 index 0000000..2c9e31d --- /dev/null +++ b/js/van.js @@ -0,0 +1,4 @@ +import * as _van from 'https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.1.3.min.js'; + +/** @type {import('./van-1.1.3.min.js').Van} */ +export const van = _van.default;