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.
+
+
+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¡£
+
+
+½ÚµãÑ¡Ïî˵Ã÷£º
+* 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 0000000..af1530c
Binary files /dev/null and b/image/color_picker.png differ
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