Add node opacity setting (#909)

* Add node opacity setting

* Add colorUtil unit test

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
bymyself
2024-09-21 23:18:38 -07:00
committed by GitHub
parent ea0f74a9f6
commit 326e0748c0
10 changed files with 161 additions and 1 deletions

View File

@@ -150,4 +150,20 @@ test.describe('Color Palette', () => {
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
})
test('Can change node opacity setting', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.page.waitForTimeout(128)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.waitForTimeout(128)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -42,6 +42,9 @@ import {
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import { useCanvasStore } from '@/stores/graphStore'
import { applyOpacity } from '@/utils/colorUtil'
import { getColorPalette } from '@/extensions/core/colorPalette'
import { debounce } from 'lodash'
const emit = defineEmits(['ready'])
const canvasRef = ref<HTMLCanvasElement | null>(null)
@@ -90,6 +93,24 @@ watchEffect(() => {
})
})
const updateNodeOpacity = (nodeOpacity: number) => {
const colorPalette = getColorPalette()
if (!canvasStore.canvas) return
const nodeBgColor = colorPalette?.colors?.litegraph_base?.NODE_DEFAULT_BGCOLOR
if (nodeBgColor) {
LiteGraph.NODE_DEFAULT_BGCOLOR = applyOpacity(nodeBgColor, nodeOpacity)
}
}
const debouncedUpdateNodeOpacity = debounce(updateNodeOpacity, 128)
watchEffect(() => {
const nodeOpacity = settingStore.get('Comfy.Node.Opacity')
debouncedUpdateNodeOpacity(nodeOpacity)
})
let dropTargetCleanup = () => {}
onMounted(async () => {

View File

@@ -2,6 +2,8 @@ import { app } from '../../scripts/app'
import { $el } from '../../scripts/ui'
import type { ColorPalettes, Palette } from '@/types/colorPalette'
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
import { applyOpacity } from '@/utils/colorUtil'
import { useSettingStore } from '@/stores/settingStore'
// Manage color palettes
@@ -682,7 +684,13 @@ app.registerExtension({
colorPalette.colors.litegraph_base.hasOwnProperty(key) &&
LiteGraph.hasOwnProperty(key)
) {
LiteGraph[key] = colorPalette.colors.litegraph_base[key]
LiteGraph[key] =
key === 'NODE_DEFAULT_BGCOLOR'
? applyOpacity(
colorPalette.colors.litegraph_base[key],
useSettingStore().get('Comfy.Node.Opacity')
)
: colorPalette.colors.litegraph_base[key]
}
}
}

View File

@@ -139,6 +139,17 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'boolean',
defaultValue: true
},
{
id: 'Comfy.Node.Opacity',
name: 'Node opacity',
type: 'slider',
defaultValue: 1,
attrs: {
min: 0.01,
max: 1,
step: 0.01
}
},
{
id: 'Comfy.Workflow.ShowMissingNodesWarning',
name: 'Show missing nodes warning',

View File

@@ -1,5 +1,6 @@
type RGB = { r: number; g: number; b: number }
type HSL = { h: number; s: number; l: number }
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
function rgbToHsl({ r, g, b }: RGB): HSL {
r /= 255
@@ -92,6 +93,16 @@ function rgbToHex({ r, g, b }: RGB): string {
)
}
function identifyColorFormat(color: string): ColorFormat | null {
if (!color) return null
if (color.startsWith('#')) return 'hex'
if (/^rgba?\(\d+,\s*\d+,\s*\d+/.test(color))
return color.includes('rgba') ? 'rgba' : 'rgb'
if (/^hsla?\(\d+(\.\d+)?,\s*\d+(\.\d+)?%,\s*\d+(\.\d+)?%/.test(color))
return color.includes('hsla') ? 'hsla' : 'hsl'
return null
}
export function lightenColor(hex: string, amount: number): string {
let rgb = hexToRgb(hex)
const hsl = rgbToHsl(rgb)
@@ -99,3 +110,46 @@ export function lightenColor(hex: string, amount: number): string {
rgb = hslToRgb(hsl)
return rgbToHex(rgb)
}
export function applyOpacity(color: string, opacity: number): string {
const colorFormat = identifyColorFormat(color)
if (!colorFormat) {
console.warn(
`Unsupported color format in user color palette for color: ${color}`
)
return color
}
const clampedOpacity = Math.max(0, Math.min(1, opacity))
switch (colorFormat) {
case 'hex': {
const { r, g, b } = hexToRgb(color)
if (isNaN(r) || isNaN(g) || isNaN(b)) {
return color
}
return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`
}
case 'rgb': {
return color.replace('rgb', 'rgba').replace(')', `, ${clampedOpacity})`)
}
case 'rgba': {
return color.replace(
/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,[^)]+\)/,
`rgba($1, $2, $3, ${clampedOpacity})`
)
}
case 'hsl': {
return color.replace('hsl', 'hsla').replace(')', `, ${clampedOpacity})`)
}
case 'hsla': {
return color.replace(
/hsla\(\s*(\d+)\s*,\s*(\d+(?:\.\d+)?)%\s*,\s*(\d+(?:\.\d+)?)%\s*,[^)]+\)/,
`hsla($1, $2%, $3%, ${clampedOpacity})`
)
}
default:
return color
}
}

View File

@@ -0,0 +1,50 @@
import { applyOpacity } from '@/utils/colorUtil'
describe('colorUtil - applyOpacity', () => {
// Same color in various formats
const solarized = {
hex: '#073642',
rgb: 'rgb(7, 54, 66)',
rgba: 'rgba(7, 54, 66, 1)',
hsl: 'hsl(192, 80.80%, 14.30%)',
hsla: 'hsla(192, 80.80%, 14.30%, 1)'
}
const opacity = 0.5
it('applies opacity consistently to hex, rgb, and rgba formats', () => {
const hexResult = applyOpacity(solarized.hex, opacity)
const rgbResult = applyOpacity(solarized.rgb, opacity)
const rgbaResult = applyOpacity(solarized.rgba, opacity)
expect(hexResult).toBe(rgbResult)
expect(rgbResult).toBe(rgbaResult)
})
it('applies opacity consistently to hsl and hsla formats', () => {
const hslResult = applyOpacity(solarized.hsl, opacity)
const hslaResult = applyOpacity(solarized.hsla, opacity)
expect(hslResult).toBe(hslaResult)
})
it('returns the original value for invalid color formats', () => {
const invalidColors = [
'#GGGGGG', // Invalid hex code (non-hex characters)
'rgb(300, -10, 256)', // Invalid RGB values (out of range)
'xyz(255, 255, 255)', // Unsupported format
'rgba(255, 255, 255)', // Missing alpha in RGBA
'hsl(100, 50, 50%)' // Missing percentage sign for saturation
]
invalidColors.forEach((color) => {
const result = applyOpacity(color, opacity)
expect(result).toBe(color)
})
})
it('returns the original value for null or undefined inputs', () => {
expect(applyOpacity(null, opacity)).toBe(null)
expect(applyOpacity(undefined, opacity)).toBe(undefined)
})
})