update web extensions

This commit is contained in:
chflame
2024-02-27 20:47:46 +08:00
parent 15e32b0e7b
commit b56fc1a1ea
8 changed files with 445 additions and 665 deletions
+8 -2
View File
@@ -12,12 +12,18 @@ 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")
"web" + os.sep + "extensions" + os.sep + "mtb")
javascript_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mtb")
outdate_file_list = ['comfy_shared.js', 'debug.js', 'mtb_widgets.js', 'parse-css.js', 'dz_widgets.js']
if not os.path.exists(extentions_folder):
log('Making the "web\extensions\dzNodes" folder')
log('Making the "web\extensions\mtb" folder')
os.mkdir(extentions_folder)
else:
for i in outdate_file_list:
outdate_file = os.path.join(extentions_folder, i)
if os.path.exists(outdate_file):
os.remove(outdate_file)
result = filecmp.dircmp(javascript_folder, extentions_folder)
+14 -159
View File
@@ -1,6 +1,5 @@
/**
* File: comfy_shared.js
* Project: comfy_mtb
* File: dz_comfy_shared.js
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
@@ -9,70 +8,8 @@
import { app } from '../../scripts/app.js'
// - crude uuid
export function makeUUID() {
let dt = new Date().getTime()
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (dt + Math.random() * 16) % 16 | 0
dt = Math.floor(dt / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
return uuid
}
//- local storage manager
export class LocalStorageManager {
constructor(namespace) {
this.namespace = namespace;
}
_namespacedKey(key) {
return `${this.namespace}:${key}`;
}
set(key, value) {
const serializedValue = JSON.stringify(value);
localStorage.setItem(this._namespacedKey(key), serializedValue);
}
get(key, default_val = null) {
const value = localStorage.getItem(this._namespacedKey(key));
return value ? JSON.parse(value) : default_val;
}
remove(key) {
localStorage.removeItem(this._namespacedKey(key));
}
clear() {
Object.keys(localStorage)
.filter(k => k.startsWith(this.namespace + ':'))
.forEach(k => localStorage.removeItem(k));
}
}
// - log utilities
function createLogger(emoji, color, consoleMethod = 'log') {
return function (message, ...args) {
if (window.MTB?.DEBUG) {
console[consoleMethod](
`%c${emoji} ${message}`,
`color: ${color};`,
...args
)
}
}
}
export const infoLogger = createLogger('ℹ️', 'yellow')
export const warnLogger = createLogger('⚠️', 'orange', 'warn')
export const errorLogger = createLogger('🔥', 'red', 'error')
export const successLogger = createLogger('✅', 'green')
export const log = (...args) => {
if (window.MTB?.DEBUG) {
if (window.DZ?.DEBUG) {
console.debug(...args)
}
}
@@ -154,38 +91,14 @@ export function getWidgetType(config) {
}
return { type, linkType }
}
export const setupDynamicConnections = (nodeType, prefix, inputType) => {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined
this.addInput(`${prefix}_1`, inputType)
return r
}
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (
type,
index,
connected,
link_info
) {
const r = onConnectionsChange
? onConnectionsChange.apply(this, arguments)
: undefined
dynamic_connection(this, index, connected, `${prefix}_`, inputType)
}
}
export const dynamic_connection = (
node,
index,
connected,
connectionPrefix = 'input_',
connectionType = 'PSDLAYER',
nameArray = []
connectionType = 'PSDLAYER'
) => {
if (!node.inputs[index].name.startsWith(connectionPrefix)) {
return
}
// remove all non connected inputs
if (!connected && node.inputs.length > 1) {
log(`Removing input ${index} (${node.inputs[index].name})`)
@@ -200,46 +113,25 @@ export const dynamic_connection = (
// make inputs sequential again
for (let i = 0; i < node.inputs.length; i++) {
const name =
i < nameArray.length ? nameArray[i] : `${connectionPrefix}${i + 1}`
node.inputs[i].label = name
node.inputs[i].name = name
node.inputs[i].label = `${connectionPrefix}${i + 1}`
}
}
// add an extra input
if (node.inputs[node.inputs.length - 1].link != undefined) {
const nextIndex = node.inputs.length
const name =
nextIndex < nameArray.length
? nameArray[nextIndex]
: `${connectionPrefix}${nextIndex + 1}`
log(
`Adding input ${node.inputs.length + 1} (${connectionPrefix}${
node.inputs.length + 1
})`
)
log(`Adding input ${nextIndex + 1} (${name})`)
node.addInput(name, connectionType)
node.addInput(
`${connectionPrefix}${node.inputs.length + 1}`,
connectionType
)
}
}
export function calculateTotalChildrenHeight(parentElement) {
let totalHeight = 0
for (const child of parentElement.children) {
const style = window.getComputedStyle(child)
// Get height as an integer (without 'px')
const height = parseInt(style.height, 10)
// Get vertical margin as integers
const marginTop = parseInt(style.marginTop, 10)
const marginBottom = parseInt(style.marginBottom, 10)
// Sum up height and vertical margins
totalHeight += height + marginTop + marginBottom
}
return totalHeight
}
/**
* Appends a callback to the extra menu options of a given node type.
* @param {*} nodeType
@@ -410,48 +302,11 @@ function getBrightness(rgbObj) {
(parseInt(rgbObj[0]) * 299 +
parseInt(rgbObj[1]) * 587 +
parseInt(rgbObj[2]) * 114) /
1000
1000
)
}
//- HTML / CSS UTILS
export const loadScript = (
FILE_URL,
async = true,
type = 'text/javascript'
) => {
return new Promise((resolve, reject) => {
try {
// Check if the script already exists
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
if (existingScript) {
resolve({ status: true, message: 'Script already loaded' })
return
}
const scriptEle = document.createElement('script')
scriptEle.type = type
scriptEle.async = async
scriptEle.src = FILE_URL
scriptEle.addEventListener('load', (ev) => {
resolve({ status: true })
})
scriptEle.addEventListener('error', (ev) => {
reject({
status: false,
message: `Failed to load the script ${FILE_URL}`,
})
})
document.body.appendChild(scriptEle)
} catch (error) {
reject(error)
}
})
}
export function defineClass(className, classStyles) {
const styleSheets = document.styleSheets
+10 -23
View File
@@ -1,6 +1,6 @@
/**
* File: debug.js
* Project: comfy_mtb
* Project: comfy_DZ
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
@@ -9,9 +9,9 @@
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'
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...
@@ -24,18 +24,9 @@ function escapeHtml(unsafe) {
.replace(/'/g, '&#039;')
}
app.registerExtension({
name: 'mtb.Debug',
name: 'DZ.Debug',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === 'Debug (mtb)') {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined
this.addInput(`anything_1`, '*')
return r
}
if (nodeData.name === 'Debug (DZ)') {
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (
type,
@@ -75,18 +66,15 @@ app.registerExtension({
// const pos = this.widgets.findIndex((w) => w.name === "anything_1");
// if (pos !== -1) {
for (let i = 0; i < this.widgets.length; i++) {
if (this.widgets[i].name !== 'output_to_console') {
this.widgets[i].onRemoved?.()
}
this.widgets[i].onRemoved?.()
}
this.widgets.length = 1
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))
DZWidgets.DEBUG_STRING(`${prefix}_${widgetI}`, escapeHtml(txt))
)
w.parent = this
widgetI++
@@ -95,7 +83,7 @@ app.registerExtension({
if (message.b64_images) {
for (const img of message.b64_images) {
const w = this.addCustomWidget(
MtbWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img)
DZWidgets.DEBUG_IMG(`${prefix}_${widgetI}`, img)
)
w.parent = this
widgetI++
@@ -112,7 +100,6 @@ app.registerExtension({
if (this.widgets[y].canvas) {
this.widgets[y].canvas.remove()
}
shared.cleanupNode(this)
this.widgets[y].onRemoved?.()
}
}
+51 -119
View File
@@ -1,20 +1,18 @@
/**
* File: mtb_widgets.js
* Project: comfy_mtb
* File: DZ_widgets.js
* Project: comfy_DZ
* Author: Mel Massadian
*
* Copyright (c) 2023 Mel Massadian
*
*/
// TODO: Use the builtin addDOMWidget everywhere appropriate
import { app } from '../../scripts/app.js'
import { api } from '../../scripts/api.js'
import parseCss from './extern/parse-css.js'
import * as shared from './comfy_shared.js'
import { log } from './comfy_shared.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']
@@ -48,7 +46,7 @@ const calculateTextDimensions = (ctx, value, width, fontSize = 16) => {
return { textHeight, maxLineWidth }
}
export const MtbWidgets = {
export const DZWidgets = {
BBOX: (key, val) => {
/** @type {import("./types/litegraph").IWidget} */
const widget = {
@@ -195,7 +193,7 @@ export const MtbWidgets = {
try {
//solve the equation if possible
v = eval(v)
} catch (e) { }
} catch (e) {}
}
this.value = Number(v)
shared.inner_value_change(this, this.value, event)
@@ -399,18 +397,18 @@ export const MtbWidgets = {
/**
* @returns {import("./types/comfy").ComfyExtension} extension
*/
const mtb_widgets = {
name: 'mtb.widgets',
const DZ_widgets = {
name: 'DZ.widgets',
init: async () => {
log('Registering mtb.widgets')
log('Registering DZ.widgets')
try {
const res = await api.fetchApi('/mtb/debug')
const res = await api.fetchApi('/DZ/debug')
const msg = await res.json()
if (!window.MTB) {
window.MTB = {}
if (!window.DZ) {
window.DZ = {}
}
window.MTB.DEBUG = msg.enabled
window.DZ.DEBUG = msg.enabled
} catch (e) {
console.error('Error:', error)
}
@@ -418,8 +416,8 @@ const mtb_widgets = {
setup: () => {
app.ui.settings.addSetting({
id: 'mtb.Debug.enabled',
name: '[⚡mtb] Enable Debug (py and js)',
id: 'DZ.Debug.enabled',
name: '[DZ] Enable Debug (py and js)',
type: 'boolean',
defaultValue: false,
@@ -434,18 +432,18 @@ const mtb_widgets = {
if (value) {
console.log('Enabled DEBUG mode')
}
if (!window.MTB) {
window.MTB = {}
if (!window.DZ) {
window.DZ = {}
}
window.MTB.DEBUG = value
window.DZ.DEBUG = value
await api
.fetchApi('/mtb/debug', {
.fetchApi('/DZ/debug', {
method: 'POST',
body: JSON.stringify({
enabled: value,
}),
})
.then((response) => { })
.then((response) => {})
.catch((error) => {
console.error('Error:', error)
})
@@ -460,7 +458,7 @@ const mtb_widgets = {
return {
widget: node.addCustomWidget(
MtbWidgets.BOOL(inputName, inputData[1]?.default || false)
DZWidgets.BOOL(inputName, inputData[1]?.default || false)
),
minWidth: 150,
minHeight: 30,
@@ -471,7 +469,7 @@ const mtb_widgets = {
console.debug('Registering color')
return {
widget: node.addCustomWidget(
MtbWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000')
DZWidgets.COLOR(inputName, inputData[1]?.default || '#ff0000')
),
minWidth: 150,
minHeight: 30,
@@ -480,7 +478,7 @@ const mtb_widgets = {
// BBOX: (node, inputName, inputData, app) => {
// console.debug("Registering bbox")
// return {
// widget: node.addCustomWidget(MtbWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])),
// widget: node.addCustomWidget(DZWidgets.BBOX(inputName, inputData[1]?.default || [0, 0, 0, 0])),
// minWidth: 150,
// minHeight: 30,
// }
@@ -563,13 +561,9 @@ const mtb_widgets = {
}
}
if (!nodeData.name.endsWith('(mtb)')) {
return
}
//- Extending Python Nodes
switch (nodeData.name) {
case 'Psd Save (mtb)': {
case 'Psd Save (DZ)': {
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (
type,
@@ -586,7 +580,7 @@ const mtb_widgets = {
break
}
//TODO: remove this non sense
case 'Get Batch From History (mtb)': {
case 'Get Batch From History (DZ)': {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
@@ -611,8 +605,8 @@ const mtb_widgets = {
break
}
case 'Save Gif (mtb)':
case 'Save Animated Image (mtb)': {
case 'Save Gif (DZ)':
case 'Save Animated Image (DZ)': {
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
const prefix = 'anything_'
@@ -650,7 +644,7 @@ const mtb_widgets = {
let i = 0
for (const img of imgURLs) {
const w = this.addCustomWidget(
MtbWidgets.DEBUG_IMG(`${prefix}_${i}`, img)
DZWidgets.DEBUG_IMG(`${prefix}_${i}`, img)
)
w.parent = this
i++
@@ -668,7 +662,7 @@ const mtb_widgets = {
break
}
case 'Animation Builder (mtb)': {
case 'Animation Builder (DZ)': {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
@@ -693,12 +687,12 @@ const mtb_widgets = {
raw_iteration._value = 0
const value_preview = this.addCustomWidget(
MtbWidgets['DEBUG_STRING']('value_preview', 'Idle')
DZWidgets['DEBUG_STRING']('value_preview', 'Idle')
)
value_preview.parent = this
const loop_preview = this.addCustomWidget(
MtbWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle')
DZWidgets['DEBUG_STRING']('loop_preview', 'Iteration: Idle')
)
loop_preview.parent = this
@@ -722,8 +716,9 @@ const mtb_widgets = {
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
window.DZ?.notify?.(
`Started a queue of ${total_frames.value} frames (for ${
loop_count.value
} loop, so ${total_frames.value * loop_count.value})`,
5000
)
@@ -738,14 +733,16 @@ const mtb_widgets = {
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}`
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
}`
loop_preview.value = `current loop: ${raw_loop.value + 1}/${
loop_count.value
}`
}
}
@@ -754,7 +751,7 @@ const mtb_widgets = {
break
}
case 'Text Encore Frames (mtb)': {
case 'Text Encore Frames (DZ)': {
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (
type,
@@ -771,7 +768,7 @@ const mtb_widgets = {
}
break
}
case 'Interpolate Clip Sequential (mtb)': {
case 'Interpolate Clip Sequential (DZ)': {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
@@ -805,7 +802,7 @@ const mtb_widgets = {
}
break
}
case 'Styles Loader (mtb)': {
case 'Styles Loader (DZ)': {
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions
nodeType.prototype.getExtraMenuOptions = function (_, options) {
const r = origGetExtraMenuOptions
@@ -814,7 +811,7 @@ const mtb_widgets = {
const getStyle = async (node) => {
try {
const getStyles = await api.fetchApi('/mtb/actions', {
const getStyles = await api.fetchApi('/DZ/actions', {
method: 'POST',
body: JSON.stringify({
name: 'getStyles',
@@ -838,7 +835,7 @@ const mtb_widgets = {
const style = await getStyle(this)
if (style && style.length >= 1) {
if (style[0]) {
window.MTB?.notify?.(
window.DZ?.notify?.(
`Extracted positive from ${this.widgets[0].value}`
)
const tn = LiteGraph.createNode('Text box')
@@ -846,7 +843,7 @@ const mtb_widgets = {
tn.title = `${this.widgets[0].value} (Positive)`
tn.widgets[0].value = style[0]
} else {
window.MTB?.notify?.(
window.DZ?.notify?.(
`No positive to extract for ${this.widgets[0].value}`
)
}
@@ -859,7 +856,7 @@ const mtb_widgets = {
const style = await getStyle(this)
if (style && style.length >= 2) {
if (style[1]) {
window.MTB?.notify?.(
window.DZ?.notify?.(
`Extracted negative from ${this.widgets[0].value}`
)
const tn = LiteGraph.createNode('Text box')
@@ -867,7 +864,7 @@ const mtb_widgets = {
tn.title = `${this.widgets[0].value} (Negative)`
tn.widgets[0].value = style[1]
} else {
window.MTB.notify(
window.DZ.notify(
`No negative to extract for ${this.widgets[0].value}`
)
}
@@ -880,72 +877,7 @@ const mtb_widgets = {
break
}
case 'Add To Playlist (mtb)': {
shared.setupDynamicConnections(nodeType, 'video', 'VIDEO')
break
}
case 'Stack Images (mtb)':
case 'Concat Images (mtb)': {
shared.setupDynamicConnections(nodeType, 'image', 'IMAGE')
break
}
case 'Batch Float Assemble (mtb)':
case 'Plot Batch Float (mtb)': {
shared.setupDynamicConnections(nodeType, 'floats', 'FLOATS')
break
}
case 'Batch Merge (mtb)': {
shared.setupDynamicConnections(nodeType, 'batches', 'IMAGE')
break
}
// TODO: remove this, recommend pythongoss's version that is much better
case 'Math Expression (mtb)': {
const onNodeCreated = nodeType.prototype.onNodeCreated
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined
this.addInput(`x`, '*')
return r
}
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, 'var_', '*', [
'x',
'y',
'z',
])
//- 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 = `number_${index + 1}`
}
}
break
}
case 'Save Tensors (mtb)': {
case 'Save Tensors (DZ)': {
const onDrawBackground = nodeType.prototype.onDrawBackground
nodeType.prototype.onDrawBackground = function (ctx, canvas) {
const r = onDrawBackground
@@ -973,4 +905,4 @@ const mtb_widgets = {
},
}
app.registerExtension(mtb_widgets)
app.registerExtension(DZ_widgets)
+362 -362
View File
@@ -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;
Binary file not shown.
Binary file not shown.
Binary file not shown.