commit 05b36d01bd216e08b9a484ddf2cb169aa09f651d Author: Nathan Date: Sun Mar 3 20:50:07 2024 -0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3f79c7c --- /dev/null +++ b/__init__.py @@ -0,0 +1,71 @@ + + +class Example: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". + Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. + The type can be a list for selection. + + Returns: `dict`: + - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` + - Value input_fields (`dict`): Contains input fields config: + * Key field_name (`string`): Name of a entry-point method's argument + * Value field_config (`tuple`): + + First value is a string indicate the type of field or a list for selection. + + Secound value is a config for type "INT", "STRING" or "FLOAT". + """ + return { + "required": { + }, + } + + RETURN_TYPES = ("IMAGE",) + #RETURN_NAMES = ("image_output_name",) + + FUNCTION = "test" + + #OUTPUT_NODE = False + + CATEGORY = "TEST" + + def test(self, image, string_field, int_field, float_field, print_to_screen): + if print_to_screen == "enable": + print(f"""Your input contains: + string_field aka input text: {string_field} + int_field: {int_field} + float_field: {float_field} + """) + #do some processing on the image, in this example I just invert it + image = 1.0 - image + return (image,) + + """ + The node will always be re executed if any of the inputs change but + this method can be used to force the node to execute again even when the inputs don't change. + You can make this node return a number or a string. This value will be compared to the one returned the last time the node was + executed, if it is different the node will be executed again. + This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash + changes between executions the LoadImage node is executed again. + """ + #@classmethod + #def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): + # return "" + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "Example": Example +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "Example": "Example Node" +} + +WEB_DIRECTORY = "js" 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/gif.js b/js/gif.js new file mode 100644 index 0000000..19a513d --- /dev/null +++ b/js/gif.js @@ -0,0 +1,374 @@ + +export const GIF = function () { + var timerID; // timer handle for set time out usage + var st; // holds the stream object when loading. + var interlaceOffsets = [0, 4, 2, 1]; // used in de-interlacing. + var interlaceSteps = [8, 8, 4, 2]; + var interlacedBufSize; // this holds a buffer to de interlace. Created on the first frame and when size changed + var deinterlaceBuf; + var pixelBufSize; // this holds a buffer for pixels. Created on the first frame and when size changed + var pixelBuf; + const GIF_FILE = { // gif file data headers + GCExt : 0xF9, + COMMENT : 0xFE, + APPExt : 0xFF, + UNKNOWN : 0x01, // not sure what this is but need to skip it in parser + IMAGE : 0x2C, + EOF : 59, // This is entered as decimal + EXT : 0x21, + }; + // simple buffered stream used to read from the file + var Stream = function (data) { + this.data = new Uint8ClampedArray(data); + this.pos = 0; + var len = this.data.length; + this.getString = function (count) { // returns a string from current pos of len count + var s = ""; + while (count--) { s += String.fromCharCode(this.data[this.pos++]) } + return s; + }; + this.readSubBlocks = function () { // reads a set of blocks as a string + var size, count, data = ""; + do { + count = size = this.data[this.pos++]; + while (count--) { data += String.fromCharCode(this.data[this.pos++]) } + } while (size !== 0 && this.pos < len); + return data; + } + this.readSubBlocksB = function () { // reads a set of blocks as binary + var size, count, data = []; + do { + count = size = this.data[this.pos++]; + while (count--) { data.push(this.data[this.pos++]);} + } while (size !== 0 && this.pos < len); + return data; + } + }; + // LZW decoder uncompressed each frames pixels + // this needs to be optimised. + // minSize is the min dictionary as powers of two + // size and data is the compressed pixels + function lzwDecode(minSize, data) { + var i, pixelPos, pos, clear, eod, size, done, dic, code, last, d, len; + pos = pixelPos = 0; + dic = []; + clear = 1 << minSize; + eod = clear + 1; + size = minSize + 1; + done = false; + while (!done) { // JavaScript optimisers like a clear exit though I never use 'done' apart from fooling the optimiser + last = code; + code = 0; + for (i = 0; i < size; i++) { + if (data[pos >> 3] & (1 << (pos & 7))) { code |= 1 << i } + pos++; + } + if (code === clear) { // clear and reset the dictionary + dic = []; + size = minSize + 1; + for (i = 0; i < clear; i++) { dic[i] = [i] } + dic[clear] = []; + dic[eod] = null; + } else { + if (code === eod) { done = true; return } + if (code >= dic.length) { dic.push(dic[last].concat(dic[last][0])) } + else if (last !== clear) { dic.push(dic[last].concat(dic[code][0])) } + d = dic[code]; + len = d.length; + for (i = 0; i < len; i++) { pixelBuf[pixelPos++] = d[i] } + if (dic.length === (1 << size) && size < 12) { size++ } + } + } + }; + function parseColourTable(count) { // get a colour table of length count Each entry is 3 bytes, for RGB. + var colours = []; + for (var i = 0; i < count; i++) { colours.push([st.data[st.pos++], st.data[st.pos++], st.data[st.pos++]]) } + return colours; + } + function parse (){ // read the header. This is the starting point of the decode and async calls parseBlock + var bitField; + st.pos += 6; + gif.width = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + gif.height = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + bitField = st.data[st.pos++]; + gif.colorRes = (bitField & 0b1110000) >> 4; + gif.globalColourCount = 1 << ((bitField & 0b111) + 1); + gif.bgColourIndex = st.data[st.pos++]; + st.pos++; // ignoring pixel aspect ratio. if not 0, aspectRatio = (pixelAspectRatio + 15) / 64 + if (bitField & 0b10000000) { gif.globalColourTable = parseColourTable(gif.globalColourCount) } // global colour flag + setTimeout(parseBlock, 0); + } + function parseAppExt() { // get application specific data. Netscape added iterations and terminator. Ignoring that + st.pos += 1; + if ('NETSCAPE' === st.getString(8)) { st.pos += 8 } // ignoring this data. iterations (word) and terminator (byte) + else { + st.pos += 3; // 3 bytes of string usually "2.0" when identifier is NETSCAPE + st.readSubBlocks(); // unknown app extension + } + }; + function parseGCExt() { // get GC data + var bitField; + st.pos++; + bitField = st.data[st.pos++]; + gif.disposalMethod = (bitField & 0b11100) >> 2; + gif.transparencyGiven = bitField & 0b1 ? true : false; // ignoring bit two that is marked as userInput??? + gif.delayTime = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + gif.transparencyIndex = st.data[st.pos++]; + st.pos++; + }; + function parseImg() { // decodes image data to create the indexed pixel image + var deinterlace, frame, bitField; + deinterlace = function (width) { // de interlace pixel data if needed + var lines, fromLine, pass, toline; + lines = pixelBufSize / width; + fromLine = 0; + if (interlacedBufSize !== pixelBufSize) { // create the buffer if size changed or undefined. + deinterlaceBuf = new Uint8Array(pixelBufSize); + interlacedBufSize = pixelBufSize; + } + for (pass = 0; pass < 4; pass++) { + for (toLine = interlaceOffsets[pass]; toLine < lines; toLine += interlaceSteps[pass]) { + deinterlaceBuf.set(pixelBuf.subarray(fromLine, fromLine + width), toLine * width); + fromLine += width; + } + } + }; + frame = {} + gif.frames.push(frame); + frame.disposalMethod = gif.disposalMethod; + frame.time = gif.length; + frame.delay = gif.delayTime * 10; + gif.length += frame.delay; + if (gif.transparencyGiven) { frame.transparencyIndex = gif.transparencyIndex } + else { frame.transparencyIndex = undefined } + frame.leftPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + frame.topPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + frame.width = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + frame.height = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8); + bitField = st.data[st.pos++]; + frame.localColourTableFlag = bitField & 0b10000000 ? true : false; + if (frame.localColourTableFlag) { frame.localColourTable = parseColourTable(1 << ((bitField & 0b111) + 1)) } + if (pixelBufSize !== frame.width * frame.height) { // create a pixel buffer if not yet created or if current frame size is different from previous + pixelBuf = new Uint8Array(frame.width * frame.height); + pixelBufSize = frame.width * frame.height; + } + lzwDecode(st.data[st.pos++], st.readSubBlocksB()); // decode the pixels + if (bitField & 0b1000000) { // de interlace if needed + frame.interlaced = true; + deinterlace(frame.width); + } else { frame.interlaced = false } + processFrame(frame); // convert to canvas image + }; + function processFrame(frame) { // creates a RGBA canvas image from the indexed pixel data. + var ct, cData, dat, pixCount, ind, useT, i, pixel, pDat, col, frame, ti; + frame.image = document.createElement('canvas'); + frame.image.width = gif.width; + frame.image.height = gif.height; + frame.image.ctx = frame.image.getContext("2d"); + ct = frame.localColourTableFlag ? frame.localColourTable : gif.globalColourTable; + if (gif.lastFrame === null) { gif.lastFrame = frame } + useT = (gif.lastFrame.disposalMethod === 2 || gif.lastFrame.disposalMethod === 3) ? true : false; + if (!useT) { frame.image.ctx.drawImage(gif.lastFrame.image, 0, 0, gif.width, gif.height) } + cData = frame.image.ctx.getImageData(frame.leftPos, frame.topPos, frame.width, frame.height); + ti = frame.transparencyIndex; + dat = cData.data; + if (frame.interlaced) { pDat = deinterlaceBuf } + else { pDat = pixelBuf } + pixCount = pDat.length; + ind = 0; + for (i = 0; i < pixCount; i++) { + pixel = pDat[i]; + col = ct[pixel]; + if (ti !== pixel) { + dat[ind++] = col[0]; + dat[ind++] = col[1]; + dat[ind++] = col[2]; + dat[ind++] = 255; // Opaque. + } else + if (useT) { + dat[ind + 3] = 0; // Transparent. + ind += 4; + } else { ind += 4 } + } + frame.image.ctx.putImageData(cData, frame.leftPos, frame.topPos); + gif.lastFrame = frame; + if (!gif.waitTillDone && typeof gif.onload === "function") { doOnloadEvent() }// if !waitTillDone the call onload now after first frame is loaded + }; + // **NOT** for commercial use. + function finnished() { // called when the load has completed + gif.loading = false; + gif.frameCount = gif.frames.length; + gif.lastFrame = null; + st = undefined; + gif.complete = true; + gif.disposalMethod = undefined; + gif.transparencyGiven = undefined; + gif.delayTime = undefined; + gif.transparencyIndex = undefined; + gif.waitTillDone = undefined; + pixelBuf = undefined; // dereference pixel buffer + deinterlaceBuf = undefined; // dereference interlace buff (may or may not be used); + pixelBufSize = undefined; + deinterlaceBuf = undefined; + gif.currentFrame = 0; + if (gif.frames.length > 0) { gif.image = gif.frames[0].image } + doOnloadEvent(); + if (typeof gif.onloadall === "function") { + (gif.onloadall.bind(gif))({ type : 'loadall', path : [gif] }); + } + if (gif.playOnLoad) { gif.play() } + } + function canceled () { // called if the load has been cancelled + finnished(); + if (typeof gif.cancelCallback === "function") { (gif.cancelCallback.bind(gif))({ type : 'canceled', path : [gif] }) } + } + function parseExt() { // parse extended blocks + const blockID = st.data[st.pos++]; + if(blockID === GIF_FILE.GCExt) { parseGCExt() } + else if(blockID === GIF_FILE.COMMENT) { gif.comment += st.readSubBlocks() } + else if(blockID === GIF_FILE.APPExt) { parseAppExt() } + else { + if(blockID === GIF_FILE.UNKNOWN) { st.pos += 13; } // skip unknow block + st.readSubBlocks(); + } + + } + function parseBlock() { // parsing the blocks + if (gif.cancel !== undefined && gif.cancel === true) { canceled(); return } + + const blockId = st.data[st.pos++]; + if(blockId === GIF_FILE.IMAGE ){ // image block + parseImg(); + if (gif.firstFrameOnly) { finnished(); return } + }else if(blockId === GIF_FILE.EOF) { finnished(); return } + else { parseExt() } + if (typeof gif.onprogress === "function") { + gif.onprogress({ bytesRead : st.pos, totalBytes : st.data.length, frame : gif.frames.length }); + } + setTimeout(parseBlock, 0); // parsing frame async so processes can get some time in. + }; + function cancelLoad(callback) { // cancels the loading. This will cancel the load before the next frame is decoded + if (gif.complete) { return false } + gif.cancelCallback = callback; + gif.cancel = true; + return true; + } + function error(type) { + if (typeof gif.onerror === "function") { (gif.onerror.bind(this))({ type : type, path : [this] }) } + gif.onload = gif.onerror = undefined; + gif.loading = false; + } + function doOnloadEvent() { // fire onload event if set + gif.currentFrame = 0; + gif.nextFrameAt = gif.lastFrameAt = new Date().valueOf(); // just sets the time now + if (typeof gif.onload === "function") { (gif.onload.bind(gif))({ type : 'load', path : [gif] }) } + gif.onerror = gif.onload = undefined; + } + function dataLoaded(data) { // Data loaded create stream and parse + st = new Stream(data); + parse(); + } + function loadGif(filename) { // starts the load + var ajax = new XMLHttpRequest(); + ajax.responseType = "arraybuffer"; + ajax.onload = function (e) { + if (e.target.status === 404) { error("File not found") } + else if(e.target.status >= 200 && e.target.status < 300 ) { dataLoaded(ajax.response) } + else { error("Loading error : " + e.target.status) } + }; + ajax.open('GET', filename, true); + ajax.send(); + ajax.onerror = function (e) { error("File error") }; + this.src = filename; + this.loading = true; + } + function play() { // starts play if paused + if (!gif.playing) { + gif.paused = false; + gif.playing = true; + playing(); + } + } + function pause() { // stops play + gif.paused = true; + gif.playing = false; + clearTimeout(timerID); + } + function togglePlay(){ + if(gif.paused || !gif.playing){ gif.play() } + else{ gif.pause() } + } + function seekFrame(frame) { // seeks to frame number. + clearTimeout(timerID); + gif.currentFrame = frame % gif.frames.length; + if (gif.playing) { playing() } + else { gif.image = gif.frames[gif.currentFrame].image } + } + function seek(time) { // time in Seconds // seek to frame that would be displayed at time + clearTimeout(timerID); + if (time < 0) { time = 0 } + time *= 1000; // in ms + time %= gif.length; + var frame = 0; + while (time > gif.frames[frame].time + gif.frames[frame].delay && frame < gif.frames.length) { frame += 1 } + gif.currentFrame = frame; + if (gif.playing) { playing() } + else { gif.image = gif.frames[gif.currentFrame].image} + } + function playing() { + var delay; + var frame; + if (gif.playSpeed === 0) { + gif.pause(); + return; + } else { + if (gif.playSpeed < 0) { + gif.currentFrame -= 1; + if (gif.currentFrame < 0) {gif.currentFrame = gif.frames.length - 1 } + frame = gif.currentFrame; + frame -= 1; + if (frame < 0) { frame = gif.frames.length - 1 } + delay = -gif.frames[frame].delay * 1 / gif.playSpeed; + } else { + gif.currentFrame += 1; + gif.currentFrame %= gif.frames.length; + delay = gif.frames[gif.currentFrame].delay * 1 / gif.playSpeed; + } + gif.image = gif.frames[gif.currentFrame].image; + timerID = setTimeout(playing, delay); + } + } + var gif = { // the gif image object + onload : null, // fire on load. Use waitTillDone = true to have load fire at end or false to fire on first frame + onerror : null, // fires on error + onprogress : null, // fires a load progress event + onloadall : null, // event fires when all frames have loaded and gif is ready + paused : false, // true if paused + playing : false, // true if playing + waitTillDone : true, // If true onload will fire when all frames loaded, if false, onload will fire when first frame has loaded + loading : false, // true if still loading + firstFrameOnly : false, // if true only load the first frame + width : null, // width in pixels + height : null, // height in pixels + frames : [], // array of frames + comment : "", // comments if found in file. Note I remember that some gifs have comments per frame if so this will be all comment concatenated + length : 0, // gif length in ms (1/1000 second) + currentFrame : 0, // current frame. + frameCount : 0, // number of frames + playSpeed : 1, // play speed 1 normal, 2 twice 0.5 half, -1 reverse etc... + lastFrame : null, // temp hold last frame loaded so you can display the gif as it loads + image : null, // the current image at the currentFrame + playOnLoad : true, // if true starts playback when loaded + // functions + load : loadGif, // call this to load a file + cancel : cancelLoad, // call to stop loading + play : play, // call to start play + pause : pause, // call to pause + seek : seek, // call to seek to time + seekFrame : seekFrame, // call to seek to frame + togglePlay : togglePlay, // call to toggle play and pause state + }; + return gif; +} + + diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..cc2df22 --- /dev/null +++ b/js/index.js @@ -0,0 +1,146 @@ +import { app } from "./app.js"; +import { api } from "./api.js"; +import { ComfyWidgets, LGraphNode } from "./widgets.js"; +import { GIF } from './gif.js'; + + +class ComfyPets extends LiteGraph.LGraphNode { + + constructor() { + super() + if (!this.properties) { + this.properties = {}; + } + + this.title = "Comfy Pet", + + this.widgets_start_y = 10; + //this.setSize(this.computeSize()); + + this.serialize_widgets = true; + this.isVirtualNode = true; + + this.x = 0; + + this.currentDirection="right" + this.time = 0; + this.directionDuration = 0; + this.petGif = GIF(); + this.petGif.load("https://raw.githubusercontent.com/tonybaloney/vscode-pets/master/media/dog/akita_walk_8fps.gif") + //this.petGif.src = ""; + + this.backgroundImage = new Image(); + this.backgroundImage.src = "https://media.istockphoto.com/id/1333010525/vector/simple-flat-pixel-art-illustration-of-cartoon-outdoor-landscape-background-pixel-arcade.jpg?s=612x612&w=0&k=20&c=uTGqB9fhmjzaNd17EGRHYU04_70K7a3M8ilRoJjDwtY=" + } + + drawBackground(ctx) { + const [width, height] = this.size + // Custom drawing logic here + ctx.fillStyle = "lightblue"; + ctx.fillRect(0, 0, this.size[0], this.size[1]); + + ctx.font = "12px Arial"; + ctx.fillStyle = "black"; + ctx.fillText("My Custom Node", 10, 20); + + ctx.drawImage(this.backgroundImage, 0, 0, width, height); + } + + + drawPet(ctx) { + const [width, height] = this.size + const rect1 = { x: height, y: height - 50, width: 50, height: 50, color: "blue", speed: 2 }; + function drawRect(rect, x) { + ctx.fillStyle = rect.color; + ctx.fillRect(x, rect.y, rect.width, rect.height); + } + + //drawRect(rect1, this.x) + + try { + ctx.drawImage(this.petGif.image, this.x, height-50, 75, 50); + } catch (e) { + + } + } + + + movePet(ctx) { + const [width, height] = this.size + + const changeDirections = () => { + Math.random() < 0.5 + ? this.currentDirection = "left" + : this.currentDirection = "right" + } + + if (Date.now() - this.time > this.directionDuration) { + changeDirections(); + this.time = Date.now(); + this.directionDuration = Math.random() * 4000 + 1000; + } + + if(this.currentDirection == "right") { + this.x += 0.5; + } + if(this.currentDirection == "left") { + this.x -= 0.5; + } + + // handle movement + if(this.x > width - 75) { + this.currentDirection = "left" + } + if(this.x < 0) { + this.currentDirection = "right" + } + } + + onDrawForeground(ctx) { + + this.drawBackground(ctx) + this.movePet(ctx) + this.drawPet(ctx) // render pet onto canvas + + // animation loop + /* + const draw = () => { + this.setDirtyCanvas(true, true) + requestAnimationFrame(draw) + } + draw(ctx) + */ + } +} + + +/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ +/** @type {ComfyExtension} */ +const ext = { + name: "nathannlu.ComfyPets", + + init(app) { + //console.log("init") + }, + + // Add in node that keeps track of workflow_name + // and etc + registerCustomNodes() { + /** @type {LGraphNode}*/ + + LiteGraph.registerNodeType( + "ComfyPets", + Object.assign(ComfyPets, { + title_mode: LiteGraph.NORMAL_TITLE, + title: "Comfy Pet", + collapsable: false, + }), + ); + ComfyPets.category = "Pets"; + }, + +}; + + +app.registerExtension(ext); + diff --git a/js/widgets.js b/js/widgets.js new file mode 100644 index 0000000..a5bd3cf --- /dev/null +++ b/js/widgets.js @@ -0,0 +1,18 @@ +// /** @typedef {import('../../../web/scripts/api.js').api} API*/ +// import { api as _api } from "../../scripts/api.js"; +// /** @type {API} */ +// export const api = _api; + +/** @typedef {typeof import('../../../web/scripts/widgets.js').ComfyWidgets} Widgets*/ +import { ComfyWidgets as _ComfyWidgets } from "../../scripts/widgets.js"; + +/** + * @type {Widgets} + */ +export const ComfyWidgets = _ComfyWidgets; + +// import { LGraphNode as _LGraphNode } from "../../types/litegraph.js"; + +/** @typedef {typeof import('../../../web/types/litegraph.js').LGraphNode} LGraphNode*/ +/** @type {LGraphNode}*/ +export const LGraphNode = LiteGraph.LGraphNode;