diff --git a/js/dz_comfy_shared.js b/js/dz_comfy_shared.js index 38bf6b4..c09aeb8 100644 --- a/js/dz_comfy_shared.js +++ b/js/dz_comfy_shared.js @@ -1,342 +1,341 @@ -/** - * File: comfy_shared.js - * Project: comfy_dz - * Author: Mel Massadian - * - * Copyright (c) 2023 Mel Massadian - * - */ - -import { app } from '../../scripts/app.js' - -export const log = (...args) => { - if (window.DZ?.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) - } - } -} +/** + * File: dz_comfy_shared.js + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' + +export const log = (...args) => { + if (window.DZ?.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/dz_debug.js b/js/dz_debug.js index 33d1b58..1fae3d1 100644 --- a/js/dz_debug.js +++ b/js/dz_debug.js @@ -1,109 +1,109 @@ -/** - * File: debug.js - * Project: comfy_dz - * 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 './dz_widgets.js' - -// TODO: respect inputs order... - -function escapeHtml(unsafe) { - return unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} -app.registerExtension({ - name: 'dz.Debug', - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === 'Debug (dz)') { - 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?.() - } - } - } - } - }, -}) +/** + * File: debug.js + * Project: comfy_DZ + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' + +import * as shared from './dz_comfy_shared.js' +import { log } from './dz_comfy_shared.js' +import { DZWidgets } from './dz_DZ_widgets.js' + +// TODO: respect inputs order... + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +app.registerExtension({ + name: 'DZ.Debug', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === 'Debug (DZ)') { + 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( + DZWidgets.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( + DZWidgets.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/dz_mtb_widgets.js b/js/dz_mtb_widgets.js new file mode 100644 index 0000000..a9e15a4 --- /dev/null +++ b/js/dz_mtb_widgets.js @@ -0,0 +1,908 @@ +/** + * File: DZ_widgets.js + * Project: comfy_DZ + * Author: Mel Massadian + * + * Copyright (c) 2023 Mel Massadian + * + */ + +import { app } from '../../scripts/app.js' +import { api } from '../../scripts/api.js' + +import parseCss from './dz_parse-css.js' +import * as shared from './dz_comfy_shared.js' +import { log } from './dz_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 DZWidgets = { + 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 DZ_widgets = { + name: 'DZ.widgets', + + init: async () => { + log('Registering DZ.widgets') + try { + const res = await api.fetchApi('/DZ/debug') + const msg = await res.json() + if (!window.DZ) { + window.DZ = {} + } + window.DZ.DEBUG = msg.enabled + } catch (e) { + console.error('Error:', error) + } + }, + + setup: () => { + app.ui.settings.addSetting({ + id: 'DZ.Debug.enabled', + name: '[DZ] 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.DZ) { + window.DZ = {} + } + window.DZ.DEBUG = value + await api + .fetchApi('/DZ/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( + DZWidgets.BOOL(inputName, inputData[1]?.default || false) + ), + minWidth: 150, + minHeight: 30, + } + }, + + COLOR: (node, inputName, inputData, app) => { + console.debug('Registering color') + return { + widget: node.addCustomWidget( + DZWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000') + ), + minWidth: 150, + minHeight: 30, + } + }, + // BBOX: (node, inputName, inputData, app) => { + // console.debug("Registering bbox") + // return { + // widget: node.addCustomWidget(DZWidgets.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 (DZ)': { + 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 (DZ)': { + 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 (DZ)': + case 'Save Animated Image (DZ)': { + 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( + DZWidgets.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 (DZ)': { + 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( + DZWidgets['DEBUG_STRING']('value_preview', 'Idle') + ) + value_preview.parent = this + + const loop_preview = this.addCustomWidget( + DZWidgets['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.DZ?.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 (DZ)': { + 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 (DZ)': { + 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 (DZ)': { + 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('/DZ/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.DZ?.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.DZ?.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.DZ?.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.DZ.notify( + `No negative to extract for ${this.widgets[0].value}` + ) + } + } + }, + }, + ] + options.push(...extracters) + } + + break + } + case 'Save Tensors (DZ)': { + 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(DZ_widgets) diff --git a/js/dz_parse-css.js b/js/dz_parse-css.js index b77811f..1ae53b0 100644 --- a/js/dz_parse-css.js +++ b/js/dz_parse-css.js @@ -1,362 +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; - + +// #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; +