From df4b1f7929443b17831e4905ca38184d0b3843b6 Mon Sep 17 00:00:00 2001 From: chflame163 <130118553+chflame163@users.noreply.github.com> Date: Fri, 19 Jan 2024 19:03:57 +0800 Subject: [PATCH] add ColorPicker node --- README.MD | 12 + README_CN.MD | 13 + __init__.py | 29 +- image/color_picker.png | Bin 0 -> 65746 bytes js/comfy_shared.js | 342 ++++++++++++++++ js/debug.js | 109 +++++ js/mtb_widgets.js | 908 +++++++++++++++++++++++++++++++++++++++++ js/parse-css.js | 362 ++++++++++++++++ py/color_picker.py | 47 +++ 9 files changed, 1821 insertions(+), 1 deletion(-) create mode 100644 image/color_picker.png create mode 100644 js/comfy_shared.js create mode 100644 js/debug.js create mode 100644 js/mtb_widgets.js create mode 100644 js/parse-css.js create mode 100644 py/color_picker.py diff --git a/README.MD b/README.MD index 6069f0a..a3a3d9c 100644 --- a/README.MD +++ b/README.MD @@ -6,6 +6,7 @@ Currently completed the Drop Shadow, OuterGlow, and some auxiliary nodes. follow [中文说明点这里](./README_CN.MD) ## Update +* Commit ColorPick node. * Commit Stroke node. * Commit MaskPreview node. * Commit ImageOpacity node. @@ -117,6 +118,17 @@ Preview the input mask input:mask +### ColorPicker: +Modify web extensions from [mtb nodes](https://github.com/melMass/comfy_mtb). Select colors on the color palette and output RGB values. +![image](image/color_picker.png) + +Node options: +* mode: The output format is available in hexadecimal (HEX) and decimal (DEC). + +Output Type: +* string + + ### Annotation: 1 The layer_image, layer_mask and the background_image(if have input), These three items must be of the same size.。 diff --git a/README_CN.MD b/README_CN.MD index 8537569..4e2589a 100644 --- a/README_CN.MD +++ b/README_CN.MD @@ -116,6 +116,19 @@ ComfyUI ÊäÈ룺mask +### ColorPicker: +ÔÚÉ«°åÉÏѡȡÑÕÉ«²¢Êä³ö¡£ +¸Ä×Ô[mtb nodes](https://github.com/melMass/comfy_mtb)µÄweb extensions¡£ +![image](image/color_picker.png) + +½ÚµãÑ¡Ïî˵Ã÷£º +* mode£º Êä³ö¸ñʽ£¬¿ÉѡʮÁù½øÖÆ(HEX)»òÊ®½øÖÆ(DEC)¡£ + +Êä³ö£º +* string + + + ### ½ÚµãÑ¡Ïî×¢½â£º 1 image¡¢maskºÍbackground_image(Èç¹ûÓÐÊäÈë)ÕâÈýÏî±ØÐëÊÇÏàͬµÄ³ß´ç¡£ 2 mask²»ÊDZØÐëµÄÊäÈëÏĬÈÏʹÓÃimageµÄalphaͨµÀ£¬Èç¹ûimageÊäÈë²»°üº¬alphaͨµÀ½«×Ô¶¯´´½¨Õû¸öͼÏñµÄalphaͨµÀ¡£Èç¹ûÊäÈëmask£¬Ô­±¾µÄalphaͨµÀ½«±»mask¸²¸Ç¡£ diff --git a/__init__.py b/__init__.py index ea84781..8a24c56 100644 --- a/__init__.py +++ b/__init__.py @@ -2,11 +2,38 @@ import importlib.util import glob import os import sys -# from .lam import init, get_ext_dir +import __main__ +import filecmp +import shutil NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} +python = sys.executable +extentions_folder = os.path.join(os.path.dirname(os.path.realpath(__main__.__file__)), + "web" + os.sep + "extensions" + os.sep + "dzNodes") +javascript_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "js") + +if not os.path.exists(extentions_folder): + print('# 😺dzNodes: Making the "web\extensions\dzNodes" folder') + os.mkdir(extentions_folder) + +result = filecmp.dircmp(javascript_folder, extentions_folder) + +if result.left_only or result.diff_files: + print('# 😺dzNodes: Update to javascripts files detected') + file_list = list(result.left_only) + file_list.extend(x for x in result.diff_files if x not in file_list) + + for file in file_list: + print(f'# 😺dzNodes:: Copying {file} to extensions folder') + src_file = os.path.join(javascript_folder, file) + dst_file = os.path.join(extentions_folder, file) + if os.path.exists(dst_file): + os.remove(dst_file) + shutil.copy(src_file, dst_file) + + def get_ext_dir(subpath=None, mkdir=False): dir = os.path.dirname(__file__) if subpath is not None: diff --git a/image/color_picker.png b/image/color_picker.png new file mode 100644 index 0000000000000000000000000000000000000000..af1530c6ad3bd7584dbeb4d717cc910b89bdfb44 GIT binary patch literal 65746 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�H+t3f}~fiOLcA@wYT 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 Ag8%>k literal 0 HcmV?d00001 diff --git a/js/comfy_shared.js b/js/comfy_shared.js new file mode 100644 index 0000000..b8a1332 --- /dev/null +++ b/js/comfy_shared.js @@ -0,0 +1,342 @@ +/** + * File: comfy_shared.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' + +export const log = (...args) => { + if (window.MTB?.DEBUG) { + console.debug(...args) + } +} + +//- WIDGET UTILS +export const CONVERTED_TYPE = 'converted-widget' + +export const hasWidgets = (node) => { + if (!node.widgets || !node.widgets?.[Symbol.iterator]) { + return false + } + return true +} + +export const cleanupNode = (node) => { + if (!hasWidgets(node)) { + return + } + + for (const w of node.widgets) { + if (w.canvas) { + w.canvas.remove() + } + if (w.inputEl) { + w.inputEl.remove() + } + // calls the widget remove callback + w.onRemoved?.() + } +} + +export function offsetDOMWidget( + widget, + ctx, + node, + widgetWidth, + widgetY, + height +) { + const margin = 10 + const elRect = ctx.canvas.getBoundingClientRect() + const transform = new DOMMatrix() + .scaleSelf( + elRect.width / ctx.canvas.width, + elRect.height / ctx.canvas.height + ) + .multiplySelf(ctx.getTransform()) + .translateSelf(margin, margin + widgetY) + + const scale = new DOMMatrix().scaleSelf(transform.a, transform.d) + Object.assign(widget.inputEl.style, { + transformOrigin: '0 0', + transform: scale, + left: `${transform.a + transform.e}px`, + top: `${transform.d + transform.f}px`, + width: `${widgetWidth - margin * 2}px`, + // height: `${(widget.parent?.inputHeight || 32) - (margin * 2)}px`, + height: `${(height || widget.parent?.inputHeight || 32) - margin * 2}px`, + + position: 'absolute', + background: !node.color ? '' : node.color, + color: !node.color ? '' : 'white', + zIndex: 5, //app.graph._nodes.indexOf(node), + }) +} + +/** + * Extracts the type and link type from a widget config object. + * @param {*} config + * @returns + */ +export function getWidgetType(config) { + // Special handling for COMBO so we restrict links based on the entries + let type = config?.[0] + let linkType = type + if (type instanceof Array) { + type = 'COMBO' + linkType = linkType.join(',') + } + return { type, linkType } +} + +export const dynamic_connection = ( + node, + index, + connected, + connectionPrefix = 'input_', + connectionType = 'PSDLAYER' +) => { + // remove all non connected inputs + if (!connected && node.inputs.length > 1) { + log(`Removing input ${index} (${node.inputs[index].name})`) + if (node.widgets) { + const w = node.widgets.find((w) => w.name === node.inputs[index].name) + if (w) { + w.onRemoved?.() + node.widgets.length = node.widgets.length - 1 + } + } + node.removeInput(index) + + // make inputs sequential again + for (let i = 0; i < node.inputs.length; i++) { + node.inputs[i].label = `${connectionPrefix}${i + 1}` + } + } + + // add an extra input + if (node.inputs[node.inputs.length - 1].link != undefined) { + log( + `Adding input ${node.inputs.length + 1} (${connectionPrefix}${ + node.inputs.length + 1 + })` + ) + + node.addInput( + `${connectionPrefix}${node.inputs.length + 1}`, + connectionType + ) + } +} + +/** + * Appends a callback to the extra menu options of a given node type. + * @param {*} nodeType + * @param {*} cb + */ +export 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 + } +} + +export function hideWidget(node, widget, suffix = '') { + widget.origType = widget.type + widget.hidden = true + widget.origComputeSize = widget.computeSize + widget.origSerializeValue = widget.serializeValue + widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically + widget.type = CONVERTED_TYPE + suffix + widget.serializeValue = () => { + // Prevent serializing the widget if we have no input linked + const { link } = node.inputs.find((i) => i.widget?.name === widget.name) + if (link == null) { + return undefined + } + return widget.origSerializeValue + ? widget.origSerializeValue() + : widget.value + } + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidget(node, w, ':' + widget.name) + } + } +} + +export function showWidget(widget) { + widget.type = widget.origType + widget.computeSize = widget.origComputeSize + widget.serializeValue = widget.origSerializeValue + + delete widget.origType + delete widget.origComputeSize + delete widget.origSerializeValue + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + showWidget(w) + } + } +} + +export function convertToWidget(node, widget) { + showWidget(widget) + const sz = node.size + node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name)) + + for (const widget of node.widgets) { + widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT + } + + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) +} + +export function convertToInput(node, widget, config) { + hideWidget(node, widget) + + const { linkType } = getWidgetType(config) + + // Add input and store widget config for creating on primitive node + const sz = node.size + node.addInput(widget.name, linkType, { + widget: { name: widget.name, config }, + }) + + for (const widget of node.widgets) { + widget.last_y += LiteGraph.NODE_SLOT_HEIGHT + } + + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]) +} + +export function hideWidgetForGood(node, widget, suffix = '') { + widget.origType = widget.type + widget.origComputeSize = widget.computeSize + widget.origSerializeValue = widget.serializeValue + widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically + widget.type = CONVERTED_TYPE + suffix + // widget.serializeValue = () => { + // // Prevent serializing the widget if we have no input linked + // const w = node.inputs?.find((i) => i.widget?.name === widget.name); + // if (w?.link == null) { + // return undefined; + // } + // return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; + // }; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidgetForGood(node, w, ':' + widget.name) + } + } +} + +export function fixWidgets(node) { + if (node.inputs) { + for (const input of node.inputs) { + log(input) + if (input.widget || node.widgets) { + // if (newTypes.includes(input.type)) { + const matching_widget = node.widgets.find((w) => w.name === input.name) + if (matching_widget) { + // if (matching_widget.hidden) { + // log(`Already hidden skipping ${matching_widget.name}`) + // continue + // } + const w = node.widgets.find((w) => w.name === matching_widget.name) + if (w && w.type != CONVERTED_TYPE) { + log(w) + log(`hidding ${w.name}(${w.type}) from ${node.type}`) + log(node) + hideWidget(node, w) + } else { + log(`converting to widget ${w}`) + + convertToWidget(node, input) + } + } + } + } + } +} +export function inner_value_change(widget, value, event = undefined) { + if (widget.type == 'number' || widget.type == 'BBOX') { + value = Number(value) + } else if (widget.type == 'BOOL') { + value = Boolean(value) + } + widget.value = value + if ( + widget.options && + widget.options.property && + node.properties[widget.options.property] !== undefined + ) { + node.setProperty(widget.options.property, value) + } + if (widget.callback) { + widget.callback(widget.value, app.canvas, node, pos, event) + } +} + +//- COLOR UTILS +export function isColorBright(rgb, threshold = 240) { + const brightess = getBrightness(rgb) + return brightess > threshold +} + +function getBrightness(rgbObj) { + return Math.round( + (parseInt(rgbObj[0]) * 299 + + parseInt(rgbObj[1]) * 587 + + parseInt(rgbObj[2]) * 114) / + 1000 + ) +} + +//- HTML / CSS UTILS +export function defineClass(className, classStyles) { + const styleSheets = document.styleSheets + + // Helper function to check if the class exists in a style sheet + function classExistsInStyleSheet(styleSheet) { + const rules = styleSheet.rules || styleSheet.cssRules + for (const rule of rules) { + if (rule.selectorText === `.${className}`) { + return true + } + } + return false + } + + // Check if the class is already defined in any of the style sheets + let classExists = false + for (const styleSheet of styleSheets) { + if (classExistsInStyleSheet(styleSheet)) { + classExists = true + break + } + } + + // If the class doesn't exist, add the new class definition to the first style sheet + if (!classExists) { + if (styleSheets[0].insertRule) { + styleSheets[0].insertRule(`.${className} { ${classStyles} }`, 0) + } else if (styleSheets[0].addRule) { + styleSheets[0].addRule(`.${className}`, classStyles, 0) + } + } +} diff --git a/js/debug.js b/js/debug.js new file mode 100644 index 0000000..cf7c89a --- /dev/null +++ b/js/debug.js @@ -0,0 +1,109 @@ +/** + * File: debug.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' + +import * as shared from './comfy_shared.js' +import { log } from './comfy_shared.js' +import { MtbWidgets } from './mtb_widgets.js' + +// TODO: respect inputs order... + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +app.registerExtension({ + name: 'mtb.Debug', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === 'Debug (mtb)') { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + // TODO: remove all widgets on disconnect once computed + shared.dynamic_connection(this, index, connected, 'anything_', '*') + + //- infer type + if (link_info) { + const fromNode = this.graph._nodes.find( + (otherNode) => otherNode.id == link_info.origin_id + ) + const type = fromNode.outputs[link_info.origin_slot].type + this.inputs[index].type = type + // this.inputs[index].label = type.toLowerCase() + } + //- restore dynamic input + if (!connected) { + this.inputs[index].type = '*' + this.inputs[index].label = `anything_${index + 1}` + } + } + + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments) + + const prefix = 'anything_' + + if (this.widgets) { + // const pos = this.widgets.findIndex((w) => w.name === "anything_1"); + // if (pos !== -1) { + for (let i = 0; i < this.widgets.length; i++) { + this.widgets[i].onRemoved?.() + } + this.widgets.length = 0 + } + let widgetI = 1 + if (message.text) { + for (const txt of message.text) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt)) + ) + w.parent = this + widgetI++ + } + } + if (message.b64_images) { + for (const img of message.b64_images) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img) + ) + w.parent = this + widgetI++ + } + // this.onResize?.(this.size); + // this.resize?.(this.size) + } + + this.setSize(this.computeSize()) + + this.onRemoved = function () { + // When removing this node we need to remove the input from the DOM + for (let y in this.widgets) { + if (this.widgets[y].canvas) { + this.widgets[y].canvas.remove() + } + this.widgets[y].onRemoved?.() + } + } + } + } + }, +}) diff --git a/js/mtb_widgets.js b/js/mtb_widgets.js new file mode 100644 index 0000000..e4afc46 --- /dev/null +++ b/js/mtb_widgets.js @@ -0,0 +1,908 @@ +/** + * File: mtb_widgets.js + * Project: comfy_mtb + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' +import { api } from '../../scripts/api.js' + +import parseCss from './parse-css.js' +import * as shared from './comfy_shared.js' +import { log } from './comfy_shared.js' + +const newTypes = [, /*'BOOL'*/ 'COLOR', 'BBOX'] + +const withFont = (ctx, font, cb) => { + const oldFont = ctx.font + ctx.font = font + cb() + ctx.font = oldFont +} + +const calculateTextDimensions = (ctx, value, width, fontSize = 16) => { + const words = value.split(' ') + const lines = [] + let currentLine = '' + for (const word of words) { + const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}` + const testWidth = ctx.measureText(testLine).width + if (testWidth > width) { + lines.push(currentLine) + currentLine = word + } else { + currentLine = testLine + } + } + if (lines.length === 0) lines.push(value) + const textHeight = (lines.length + 1) * fontSize + const maxLineWidth = lines.reduce( + (maxWidth, line) => Math.max(maxWidth, ctx.measureText(line).width), + 0 + ) + return { textHeight, maxLineWidth } +} + +export const MtbWidgets = { + BBOX: (key, val) => { + /** @type {import("./types/litegraph").IWidget} */ + const widget = { + name: key, + type: 'BBOX', + // options: val, + y: 0, + value: val?.default || [0, 0, 0, 0], + options: {}, + + draw: function (ctx, node, widget_width, widgetY, height) { + const hide = this.type !== 'BBOX' && app.canvas.ds.scale > 0.5 + + const show_text = true + const outline_color = LiteGraph.WIDGET_OUTLINE_COLOR + const background_color = LiteGraph.WIDGET_BGCOLOR + const text_color = LiteGraph.WIDGET_TEXT_COLOR + const secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR + const H = LiteGraph.NODE_WIDGET_HEIGHT + + let margin = 15 + let numWidgets = 4 // Number of stacked widgets + + if (hide) return + + for (let i = 0; i < numWidgets; i++) { + let currentY = widgetY + i * (H + margin) // Adjust Y position for each widget + + ctx.textAlign = 'left' + ctx.strokeStyle = outline_color + ctx.fillStyle = background_color + ctx.beginPath() + if (show_text) + ctx.roundRect(margin, currentY, widget_width - margin * 2, H, [ + H * 0.5, + ]) + else ctx.rect(margin, currentY, widget_width - margin * 2, H) + ctx.fill() + if (show_text) { + if (!this.disabled) ctx.stroke() + ctx.fillStyle = text_color + if (!this.disabled) { + ctx.beginPath() + ctx.moveTo(margin + 16, currentY + 5) + ctx.lineTo(margin + 6, currentY + H * 0.5) + ctx.lineTo(margin + 16, currentY + H - 5) + ctx.fill() + ctx.beginPath() + ctx.moveTo(widget_width - margin - 16, currentY + 5) + ctx.lineTo(widget_width - margin - 6, currentY + H * 0.5) + ctx.lineTo(widget_width - margin - 16, currentY + H - 5) + ctx.fill() + } + ctx.fillStyle = secondary_text_color + ctx.fillText( + this.label || this.name, + margin * 2 + 5, + currentY + H * 0.7 + ) + ctx.fillStyle = text_color + ctx.textAlign = 'right' + + ctx.fillText( + Number(this.value).toFixed( + this.options?.precision !== undefined + ? this.options.precision + : 3 + ), + widget_width - margin * 2 - 20, + currentY + H * 0.7 + ) + } + } + }, + mouse: function (event, pos, node) { + let old_value = this.value + let x = pos[0] - node.pos[0] + let y = pos[1] - node.pos[1] + let width = node.size[0] + let H = LiteGraph.NODE_WIDGET_HEIGHT + let margin = 5 + let numWidgets = 4 // Number of stacked widgets + + for (let i = 0; i < numWidgets; i++) { + let currentY = y + i * (H + margin) // Adjust Y position for each widget + + if ( + event.type == LiteGraph.pointerevents_method + 'move' && + this.type == 'BBOX' + ) { + if (event.deltaX) + this.value += event.deltaX * 0.1 * (this.options?.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (event.type == LiteGraph.pointerevents_method + 'down') { + let values = this.options?.values + if (values && values.constructor === Function) { + values = this.options.values(w, node) + } + let values_list = null + + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + if (this.type == 'BBOX') { + this.value += delta * 0.1 * (this.options.step || 1) + if (this.options.min != null && this.value < this.options.min) { + this.value = this.options.min + } + if (this.options.max != null && this.value > this.options.max) { + this.value = this.options.max + } + } else if (delta) { + //clicked in arrow, used for combos + let index = -1 + this.last_mouseclick = 0 //avoids dobl click event + if (values.constructor === Object) + index = values_list.indexOf(String(this.value)) + delta + else index = values_list.indexOf(this.value) + delta + if (index >= values_list.length) { + index = values_list.length - 1 + } + if (index < 0) { + index = 0 + } + if (values.constructor === Array) this.value = values[index] + else this.value = index + } + } //end mousedown + else if ( + event.type == LiteGraph.pointerevents_method + 'up' && + this.type == 'BBOX' + ) { + let delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0 + if (event.click_time < 200 && delta == 0) { + this.prompt( + 'Value', + this.value, + function (v) { + // check if v is a valid equation or a number + if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) { + try { + //solve the equation if possible + v = eval(v) + } catch (e) {} + } + this.value = Number(v) + shared.inner_value_change(this, this.value, event) + }.bind(w), + event + ) + } + } + + if (old_value != this.value) + setTimeout( + function () { + shared.inner_value_change(this, this.value, event) + }.bind(this), + 20 + ) + + app.canvas.setDirty(true) + } + }, + computeSize: function (width) { + return [width, LiteGraph.NODE_WIDGET_HEIGHT * 4] + }, + // onDrawBackground: function (ctx) { + // if (!this.flags.collapsed) return; + // this.inputEl.style.display = "block"; + // this.inputEl.style.top = this.graphcanvas.offsetTop + this.pos[1] + "px"; + // this.inputEl.style.left = this.graphcanvas.offsetLeft + this.pos[0] + "px"; + // }, + // onInputChange: function (e) { + // const property = e.target.dataset.property; + // const bbox = this.getInputData(0); + // if (!bbox) return; + // bbox[property] = parseFloat(e.target.value); + // this.setOutputData(0, bbox); + // } + } + + widget.desc = 'Represents a Bounding Box with x, y, width, and height.' + return widget + }, + + COLOR: (key, val, compute = false) => { + /** @type {import("/types/litegraph").IWidget} */ + const widget = {} + widget.y = 0 + widget.name = key + widget.type = 'COLOR' + widget.options = { default: '#ff0000' } + widget.value = val || '#ff0000' + widget.draw = function (ctx, node, widgetWidth, widgetY, height) { + const hide = this.type !== 'COLOR' && app.canvas.ds.scale > 0.5 + if (hide) { + return + } + const border = 3 + ctx.fillStyle = '#000' + ctx.fillRect(0, widgetY, widgetWidth, height) + ctx.fillStyle = this.value + ctx.fillRect( + border, + widgetY + border, + widgetWidth - border * 2, + height - border * 2 + ) + const color = parseCss(this.value.default || this.value) + if (!color) { + return + } + ctx.fillStyle = shared.isColorBright(color.values, 125) ? '#000' : '#fff' + + ctx.font = '14px Arial' + ctx.textAlign = 'center' + ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14) + } + widget.mouse = function (e, pos, node) { + if (e.type === 'pointerdown') { + const widgets = node.widgets.filter((w) => w.type === 'COLOR') + + for (const w of widgets) { + // color picker + const rect = [w.last_y, w.last_y + 32] + if (pos[1] > rect[0] && pos[1] < rect[1]) { + const picker = document.createElement('input') + picker.type = 'color' + picker.value = this.value + + picker.style.position = 'absolute' + picker.style.left = '999999px' //(window.innerWidth / 2) + "px"; + picker.style.top = '999999px' //(window.innerHeight / 2) + "px"; + + document.body.appendChild(picker) + + picker.addEventListener('change', () => { + this.value = picker.value + node.graph._version++ + node.setDirtyCanvas(true, true) + picker.remove() + }) + + picker.click() + } + } + } + } + widget.computeSize = function (width) { + return [width, 32] + } + + return widget + }, + + DEBUG_IMG: (name, val) => { + const w = { + name, + type: 'image', + value: val, + draw: function (ctx, node, widgetWidth, widgetY, height) { + const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch) + }, + computeSize: function (width) { + const ratio = this.inputRatio || 1 + if (width) { + return [width, width / ratio + 4] + } + return [128, 128] + }, + onRemoved: function () { + if (this.inputEl) { + this.inputEl.remove() + } + }, + } + + w.inputEl = document.createElement('img') + w.inputEl.src = w.value + w.inputEl.onload = function () { + w.inputRatio = w.inputEl.naturalWidth / w.inputEl.naturalHeight + } + document.body.appendChild(w.inputEl) + return w + }, + DEBUG_STRING: (name, val) => { + const fontSize = 16 + const w = { + name, + type: 'debug_text', + + draw: function (ctx, node, widgetWidth, widgetY, height) { + // const [cw, ch] = this.computeSize(widgetWidth) + shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, height) + }, + computeSize(width) { + if (!this.value) { + return [32, 32] + } + if (!width) { + console.debug(`No width ${this.parent.size}`) + } + let dimensions + withFont(app.ctx, `${fontSize}px monospace`, () => { + dimensions = calculateTextDimensions(app.ctx, this.value, width) + }) + const widgetWidth = Math.max( + width || this.width || 32, + dimensions.maxLineWidth + ) + const widgetHeight = dimensions.textHeight * 1.5 + return [widgetWidth, widgetHeight] + }, + onRemoved: function () { + if (this.inputEl) { + this.inputEl.remove() + } + }, + get value() { + return this.inputEl.innerHTML + }, + set value(val) { + this.inputEl.innerHTML = val + this.parent?.setSize?.(this.parent?.computeSize()) + }, + } + + w.inputEl = document.createElement('p') + w.inputEl.style = ` + text-align: center; + font-size: ${fontSize}px; + color: var(--input-text); + line-height: 0; + font-family: monospace; + ` + w.value = val + document.body.appendChild(w.inputEl) + + return w + }, +} + +/** + * @returns {import("./types/comfy").ComfyExtension} extension + */ +const mtb_widgets = { + name: 'mtb.widgets', + + init: async () => { + log('Registering mtb.widgets') + try { + const res = await api.fetchApi('/mtb/debug') + const msg = await res.json() + if (!window.MTB) { + window.MTB = {} + } + window.MTB.DEBUG = msg.enabled + } catch (e) { + console.error('Error:', error) + } + }, + + setup: () => { + app.ui.settings.addSetting({ + id: 'mtb.Debug.enabled', + name: '[mtb] Enable Debug (py and js)', + type: 'boolean', + defaultValue: false, + + tooltip: + 'This will enable debug messages in the console and in the python console respectively', + attrs: { + style: { + fontFamily: 'monospace', + }, + }, + async onChange(value) { + if (value) { + console.log('Enabled DEBUG mode') + } + if (!window.MTB) { + window.MTB = {} + } + window.MTB.DEBUG = value + await api + .fetchApi('/mtb/debug', { + method: 'POST', + body: JSON.stringify({ + enabled: value, + }), + }) + .then((response) => {}) + .catch((error) => { + console.error('Error:', error) + }) + }, + }) + }, + + getCustomWidgets: function () { + return { + BOOL: (node, inputName, inputData, app) => { + console.debug('Registering bool') + + return { + widget: node.addCustomWidget( + MtbWidgets.BOOL(inputName, inputData[1]?.default || false) + ), + minWidth: 150, + minHeight: 30, + } + }, + + COLOR: (node, inputName, inputData, app) => { + console.debug('Registering color') + return { + widget: node.addCustomWidget( + MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000') + ), + minWidth: 150, + minHeight: 30, + } + }, + // BBOX: (node, inputName, inputData, app) => { + // console.debug("Registering bbox") + // return { + // widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])), + // minWidth: 150, + // minHeight: 30, + // } + + // } + } + }, + /** + * @param {import("./types/comfy").NodeType} nodeType + * @param {import("./types/comfy").NodeDef} nodeData + * @param {import("./types/comfy").App} app + */ + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // const rinputs = nodeData.input?.required + + let has_custom = false + if (nodeData.input && nodeData.input.required) { + for (const i of Object.keys(nodeData.input.required)) { + const input_type = nodeData.input.required[i][0] + + if (newTypes.includes(input_type)) { + has_custom = true + break + } + } + } + if (has_custom) { + //- Add widgets on node creation + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + this.serialize_widgets = true + this.setSize?.(this.computeSize()) + + this.onRemoved = function () { + // When removing this node we need to remove the input from the DOM + shared.cleanupNode(this) + } + return r + } + + //- Extra menus + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined + if (this.widgets) { + let toInput = [] + let toWidget = [] + for (const w of this.widgets) { + if (w.type === shared.CONVERTED_TYPE) { + //- This is already handled by widgetinputs.js + // toWidget.push({ + // content: `Convert ${w.name} to widget`, + // callback: () => shared.convertToWidget(this, w), + // }); + } else if (newTypes.includes(w.type)) { + const config = nodeData?.input?.required[w.name] || + nodeData?.input?.optional?.[w.name] || [w.type, w.options || {}] + + toInput.push({ + content: `Convert ${w.name} to input`, + callback: () => shared.convertToInput(this, w, config), + }) + } + } + if (toInput.length) { + options.push(...toInput, null) + } + + if (toWidget.length) { + options.push(...toWidget, null) + } + } + + return r + } + } + + //- Extending Python Nodes + switch (nodeData.name) { + case 'Psd Save (mtb)': { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + shared.dynamic_connection(this, index, connected) + return r + } + break + } + //TODO: remove this non sense + case 'Get Batch From History (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + const internal_count = this.widgets.find( + (w) => w.name === 'internal_count' + ) + shared.hideWidgetForGood(this, internal_count) + internal_count.afterQueued = function () { + this.value++ + } + + return r + } + + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + const r = onExecuted ? onExecuted.apply(this, message) : undefined + return r + } + + break + } + case 'Save Gif (mtb)': + case 'Save Animated Image (mtb)': { + const onExecuted = nodeType.prototype.onExecuted + nodeType.prototype.onExecuted = function (message) { + const prefix = 'anything_' + const r = onExecuted ? onExecuted.apply(this, message) : undefined + + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === `${prefix}_0`) + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + this.widgets[i].onRemoved?.() + } + this.widgets.length = pos + } + + let imgURLs = [] + if (message) { + if (message.gif) { + imgURLs = imgURLs.concat( + message.gif.map((params) => { + return api.apiURL( + '/view?' + new URLSearchParams(params).toString() + ) + }) + ) + } + if (message.apng) { + imgURLs = imgURLs.concat( + message.apng.map((params) => { + return api.apiURL( + '/view?' + new URLSearchParams(params).toString() + ) + }) + ) + } + let i = 0 + for (const img of imgURLs) { + const w = this.addCustomWidget( + MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img) + ) + w.parent = this + i++ + } + } + const onRemoved = this.onRemoved + this.onRemoved = () => { + shared.cleanupNode(this) + return onRemoved?.() + } + } + this.setSize?.(this.computeSize()) + return r + } + + break + } + case 'Animation Builder (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + + this.changeMode(LiteGraph.ALWAYS) + + const raw_iteration = this.widgets.find( + (w) => w.name === 'raw_iteration' + ) + const raw_loop = this.widgets.find((w) => w.name === 'raw_loop') + + const total_frames = this.widgets.find( + (w) => w.name === 'total_frames' + ) + const loop_count = this.widgets.find((w) => w.name === 'loop_count') + + shared.hideWidgetForGood(this, raw_iteration) + shared.hideWidgetForGood(this, raw_loop) + + raw_iteration._value = 0 + + const value_preview = this.addCustomWidget( + MtbWidgets['DEBUG_STRING']('value_preview', 'Idle') + ) + value_preview.parent = this + + const loop_preview = this.addCustomWidget( + MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle') + ) + loop_preview.parent = this + + const onReset = () => { + raw_iteration.value = 0 + raw_loop.value = 0 + + value_preview.value = 'Idle' + loop_preview.value = 'Iteration: Idle' + + app.canvas.setDirty(true) + } + + const reset_button = this.addWidget( + 'button', + `Reset`, + 'reset', + onReset + ) + + const run_button = this.addWidget('button', `Queue`, 'queue', () => { + onReset() // this could maybe be a setting or checkbox + app.queuePrompt(0, total_frames.value * loop_count.value) + window.MTB?.notify?.( + `Started a queue of ${total_frames.value} frames (for ${ + loop_count.value + } loop, so ${total_frames.value * loop_count.value})`, + 5000 + ) + }) + + this.onRemoved = () => { + shared.cleanupNode(this) + app.canvas.setDirty(true) + } + + raw_iteration.afterQueued = function () { + this.value++ + raw_loop.value = Math.floor(this.value / total_frames.value) + + value_preview.value = `frame: ${ + raw_iteration.value % total_frames.value + } / ${total_frames.value - 1}` + + if (raw_loop.value + 1 > loop_count.value) { + loop_preview.value = 'Done 😎!' + } else { + loop_preview.value = `current loop: ${raw_loop.value + 1}/${ + loop_count.value + }` + } + } + + return r + } + + break + } + case 'Text Encore Frames (mtb)': { + const onConnectionsChange = nodeType.prototype.onConnectionsChange + nodeType.prototype.onConnectionsChange = function ( + type, + index, + connected, + link_info + ) { + const r = onConnectionsChange + ? onConnectionsChange.apply(this, arguments) + : undefined + + shared.dynamic_connection(this, index, connected) + return r + } + break + } + case 'Interpolate Clip Sequential (mtb)': { + const onNodeCreated = nodeType.prototype.onNodeCreated + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined + const addReplacement = () => { + const input = this.addInput( + `replacement_${this.widgets.length}`, + 'STRING', + '' + ) + console.log(input) + this.addWidget('STRING', `replacement_${this.widgets.length}`, '') + } + //- add + this.addWidget('button', '+', 'add', function (value, widget, node) { + console.log('Button clicked', value, widget, node) + addReplacement() + }) + //- remove + this.addWidget( + 'button', + '-', + 'remove', + function (value, widget, node) { + console.log(`Button clicked: ${value}`, widget, node) + } + ) + + return r + } + break + } + case 'Styles Loader (mtb)': { + const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions + nodeType.prototype.getExtraMenuOptions = function (_, options) { + const r = origGetExtraMenuOptions + ? origGetExtraMenuOptions.apply(this, arguments) + : undefined + + const getStyle = async (node) => { + try { + const getStyles = await api.fetchApi('/mtb/actions', { + method: 'POST', + body: JSON.stringify({ + name: 'getStyles', + args: + node.widgets && node.widgets[0].value + ? node.widgets[0].value + : '', + }), + }) + + const output = await getStyles.json() + return output?.result + } catch (e) { + console.error(e) + } + } + const extracters = [ + { + content: 'Extract Positive to Text node', + callback: async () => { + const style = await getStyle(this) + if (style && style.length >= 1) { + if (style[0]) { + window.MTB?.notify?.( + `Extracted positive from ${this.widgets[0].value}` + ) + const tn = LiteGraph.createNode('Text box') + app.graph.add(tn) + tn.title = `${this.widgets[0].value} (Positive)` + tn.widgets[0].value = style[0] + } else { + window.MTB?.notify?.( + `No positive to extract for ${this.widgets[0].value}` + ) + } + } + }, + }, + { + content: 'Extract Negative to Text node', + callback: async () => { + const style = await getStyle(this) + if (style && style.length >= 2) { + if (style[1]) { + window.MTB?.notify?.( + `Extracted negative from ${this.widgets[0].value}` + ) + const tn = LiteGraph.createNode('Text box') + app.graph.add(tn) + tn.title = `${this.widgets[0].value} (Negative)` + tn.widgets[0].value = style[1] + } else { + window.MTB.notify( + `No negative to extract for ${this.widgets[0].value}` + ) + } + } + }, + }, + ] + options.push(...extracters) + } + + break + } + case 'Save Tensors (mtb)': { + const onDrawBackground = nodeType.prototype.onDrawBackground + nodeType.prototype.onDrawBackground = function (ctx, canvas) { + const r = onDrawBackground + ? onDrawBackground.apply(this, arguments) + : undefined + // // draw a circle on the top right of the node, with text inside + // ctx.fillStyle = "#fff"; + // ctx.beginPath(); + // ctx.arc(this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5, this.node_width * 0.5, 0, Math.PI * 2); + // ctx.fill(); + + // ctx.fillStyle = "#000"; + // ctx.textAlign = "center"; + // ctx.font = "bold 12px Arial"; + // ctx.fillText("Save Tensors", this.size[0] - this.node_width * 0.5, this.size[1] - this.node_height * 0.5); + + return r + } + break + } + default: { + break + } + } + }, +} + +app.registerExtension(mtb_widgets) diff --git a/js/parse-css.js b/js/parse-css.js new file mode 100644 index 0000000..1ae53b0 --- /dev/null +++ b/js/parse-css.js @@ -0,0 +1,362 @@ + +// #region patterns +const float = '-?\\d*(?:\\.\\d+)'; +export const number = `(${float}?)`; +export const percentage = `(${float}?%)`; +export const numberOrPercentage = `(${float}?%?)`; +const clamp = (num, min, max) => Math.min(Math.max(min, num), max); + +const hexCharacters = 'a-f\\d'; +const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`; +const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`; +const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi'); +const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i'); + + +export const hex_pattern = new RegExp(/^#([a-f0-9]{3,4}|[a-f0-9]{4}(?:[a-f0-9]{2}){1,2})\b$/, "i"); + +export const hsl3_pattern = new RegExp(`^ + hsla?\\( + \\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s*, + \\s*${percentage}\\s*, + \\s*${percentage}\\s* + (?:,\\s*${numberOrPercentage}\\s*)? + \\) + $ +`.replace(/\n|\s/g, '')) + +export const hsl4_pattern = new RegExp(`^ + hsla?\\( + \\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s* + \\s+${percentage} + \\s+${percentage} + \\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)? + \\) + $ +`.replace(/\n|\s/g, '')) + +export const rgb3_pattern = new RegExp(`^ + rgba?\\( + \\s*${number}\\s*, + \\s*${number}\\s*, + \\s*${number}\\s* + (?:,\\s*${numberOrPercentage}\\s*)? + \\) + $ +`.replace(/\n|\s/g, '')) + +export const rgb4_pattern = new RegExp(`^ + rgba?\\( + \\s*${number} + \\s+${number} + \\s+${number} + \\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)? + \\) +$ +`.replace(/\n|\s/g, '')); + +export const transparent_pattern = new RegExp(/^transparent$/, 'i'); +// #endregion + + +// #region utils + + +/* 500 => 255, -10 => 0, 128 => 128 */ +const parseRGB = (num) => { + let n = num; + if (typeof n !== 'number') { + n = n.endsWith('%') ? (parseFloat(n) * 255) / 100 : parseFloat(n); + } + return clamp(Math.round(n), 0, 255); +}; + +/* 200 => 100, -100 => 0, 50 => 50 */ +const parsePercentage = (percentage) => clamp(parseFloat(percentage), 0, 100); + +/* '50%' => 5.0, 200 => 1, -10 => 0 */ +function parseAlpha(alpha) { + let a = alpha; + if (typeof a !== 'number') { + a = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a); + } + return clamp(a, 0, 1); +} + +export function getHEX(hex) { + const [r, g, b, a] = hex2Rgb(hex, { format: 'array' }); + return getRGB([null, ...[r, g, b, a]]); +} + +export function getHSL([, h, s, l, a = 1]) { + let hh = h; + if (hh.endsWith('turn')) { + hh = (parseFloat(hh) * 360) / 1; + } else if (hh.endsWith('rad')) { + hh = Math.round((parseFloat(hh) * 180) / Math.PI); + } else { + hh = parseFloat(hh); + } + return { + type: 'hsl', + values: [hh, parsePercentage(s), parsePercentage(l)], + alpha: parseAlpha(a === null ? 1 : a) + }; +} + +export function getRGB([, r, g, b, a = 1]) { + return { + type: 'rgb', + values: [r, g, b].map(parseRGB), + alpha: parseAlpha(a === null ? 1 : a) + }; +} +export function hex2Rgb(hex, options = {}) { + if (typeof hex !== 'string' || nonHexChars.test(hex) || !validHexSize.test(hex)) { + throw new TypeError('Expected a valid hex string'); + } + + hex = hex.replace(/^#/, ''); + let alphaFromHex = 1; + + if (hex.length === 8) { + alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255; + hex = hex.slice(0, 6); + } + + if (hex.length === 4) { + alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255; + hex = hex.slice(0, 3); + } + + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + + const number = Number.parseInt(hex, 16); + const red = number >> 16; + const green = (number >> 8) & 255; + const blue = number & 255; + const alpha = typeof options.alpha === 'number' ? options.alpha : alphaFromHex; + + if (options.format === 'array') { + return [red, green, blue, alpha]; + } + + if (options.format === 'css') { + const alphaString = alpha === 1 ? '' : ` / ${Number((alpha * 100).toFixed(2))}%`; + return `rgb(${red} ${green} ${blue}${alphaString})`; + } + + return {red, green, blue, alpha}; +} +// #endregion + + +// #region colorNames +export const colorName = { + aliceblue: [240, 248, 255], + antiquewhite: [250, 235, 215], + aqua: [0, 255, 255], + aquamarine: [127, 255, 212], + azure: [240, 255, 255], + beige: [245, 245, 220], + bisque: [255, 228, 196], + black: [0, 0, 0], + blanchedalmond: [255, 235, 205], + blue: [0, 0, 255], + blueviolet: [138, 43, 226], + brown: [165, 42, 42], + burlywood: [222, 184, 135], + cadetblue: [95, 158, 160], + chartreuse: [127, 255, 0], + chocolate: [210, 105, 30], + coral: [255, 127, 80], + cornflowerblue: [100, 149, 237], + cornsilk: [255, 248, 220], + crimson: [220, 20, 60], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgoldenrod: [184, 134, 11], + darkgray: [169, 169, 169], + darkgreen: [0, 100, 0], + darkgrey: [169, 169, 169], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkseagreen: [143, 188, 143], + darkslateblue: [72, 61, 139], + darkslategray: [47, 79, 79], + darkslategrey: [47, 79, 79], + darkturquoise: [0, 206, 209], + darkviolet: [148, 0, 211], + deeppink: [255, 20, 147], + deepskyblue: [0, 191, 255], + dimgray: [105, 105, 105], + dimgrey: [105, 105, 105], + dodgerblue: [30, 144, 255], + firebrick: [178, 34, 34], + floralwhite: [255, 250, 240], + forestgreen: [34, 139, 34], + fuchsia: [255, 0, 255], + gainsboro: [220, 220, 220], + ghostwhite: [248, 248, 255], + gold: [255, 215, 0], + goldenrod: [218, 165, 32], + gray: [128, 128, 128], + green: [0, 128, 0], + greenyellow: [173, 255, 47], + grey: [128, 128, 128], + honeydew: [240, 255, 240], + hotpink: [255, 105, 180], + indianred: [205, 92, 92], + indigo: [75, 0, 130], + ivory: [255, 255, 240], + khaki: [240, 230, 140], + lavender: [230, 230, 250], + lavenderblush: [255, 240, 245], + lawngreen: [124, 252, 0], + lemonchiffon: [255, 250, 205], + lightblue: [173, 216, 230], + lightcoral: [240, 128, 128], + lightcyan: [224, 255, 255], + lightgoldenrodyellow: [250, 250, 210], + lightgray: [211, 211, 211], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightsalmon: [255, 160, 122], + lightseagreen: [32, 178, 170], + lightskyblue: [135, 206, 250], + lightslategray: [119, 136, 153], + lightslategrey: [119, 136, 153], + lightsteelblue: [176, 196, 222], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + limegreen: [50, 205, 50], + linen: [250, 240, 230], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + mediumaquamarine: [102, 205, 170], + mediumblue: [0, 0, 205], + mediumorchid: [186, 85, 211], + mediumpurple: [147, 112, 219], + mediumseagreen: [60, 179, 113], + mediumslateblue: [123, 104, 238], + mediumspringgreen: [0, 250, 154], + mediumturquoise: [72, 209, 204], + mediumvioletred: [199, 21, 133], + midnightblue: [25, 25, 112], + mintcream: [245, 255, 250], + mistyrose: [255, 228, 225], + moccasin: [255, 228, 181], + navajowhite: [255, 222, 173], + navy: [0, 0, 128], + oldlace: [253, 245, 230], + olive: [128, 128, 0], + olivedrab: [107, 142, 35], + orange: [255, 165, 0], + orangered: [255, 69, 0], + orchid: [218, 112, 214], + palegoldenrod: [238, 232, 170], + palegreen: [152, 251, 152], + paleturquoise: [175, 238, 238], + palevioletred: [219, 112, 147], + papayawhip: [255, 239, 213], + peachpuff: [255, 218, 185], + peru: [205, 133, 63], + pink: [255, 192, 203], + plum: [221, 160, 221], + powderblue: [176, 224, 230], + purple: [128, 0, 128], + rebeccapurple: [102, 51, 153], + red: [255, 0, 0], + rosybrown: [188, 143, 143], + royalblue: [65, 105, 225], + saddlebrown: [139, 69, 19], + salmon: [250, 128, 114], + sandybrown: [244, 164, 96], + seagreen: [46, 139, 87], + seashell: [255, 245, 238], + sienna: [160, 82, 45], + silver: [192, 192, 192], + skyblue: [135, 206, 235], + slateblue: [106, 90, 205], + slategray: [112, 128, 144], + slategrey: [112, 128, 144], + snow: [255, 250, 250], + springgreen: [0, 255, 127], + steelblue: [70, 130, 180], + tan: [210, 180, 140], + teal: [0, 128, 128], + thistle: [216, 191, 216], + tomato: [255, 99, 71], + turquoise: [64, 224, 208], + violet: [238, 130, 238], + wheat: [245, 222, 179], + white: [255, 255, 255], + whitesmoke: [245, 245, 245], + yellow: [255, 255, 0], + yellowgreen: [154, 205, 50] +} +// #endregion + + +export const parseCSSColor = (str, debug=false) => { + if (typeof str !== 'string') { + console.error(`parseCSSColor: expected a string found ${typeof str}`,str); + return null; + } + + const hex = hex_pattern.exec(str); + if (hex) { + if (debug){ + console.debug('parseCSSColor: hex', hex); + } + return getHEX(hex[0]); + } + + const hsl = hsl4_pattern.exec(str) || hsl3_pattern.exec(str); + if (hsl) { + if (debug){ + console.debug('parseCSSColor: hsl', hsl); + } + return getHSL(hsl); + } + + const rgb = + rgb4_pattern.exec(str) || + rgb3_pattern.exec(str) + if (rgb) { + if (debug){ + console.debug('parseCSSColor: rgb', rgb); + } + return getRGB(rgb); + } + + if (transparent_pattern.exec(str)) { + if (debug){ + console.debug('parseCSSColor: transparent'); + } + return getRGB([null, 0, 0, 0, 0]); + } + + const cn = colorName[str.toLowerCase()]; + if (cn) { + if (debug){ + console.debug('parseCSSColor: colorName', cn); + } + return getRGB([null, cn[0], cn[1], cn[2], 1]); + } + + console.error('parseCSSColor: unknown color', str); + return null; +}; + +export default parseCSSColor; + diff --git a/py/color_picker.py b/py/color_picker.py new file mode 100644 index 0000000..736c15a --- /dev/null +++ b/py/color_picker.py @@ -0,0 +1,47 @@ + +mode_list = ['HEX', 'DEC'] + +def hex_to_dec(inhex): + rval = inhex[1:3] + gval = inhex[3:5] + bval = inhex[5:] + rgbval = (int(rval, 16), int(gval, 16), int(bval, 16)) + return rgbval + +class ColorPicker: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "color": ("COLOR", {"default": "white"},), + "mode": (mode_list,), # 输出模� + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("value",) + FUNCTION = 'picker' + CATEGORY = '😺dzNodes' + OUTPUT_NODE = True + + def picker(self, color, mode,): + ret = color + if mode == 'DEC': + ret = hex_to_dec(color) + return (ret,) + + +NODE_CLASS_MAPPINGS = { + "LayerStyle_ColorPicker": ColorPicker +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle_ColorPicker": "LayerStyle: ColorPicker" +} \ No newline at end of file