├── README.md ├── __init__.py ├── p5jsimage.py └── web ├── js └── p5jsimage.js └── preview └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # comfyui-p5js-node 2 | Custom node for ComfyUI to run p5js 3 | 4 | ## What this is 5 | 6 | A simple proof of concept node to pass a p5js canvas through ComfyUI for img2img generation use. 7 | 8 | ## What this isn't 9 | 10 | A full blown p5js editor. That already exists. There's no debugging or error checking here. This node expects a working sketch to be pasted in the input to render to a canvas. 11 | 12 | ## How it works 13 | 14 | * Paste your sketch in the text box and press the "*Run Sketch*" button. 15 | * The sketch is saved to the temp folder in a subdirectory, named `p5js/sketch.js` 16 | * The iframe gets refreshed with this sketch. 17 | * Pressing "*Queue Prompt*" will trigger the node to pass control to the JS to query for the canvas obect in the iframe and then return it back for * processing to an image. The image then gets passed along the pipeline. 18 | 19 | ## Installation and tips 20 | 21 | Clone the repository into your ComfyUI custom_nodes directory. You can clone the repository with the command: 22 | 23 | ``` 24 | git clone https://github.com/tracerstar/comfyui-p5js-node.git 25 | ``` 26 | 27 | When writing your p5js sketch, make sure you use the basic method of creating a canvas in your setup method. Right now the JS will only grab the canvas object by the default ID p5js adds (`defaultCanvas0`). This can be improved later on. 28 | 29 | ``` 30 | function setup() { 31 | createCanvas(512,512); 32 | } 33 | ``` 34 | 35 | ## What's next 36 | 37 | * Some UI improvements (maybe allowing sketches to save and later be picked from a dropdown like the loadImage node) 38 | * Looking into the feasibility of animation / batch iamges 39 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .p5jsimage import HYPE_P5JSImage 2 | 3 | NODE_CLASS_MAPPINGS = { 4 | "HYPE_P5JSImage": HYPE_P5JSImage 5 | } 6 | 7 | NODE_DISPLAY_NAME_MAPPINGS = { 8 | "HYPE_P5JSImage": "p5js image" 9 | } 10 | 11 | WEB_DIRECTORY = "./web" 12 | 13 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"] 14 | -------------------------------------------------------------------------------- /p5jsimage.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import nodes 3 | import folder_paths 4 | import os 5 | import time 6 | import torch 7 | import torchvision.transforms as transforms 8 | from aiohttp import web 9 | from io import BytesIO 10 | from PIL import Image 11 | from server import PromptServer 12 | 13 | # handle proxy response 14 | @PromptServer.instance.routes.post('/HYPE/proxy_reply') 15 | async def proxyHandle(request): 16 | post = await request.json() 17 | MessageHolder.addMessage(post["node_id"], post["outputs"]) 18 | return web.json_response({"status": "ok"}) 19 | 20 | class HYPE_P5JSImage(nodes.LoadImage): 21 | 22 | @classmethod 23 | def INPUT_TYPES(s): 24 | return { 25 | "required": { 26 | "script": ("STRING", {"default": "function setup() {\n createCanvas(512, 512);\n}\n\nfunction draw() {\n background(220);\n}", "multiline": True, "dynamicPrompts": False}), 27 | "image": ("P5JS", {}), 28 | }, 29 | } 30 | 31 | def IS_CHANGED(id): 32 | return True 33 | 34 | RETURN_TYPES = ("IMAGE",) 35 | RETURN_NAMES = ("image",) 36 | 37 | FUNCTION = "run" 38 | 39 | #OUTPUT_NODE = False 40 | 41 | CATEGORY = "p5js" 42 | 43 | def run(s, script, image, **kwargs): 44 | return super().load_image(folder_paths.get_annotated_filepath(image)) 45 | 46 | # Message Handling 47 | class MessageHolder: 48 | messages = {} 49 | 50 | @classmethod 51 | def addMessage(self, id, message): 52 | self.messages[str(id)] = message 53 | 54 | @classmethod 55 | def waitForMessage(self, id, period = 0.1): 56 | sid = str(id) 57 | while not (sid in self.messages): 58 | time.sleep(period) 59 | message = self.messages.pop(str(id),None) 60 | return message 61 | -------------------------------------------------------------------------------- /web/js/p5jsimage.js: -------------------------------------------------------------------------------- 1 | import { app } from "/scripts/app.js"; 2 | import { api } from "/scripts/api.js"; 3 | import { $el } from "/scripts/ui.js"; 4 | 5 | const p5jsPreviewSrc = new URL(`../preview/index.html`, import.meta.url); 6 | 7 | async function saveSketch(filename, srcCode) { 8 | try { 9 | const blob = new Blob([srcCode], {type: 'text/plain'}); 10 | const file = new File([blob], filename+'.js'); 11 | const body = new FormData(); 12 | body.append("image", file); 13 | body.append("subfolder", "p5js"); 14 | body.append("type", "temp"); 15 | body.append("overwrite", "true");//can also be set to 1 16 | const resp = await api.fetchApi("/upload/image", { 17 | method: "POST", 18 | body, 19 | }); 20 | if (resp.status !== 200) { 21 | const err = `Error uploading sketch: ${resp.status} - ${resp.statusText}`; 22 | alert(err); 23 | throw new Error(err); 24 | } 25 | 26 | return resp; 27 | } catch (e) { 28 | console.log(`Error sending sketch file for saving: ${e}`); 29 | } 30 | }//end saveSketch 31 | 32 | 33 | app.registerExtension({ 34 | 35 | name: "HYPE_P5JSImage", 36 | 37 | getCustomWidgets(app) { 38 | return { 39 | P5JS(node, inputName) { 40 | 41 | const d = new Date(); 42 | const base_filename = d.getUTCFullYear() + "_" + (d.getUTCMonth()+1) + "_" + d.getUTCDate() + '_'; 43 | 44 | const widget = { 45 | type: "P5JS", 46 | name: "image", 47 | size: [512,120], 48 | sketchfile: base_filename + Math.floor(Math.random() * 10000), //unique filename for each widget, don't love this... maybe make it a millis based time stamp? 49 | iframe: $el("iframe", { width:400, height:400, src:p5jsPreviewSrc }), 50 | 51 | draw(ctx, node, widget_width, y, widget_height) { 52 | const margin = 10, 53 | left_offset = 0, 54 | top_offset = 0, 55 | visible = app.canvas.ds.scale > 0.6 && this.type === "p5js_widget", 56 | w = widget_width - margin * 2, 57 | clientRectBound = ctx.canvas.getBoundingClientRect(), 58 | transform = new DOMMatrix().scaleSelf(clientRectBound.width / ctx.canvas.width, clientRectBound.height / ctx.canvas.height).multiplySelf(ctx.getTransform()).translateSelf(margin, margin + y), 59 | scale = new DOMMatrix().scaleSelf(transform.a, transform.d); 60 | 61 | Object.assign(this.iframe.style, { 62 | left: `${transform.a * margin * left_offset + transform.e}px`, 63 | top: `${transform.d + transform.f + top_offset}px`, 64 | width: `${w * transform.a}px`, 65 | height: `${w * transform.d}px`, 66 | position: "absolute", 67 | border: '1px solid #000', 68 | padding: 0, 69 | margin: 0, 70 | zIndex: app.graph._nodes.indexOf(node), 71 | }); 72 | }, 73 | 74 | computeSize(width) { 75 | return [512,512]; 76 | }, 77 | }; 78 | 79 | node.onRemoved = function () { 80 | node.widgets[0].inputEl.remove(); 81 | widget.iframe.remove(); 82 | }; 83 | 84 | node.serialize_widgets = false; 85 | 86 | //add run sketch 87 | const btn = node.addWidget("button", "Run Sketch", "run_p5js_sketch", () => { 88 | saveSketch(widget.sketchfile, node.widgets[0].value).then((response) => { 89 | widget.iframe.src = p5jsPreviewSrc + "?sketch=" + widget.sketchfile+'.js'; 90 | }); 91 | }); 92 | btn.serializeValue = () => undefined; 93 | 94 | return node.addCustomWidget(widget); 95 | }, 96 | }; 97 | }, 98 | 99 | nodeCreated(node) { 100 | if ((node.type, node.constructor.comfyClass !== "HYPE_P5JSImage")) return; 101 | 102 | //get the p5js widget 103 | const p5jsWidget = node.widgets.find((w) => w.name === "image"); 104 | 105 | //add serialize method here.... 106 | p5jsWidget.serializeValue = async () => { 107 | //get the canvas from iframe 108 | var theFrame = p5jsWidget.iframe; 109 | var iframe_doc = theFrame.contentDocument || theFrame.contentWindow.document; 110 | var canvas = iframe_doc.getElementById("defaultCanvas0");//TODO: maybe change this to pull all canvas elements and return the first one created 111 | 112 | const blob = await new Promise((r) => canvas.toBlob(r)); 113 | const name = `${+new Date()}.png`; 114 | const file = new File([blob], name); 115 | const body = new FormData(); 116 | body.append("image", file); 117 | body.append("subfolder", "p5js"); 118 | body.append("type", "temp"); 119 | const resp = await api.fetchApi("/upload/image", { 120 | method: "POST", 121 | body, 122 | }); 123 | if (resp.status !== 200) { 124 | const err = `Error uploading image: ${resp.status} - ${resp.statusText}`; 125 | alert(err); 126 | throw new Error(err); 127 | } 128 | return `p5js/${name} [temp]`; 129 | } 130 | 131 | //add the iframe to the bottom of the node 132 | document.body.appendChild(p5jsWidget.iframe); 133 | 134 | }, 135 | }); 136 | -------------------------------------------------------------------------------- /web/preview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 10 | 11 | 12 | 30 | 31 | 32 | --------------------------------------------------------------------------------