mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 14:54:37 +00:00
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:
@@ -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 |
@@ -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 () => {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
50
tests-ui/tests/colorUtil.test.ts
Normal file
50
tests-ui/tests/colorUtil.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user