diff --git a/README.md b/README.md
index 18f7051..0b0d420 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,88 @@
-# ComfyUI-Benripack
+# ComfyUI Benripack Nodes
-## Description
+This package contains three custom nodes for ComfyUI:
-ComfyUI-Benripack is an extension for ComfyUI that provides a CharacterPipe node. This node allows for managing various elements such as images, prompts, and models in a single structure, simplifying the workflow for character-based image generation.
+1. Benri Character Pipe
+2. Benri Load 3D Model
+3. Benri Animation Extractor
-## Features
+## 1. Benri Character Pipe
-- Management of face and pose images
-- Handling of positive and negative master prompts
-- Handling of positive and negative character prompts
-- Integration of model, CLIP, VAE, and IPAdapter
+This node manages character information and prompts for image generation.
+
+### Parameters:
+
+- **character_pipe** (optional): An existing CHARACTER_PIPE object
+- **face_image** (optional): A face image (IMAGE)
+- **pose_image** (optional): A pose image (IMAGE)
+- **add_positive_master_prompt** (optional): Positive master prompt (CONDITIONING)
+- **add_negative_master_prompt** (optional): Negative master prompt (CONDITIONING)
+- **add_positive_char_prompt** (optional): Positive character prompt (CONDITIONING)
+- **add_negative_char_prompt** (optional): Negative character prompt (CONDITIONING)
+- **model** (optional): An AI model (MODEL)
+- **clip** (optional): A CLIP model (CLIP)
+- **vae** (optional): A VAE model (VAE)
+- **ipadapter** (optional): An IPAdapter model (IPADAPTER)
+
+### Functionality:
+
+The node combines all inputs into a CharacterPipe object that can be used for further processing in the image generation pipeline.
+
+## 2. Benri Load 3D Model
+
+This node loads a 3D model (.fbx) from the 3d_models directory in the root of your ComfyUI installation. Before using this node create the
+directory 3d_models. Than put in some .fbx files.
+
+### Parameters:
+
+- **model_name** (required): Name of the 3D model to load
+
+### Functionality:
+
+The node loads the selected 3D model.
+
+## 3. Benri Animation Extractor
+
+This node extracts animation frames from a 3D model. If you select 4 rows and 4 columns the node will take a snapshot every 1/16 of the
+animation time. You can set the orientation of the model. You can also rotate the model. The created tile-image can be used to create consistent
+animations of the given object.
+
+### Parameters:
+
+- **cols** (required): Number of columns in the output image (INT, default: 4)
+- **rows** (required): Number of rows in the output image (INT, default: 4)
+- **orientation** (optional): Orientation of the model ("Front", "Back", "Left", "Right", "Rotate")
+- **model** (optional): A 3D model (MODEL_3D)
+
+### Functionality:
+
+The node generates an image with multiple frames of an animation based on the given 3D model and specified parameters.
+
+## Example GIFs
+
+Here are some example GIFs demonstrating the functionality of these nodes:
+
+
+
+
+
+
## Installation
-1. Clone this repository into your ComfyUI extensions folder:
-2. Restart ComfyUI to load the extension.
+1. Copy the node files into your ComfyUI custom_nodes directory.
+2. Ensure you have a `3d_models` folder in your ComfyUI main directory.
+3. Place your 3D models (.fbx) in the `3d_models` folder.
## Usage
-The CharacterPipe node can be used in your ComfyUI workflow. It accepts various inputs and returns a consolidated CharacterPipe structure as well as individual components.
+After installation, you can use the nodes in your ComfyUI workflow. Connect the nodes according to your requirements and experiment with different settings to achieve desired results.
-### Inputs
+## Dependencies
-- `character_pipe`: An existing CharacterPipe structure (optional)
-- `face_image`: A face image
-- `pose_image`: A pose image
-- `add_positive_master_prompt`: Positive master prompt to add
-- `add_negative_master_prompt`: Negative master prompt to add
-- `add_positive_char_prompt`: Positive character prompt to add
-- `add_negative_char_prompt`: Negative character prompt to add
-- `model`: A model
-- `clip`: A CLIP model
-- `vae`: A VAE model
-- `ipadapter`: An IPAdapter model
+This package relies on the following Python libraries:
+- torch
+- numpy
+- PIL (Python Imaging Library)
-### Outputs
-
-- `character_pipe`: The updated CharacterPipe structure
-- Individual components (images, prompts, models)
-
-## License
-
-MIT
\ No newline at end of file
+Make sure these dependencies are installed in your ComfyUI environment.
diff --git a/__init__.py b/__init__.py
index 9d84aa6..227b7b8 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1 +1,50 @@
-from .nodes import *
+import os
+import shutil
+import sys
+import os
+import filecmp
+import shutil
+
+import __main__
+
+from distutils.dir_util import copy_tree
+from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
+
+WEB_DIRECTORY = "./web"
+
+__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
+# Get the current script's directory
+current_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Define paths
+folder_web = os.path.join(current_dir, "web")
+extensions_folder = os.path.join(current_dir,"ComfyUI", "custom_nodes", 'ComfyUI_BenriPack','web')
+folder_3d_models = os.path.join(current_dir, "ComfyUI", "3d_models")
+destination_3d_models = os.path.join(extensions_folder, "3d_models")
+
+def cleanup():
+ if os.path.exists(extensions_folder):
+ try:
+ shutil.rmtree(extensions_folder)
+ print('\033[92mBenriPack: Removed old extension folder\033[0m')
+ except Exception as e:
+ print(f'\033[91mBenriPack: Error removing old extension folder: {e}\033[0m')
+
+def copy_3d_models():
+ try:
+ if not os.path.exists(folder_3d_models):
+ print(f'\033[93mBenriPack: 3d_models folder not found at {folder_3d_models}\033[0m')
+ return
+
+ if not os.path.exists(extensions_folder):
+ os.makedirs(extensions_folder)
+
+
+ shutil.copytree(folder_3d_models, destination_3d_models, dirs_exist_ok=True)
+ print('\033[92mBenriPack: Copied 3d_models to extension folder\033[0m')
+ except Exception as e:
+ print(f'\033[91mBenriPack: Error copying 3d_models: {e}\033[0m')
+
+if __name__ == "__main__":
+ cleanup()
+ copy_3d_models()
\ No newline at end of file
diff --git a/example/example_workflow.json b/example/example_workflow.json
new file mode 100644
index 0000000..16a5024
--- /dev/null
+++ b/example/example_workflow.json
@@ -0,0 +1,161 @@
+{
+ "last_node_id": 76,
+ "last_link_id": 181,
+ "nodes": [
+ {
+ "id": 74,
+ "type": "Load3DModel",
+ "pos": {
+ "0": 538.924560546875,
+ "1": 886.9126586914062,
+ "2": 0,
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 0,
+ "7": 0,
+ "8": 0,
+ "9": 0
+ },
+ "size": {
+ "0": 315,
+ "1": 58
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "3d model",
+ "type": "MODEL_3D",
+ "links": [
+ 180
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "Load3DModel"
+ },
+ "widgets_values": [
+ "Walk.fbx"
+ ]
+ },
+ {
+ "id": 75,
+ "type": "AnimationExtractor",
+ "pos": {
+ "0": 998,
+ "1": 768,
+ "2": 0,
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 0,
+ "7": 0,
+ "8": 0,
+ "9": 0
+ },
+ "size": [
+ 464.94115517539467,
+ 713.950513559133
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "MODEL_3D",
+ "link": 180
+ }
+ ],
+ "outputs": [
+ {
+ "name": "animation",
+ "type": "IMAGE",
+ "links": [
+ 181
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {},
+ "widgets_values": [
+ 4,
+ 4,
+ "Right",
+ null
+ ]
+ },
+ {
+ "id": 76,
+ "type": "SaveImage",
+ "pos": {
+ "0": 1677.9161376953125,
+ "1": 874.073486328125,
+ "2": 0,
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 0,
+ "7": 0,
+ "8": 0,
+ "9": 0
+ },
+ "size": [
+ 369.8974890511033,
+ 607.9224278894808
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 181
+ }
+ ],
+ "outputs": [],
+ "properties": {},
+ "widgets_values": [
+ "ComfyUI"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 180,
+ 74,
+ 0,
+ 75,
+ 0,
+ "MODEL_3D"
+ ],
+ [
+ 181,
+ 75,
+ 0,
+ 76,
+ 0,
+ "IMAGE"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "ds": {
+ "scale": 0.740024994425886,
+ "offset": [
+ -152.98264049451026,
+ -637.7301452204401
+ ]
+ },
+ "groupNodes": {}
+ },
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/img/jump.gif b/img/jump.gif
new file mode 100644
index 0000000..d749555
Binary files /dev/null and b/img/jump.gif differ
diff --git a/img/walk.gif b/img/walk.gif
new file mode 100644
index 0000000..3517eab
Binary files /dev/null and b/img/walk.gif differ
diff --git a/img/workflow.png b/img/workflow.png
new file mode 100644
index 0000000..1ea4f54
Binary files /dev/null and b/img/workflow.png differ
diff --git a/nodes.py b/nodes.py
index ee9eb8a..9e0ca1f 100644
--- a/nodes.py
+++ b/nodes.py
@@ -1,6 +1,23 @@
import torch
import numpy as np
import comfy.model_management as model_management
+import folder_paths
+from PIL import Image, ImageOps
+import os
+from server import PromptServer
+import shutil
+import __main__
+
+class Model3D:
+ def __init__(self, model_path):
+ self.model_path = model_path
+
+ def to_dict(self):
+ return {
+ "class_type": "Model3D",
+ "model_path": self.model_path
+ }
+
class CharacterPipe:
def __init__(self, face_image=None, pose_image=None, positive_master_prompt=None, negative_master_prompt=None, positive_char_prompt=None, negative_char_prompt=None, model=None, clip=None, vae=None, ipadapter=None):
@@ -122,11 +139,98 @@ class CharacterPipeNode:
out.append(n)
return out
+
+
+class AnimationExtractor:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "cols": ("INT", {"default": 4, "min": 1, "max": 20, "step": 1}),
+ "rows": ("INT", {"default": 4, "min": 1, "max": 20, "step": 1}),
+ },
+ "optional": {
+ "orientation": (["Front", "Back", "Left", "Right", "Rotate"],),
+ "model": ("MODEL_3D",),
+ },
+ "hidden": {
+ "unique_id": "UNIQUE_ID",
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE",)
+ RETURN_NAMES = ("animation",)
+
+ @classmethod
+ def IS_CHANGED(s):
+ # This method tells ComfyUI to always consider the node as changed for certain inputs
+ return float("NaN")
+
+ FUNCTION = "extract_animation"
+
+
+ def extract_animation(self, cols=4, rows=4, orientation="Front", model=None, unique_id=None):
+ temp_dir = folder_paths.get_temp_directory()
+
+ image_path = os.path.join(temp_dir, "animation_"+unique_id+".png")
+ i = Image.open(image_path)
+ poseImage = i.convert("RGB")
+ poseImage = np.array(poseImage).astype(np.float32) / 255.0
+ poseImage = torch.from_numpy(poseImage)[None,]
+ return (poseImage, poseImage)
+
+
+
+folder_paths.add_model_folder_path("3d_models","./ComfyUI/3d_models")
+
+def copy_files():
+ files = folder_paths.get_filename_list("3d_models")
+ if not os.path.exists("./ComfyUI/custom_nodes/ComfyUI-Benripack/web/3d_models"):
+ os.makedirs("./ComfyUI/custom_nodes/ComfyUI-Benripack/web/3d_models")
+
+ if len(files) > 0:
+ for file in files:
+ shutil.copy( os.path.join("./ComfyUI/3d_models/", file), "./ComfyUI/custom_nodes/ComfyUI-Benripack/web/3d_models")
+
+
+class Load3DModel:
+ @classmethod
+ def INPUT_TYPES(s):
+ copy_files()
+ return {
+ "required": {
+ "model_name": (folder_paths.get_filename_list("3d_models"), ),
+ },
+ "optional": {
+ }
+ }
+
+ RETURN_TYPES = ("MODEL_3D",)
+ RETURN_NAMES = ("3d model",)
+ FUNCTION = "load_model"
+
+ def load_model(self, model_name):
+ model_path = folder_paths.get_full_path("3d_models", model_name)
+ model = Model3D(model_path)
+
+ return (model,)
+
+ @classmethod
+ def IS_CHANGED(s, model_name):
+ copy_files()
+ # This method tells ComfyUI to always consider the node as changed for certain inputs
+ return float("NaN")
NODE_CLASS_MAPPINGS = {
- "CharacterPipe": CharacterPipeNode
+ "CharacterPipe": CharacterPipeNode,
+ "Load3DModel": Load3DModel,
+ "AnimationExtractor": AnimationExtractor
}
NODE_DISPLAY_NAME_MAPPINGS = {
- "CharacterPipe": "Character Pipe"
-}
\ No newline at end of file
+ "CharacterPipe": "Benri Character Pipe",
+ "Load3DModel": "Benri Load 3D Model",
+ "AnimationExtractor": "Benri Animation Extractor"
+}
+
+WEB_DIRECTORY = "./web"
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..10513f0
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+torch
+numpy
+Pillow
\ No newline at end of file
diff --git a/web/editor.html b/web/editor.html
new file mode 100644
index 0000000..d8640a7
--- /dev/null
+++ b/web/editor.html
@@ -0,0 +1,13 @@
+
+
+
+ FBX Viewer
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/editor.js b/web/editor.js
new file mode 100644
index 0000000..7c01dbe
--- /dev/null
+++ b/web/editor.js
@@ -0,0 +1,256 @@
+class Editor {
+ constructor({ orientation, model, rows, cols,id }) {
+ this.id = id;
+ this.orientation = orientation;
+ this.model = model;
+ this.rows = rows;
+ this.cols = cols;
+ this.isInitialCapturing = false;
+ this.capturedFrames = 0;
+ this.frames = [];
+ this.isCapturing = false;
+ this.captureCount = 0;
+
+ this.scene = null;
+ this.camera = null;
+ this.renderer = null;
+ this.mixer = null;
+ this.clock = null;
+ this.gModel = null;
+ }
+
+ async init() {
+ this.scene = new THREE.Scene();
+ this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.5, 1000);
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
+ this.renderer.domElement.id = 'editor'+this.id;
+ this.renderer.setClearColor(0x0000ff);
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
+ this.renderer.shadowMap.enabled = true;
+ this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
+ document.getElementById('container').appendChild(this.renderer.domElement);
+
+ const ambientLight = new THREE.AmbientLight(0xeeeeee, 0.5);
+ this.scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
+ directionalLight.position.set(5, 30, 57.5);
+ directionalLight.castShadow = true;
+ directionalLight.shadow.mapSize.width = 2048;
+ directionalLight.shadow.mapSize.height = 2048;
+ this.scene.add(directionalLight);
+
+ this.clock = new THREE.Clock();
+ this.gModel = await this.loadModel().catch(console.error);
+ if (this.gModel) this.captureInitialFrames();
+ }
+
+ async update({ orientation, model, rows, cols }) {
+ if (orientation && orientation !== this.orientation) {
+ this.orientation = orientation;
+ this.updateOrientation(this.gModel);
+ this.captureInitialFrames();
+ }
+
+ if (model && model !== this.model) {
+ this.model = model;
+ this.gModel = await this.loadModel();
+ this.captureInitialFrames();
+ }
+
+ if (rows && rows !== this.rows) {
+ this.rows = rows;
+ this.captureInitialFrames();
+ }
+
+ if (cols && cols !== this.cols) {
+ this.cols = cols;
+ this.captureInitialFrames();
+ }
+ }
+
+ loadModel() {
+ return new Promise((resolve, reject) => {
+ const loader = new THREE.FBXLoader();
+ if (!this.model) {
+ reject('No model selected');
+ return;
+ }
+
+ loader.load('/extensions/ComfyUI-Benripack/3d_models/'+this.model, (object) => {
+ let box = new THREE.Box3().setFromObject(object);
+ let size = box.getSize(new THREE.Vector3());
+ const maxAxis = Math.max(size.x, size.y, size.z);
+ object.scale.multiplyScalar(100.0 / maxAxis);
+
+ object.traverse((child) => {
+ if (child.isMesh && !child.name.includes('Joints')) {
+ child.castShadow = true;
+ child.receiveShadow = true;
+ // set color
+ //const material = new THREE.MeshPhongMaterial({ color: 0xaaaaaa });
+ //child.material.color = material.color;
+ }
+ });
+
+ if (this.gModel) this.scene.remove(this.gModel);
+ this.updateOrientation(object);
+ this.scene.add(object);
+ box.getCenter(object.position).multiplyScalar(-1);
+ object.position.y = 0;
+
+ if (object.animations.length > 0) {
+ this.mixer = new THREE.AnimationMixer(object);
+ const action = this.mixer.clipAction(object.animations[0]);
+ action.play();
+ }
+ box = new THREE.Box3().setFromObject(object);
+ size = box.getSize(new THREE.Vector3());
+ this.camera.position.set(0, size.y/2 + 20, 120);
+ this.camera.lookAt(new THREE.Vector3(0, size.y/2, 0));
+ resolve(object);
+ });
+ });
+ }
+
+ updateOrientation(object) {
+ switch (this.orientation) {
+ case 'Front': object.rotation.y = 0; break;
+ case 'Rotate': object.rotation.y = 0; break;
+ case 'Back': object.rotation.y = Math.PI; break;
+ case 'Left': object.rotation.y = -Math.PI / 2; break;
+ case 'Right': object.rotation.y = Math.PI / 2; break;
+ }
+ }
+
+ captureInitialFrames() {
+ this.isInitialCapturing = true;
+ this.capturedFrames = 0;
+ this.frames = [];
+ const captureNextFrame = () => {
+ if (this.capturedFrames < this.rows * this.cols) {
+ if (this.mixer) {
+ var t = this.mixer._actions[0]._clip.duration;
+ this.mixer.setTime(t * (this.capturedFrames / (this.rows * this.cols)));
+ }
+ if(this.orientation == "Rotate") this.gModel.rotation.y = 2 * Math.PI * (this.capturedFrames / (this.rows * this.cols));
+ this.renderer.render(this.scene, this.camera);
+ this.captureFrame();
+ this.capturedFrames++;
+ requestAnimationFrame(captureNextFrame);
+ } else {
+ this.isInitialCapturing = false;
+ if (this.mixer) this.mixer.setTime(0);
+ requestAnimationFrame(this.animate.bind(this));
+ }
+ };
+ setTimeout(() => {
+ captureNextFrame();
+ }, 2000);
+ }
+
+ async captureFrame() {
+ this.renderer.render(this.scene, this.camera);
+ this.frames.push(this.renderer.domElement.toDataURL());
+ this.captureCount++;
+ if (this.captureCount >= (this.rows * this.cols)) {
+ this.isCapturing = false;
+ console.log('Captured frames', this.frames);
+ var animation = await this.createAnimationImage(this.rows, this.cols, this.frames);
+ window.parent.postMessage({ type: 'capture', frames: this.frames, animation: animation, id: this.id }, '*');
+ }
+ }
+
+ animate() {
+ requestAnimationFrame(this.animate.bind(this));
+ if (!this.isInitialCapturing) {
+ if(this.mixer) this.mixer.update(this.clock.getDelta());
+ if(this.orientation == "Rotate"){
+ this.gModel.rotation.y += 0.01
+ this.gModel.rotation.y %= 2 * Math.PI
+ }
+ }
+ this.renderer.render(this.scene, this.camera);
+ }
+
+ startCapture() {
+ this.frames = [];
+ this.isCapturing = true;
+ this.captureCount = 0;
+ }
+
+ createAnimationImage(rows, cols, frames) {
+
+ return new Promise((resolve, reject) => {
+ if (frames.length === 0) {
+ reject('No frames to create animation image');
+ return;
+ }
+
+
+ // Create a temporary image to get dimensions
+ const tempImg = new Image();
+ tempImg.onload = () => {
+
+ const frameWidth = tempImg.width;
+ const frameHeight = tempImg.height;
+
+
+ // Create a canvas that will hold all frames in a grid
+ this.canvas = document.createElement('canvas');
+ this.canvas.width = frameWidth * cols;
+ this.canvas.height = frameHeight * rows;
+
+ const ctx = this.canvas.getContext('2d');
+
+ // Function to draw each frame
+ const drawFrame = (index) => {
+ if (index >= frames.length || index >= rows * cols) {
+ // All frames have been drawn or grid is full, resolve with the result
+ console.log("Länge: " + this.canvas.toDataURL().length,this.id)
+ resolve(this.canvas.toDataURL());
+ return;
+ }
+
+ const img = new Image();
+ img.onload = () => {
+ const col = index % cols;
+ const row = Math.floor(index / cols);
+ ctx.drawImage(img, col * frameWidth, row * frameHeight, frameWidth, frameHeight);
+ drawFrame(index + 1);
+ };
+ img.onerror = reject;
+ img.src = frames[index];
+ };
+
+ // Start drawing frames
+ drawFrame(0);
+ };
+ tempImg.onerror = reject;
+ tempImg.src = frames[0];
+ });
+ }
+
+ onWindowResize() {
+ this.camera.aspect = window.innerWidth / window.innerHeight;
+ this.camera.updateProjectionMatrix();
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
+ }
+ }
+
+ // Event listeners
+ function onMessage(event) {
+ if(!window.editor) window.editor = {};
+ if (!event.data.orientation) return;
+ if (window.editor && window.editor[event.data.id]) {
+ window.editor[event.data.id].update(event.data);
+ } else {
+ window.editor[event.data.id] = new Editor(event.data);
+ window.editor[event.data.id].init();
+ }
+ }
+
+ window.addEventListener('resize', () => {
+ for (const key in window.editor) window.editor[key].onWindowResize();
+ })
+ window.addEventListener('message', onMessage);
\ No newline at end of file
diff --git a/web/main.js b/web/main.js
new file mode 100644
index 0000000..7cf87c9
--- /dev/null
+++ b/web/main.js
@@ -0,0 +1,180 @@
+import { app } from "/scripts/app.js"
+
+class AnimationExtractor{
+ orientation;
+ model;
+ container;
+ frames = [];
+ animation;
+ constructor(data) {
+ this.orientation = data.orientation;
+ this.model = data.model;
+ this.container = data.container;
+ this.iframe = this.container.children[0];
+ this.id = data.id;
+ window.addEventListener('message', this.onMessage.bind(this));
+ }
+ onMessage(event) {
+ if(event.data.type === "capture" && event.data.id === this.id) {
+ this.frames = event.data.frames;
+ this.animation = event.data.animation;
+ this.uploadData();
+ }
+ }
+ update(data) {
+ this.orientation = data.orientation;
+ this.model = data.model;
+ data.id = this.id;
+ this.iframe?.contentWindow?.postMessage(data, "*");
+ }
+
+ async uploadData() {
+ for(let i = 0; i < this.frames.length; i++){
+ const blobData = await fetch(this.frames[i]).then(r => r.blob())
+ const filename = `frame_${i}.png`
+
+ let formData = new FormData()
+ formData.append('image', blobData, filename)
+ formData.append('overwrite', 'true')
+ formData.append('type', 'temp')
+ formData.append('subFolder', 'AnimationExtractor')
+ const resp = await fetch('/upload/image', {
+ method: 'POST',
+ body: formData,
+ })
+ if (resp.status === 200) {
+ const data = await resp.json()
+
+ console.log("[AnimationExtractor] Upload image success.", data.name)
+ }
+ }
+ if(this.animation) {
+ const blobData = await fetch(this.animation).then(r => r.blob())
+ const filename = `animation_${this.id}.png`
+
+ let formData = new FormData()
+ formData.append('image', blobData, filename)
+ formData.append('overwrite', 'true')
+ formData.append('type', 'temp')
+ formData.append('subFolder', 'AnimationExtractor')
+ const resp = await fetch('/upload/image', {
+ method: 'POST',
+ body: formData,
+ })
+ if (resp.status === 200) {
+ const data = await resp.json()
+
+ console.log("[AnimationExtractor] Upload animation success.", data.name)
+ }
+ }
+ }
+}
+
+function createSimpleHtmlLoader(node, inputName, app) {
+ node.name = inputName;
+
+ const container = document.createElement('div');
+ container.id = `comfyui-${inputName.toLowerCase()}`;
+ container.style.position = 'absolute';
+ container.style.width = '100%';
+ container.style.height = '100%';
+ container.style.overflow = 'none';
+ const iframe = document.createElement('iframe');
+ iframe.src = '/extensions/ComfyUI-Benripack/editor.html';
+ iframe.style.width = '100%';
+ iframe.style.height = '100%';
+ iframe.style.border = 'none';
+
+ container.appendChild(iframe);
+ document.body.appendChild(container);
+
+ node.onRemoved = () => {
+ container.remove();
+ };
+
+ function findModelName(linkId) {
+ if (!linkId) return ""
+ var graph = app.graph;
+ var originalNodeId = app.graph.links.find(x => x?.id == linkId).origin_id
+ var modelName = app.graph.getNodeById(originalNodeId).widgets[0].value
+ return modelName
+ }
+
+ var anim = new AnimationExtractor({
+ id: node.id,
+ orientation: node.widgets.find(x => x.name === "orientation").value,
+ model: findModelName(node.inputs.find(x => x.name === "model").link),
+ rows: node.widgets.find(x => x.name === "rows").value,
+ cols: node.widgets.find(x => x.name === "cols").value,
+ container: container
+ })
+ let widget = {
+ type: "AnimationExtractor",
+ name: `AnimationExtractor${inputName}`,
+ AnimationExtractor:anim,
+ draw: function (ctx, _, widgetWidth, y, widgetHeight) {
+ const margin = 20
+ const top_offset = 20
+ const visible = app.canvas.ds.scale > 0.6 && this.type === "AnimationExtractor"
+ const w = _.size[0] - margin * 4
+ const h = _.size[1] - margin * 4
+ const clientRectBound = ctx.canvas.getBoundingClientRect()
+ const transform = new DOMMatrix()
+ .scaleSelf(
+ clientRectBound.width / ctx.canvas.width,
+ clientRectBound.height / ctx.canvas.height
+ )
+ .multiplySelf(ctx.getTransform())
+ .translateSelf(margin, margin + y)
+
+ Object.assign(this.AnimationExtractor.container.style, {
+ left: `${transform.a * margin + transform.e}px`,
+ top: `${transform.d + transform.f + transform.d *margin}px`,
+ width: `${(w * transform.a) }px`,
+ height: `${((h * transform.d) + (- widgetHeight-100)*transform.d)}px`,
+ position: "absolute",
+ overflow: "hidden",
+ zIndex: app.graph._nodes.indexOf(node),
+ })
+
+ Object.assign(this.AnimationExtractor.container.children[0].style, {
+ transformOrigin: "50% 50%",
+ width: '100%',
+ height: '100%',
+ border: '0 none',
+ })
+
+ this.AnimationExtractor.container.hidden = !visible
+
+
+ this.AnimationExtractor.update({
+ orientation: node.widgets.find(x => x.name === "orientation").value,
+ model: findModelName(node.inputs.find(x => x.name === "model").link),
+ rows: node.widgets.find(x => x.name === "rows").value,
+ cols: node.widgets.find(x => x.name === "cols").value,
+ })
+ },
+ parent: node
+ }
+ node.addCustomWidget(widget)
+ return {
+ widget: widget
+ };
+}
+
+app.registerExtension({
+ name: "AnimationExtractor",
+ async init(app) {
+ console.log("AnimationExtractor initialized");
+ },
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "AnimationExtractor") {
+ nodeType.prototype.onNodeCreated = function() {
+ setTimeout(() => {
+ createSimpleHtmlLoader(this, "TestNode", app);
+ }, 1000);
+
+ };
+ }
+ }
+});
\ No newline at end of file