mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
feat(widgets): normalize color widget values to #hex across inputs (hex/rgb/hsb); always emit with leading # using colorUtil conversions
This commit is contained in:
@@ -83,6 +83,46 @@ describe('WidgetColorPicker Value Binding', () => {
|
|||||||
expect(emitted).toBeDefined()
|
expect(emitted).toBeDefined()
|
||||||
expect(emitted![0]).toContain('#ff00ff')
|
expect(emitted![0]).toContain('#ff00ff')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('normalizes bare hex without # to #hex on emit', async () => {
|
||||||
|
const widget = createMockWidget('ff0000')
|
||||||
|
const wrapper = mountComponent(widget, 'ff0000')
|
||||||
|
|
||||||
|
const emitted = await setColorPickerValue(wrapper, '00ff00')
|
||||||
|
expect(emitted).toBeDefined()
|
||||||
|
expect(emitted![0]).toContain('#00ff00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes rgb() strings to #hex on emit', async () => {
|
||||||
|
const widget = createMockWidget('#000000')
|
||||||
|
const wrapper = mountComponent(widget, '#000000')
|
||||||
|
|
||||||
|
const emitted = await setColorPickerValue(wrapper, 'rgb(255, 0, 0)')
|
||||||
|
expect(emitted).toBeDefined()
|
||||||
|
expect(emitted![0]).toContain('#ff0000')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes hsb() strings to #hex on emit', async () => {
|
||||||
|
const widget = createMockWidget('#000000', { format: 'hsb' })
|
||||||
|
const wrapper = mountComponent(widget, '#000000')
|
||||||
|
|
||||||
|
const emitted = await setColorPickerValue(wrapper, 'hsb(120, 100, 100)')
|
||||||
|
expect(emitted).toBeDefined()
|
||||||
|
expect(emitted![0]).toContain('#00ff00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes HSB object values to #hex on emit', async () => {
|
||||||
|
const widget = createMockWidget('#000000', { format: 'hsb' })
|
||||||
|
const wrapper = mountComponent(widget, '#000000')
|
||||||
|
|
||||||
|
const emitted = await setColorPickerValue(wrapper, {
|
||||||
|
h: 240,
|
||||||
|
s: 100,
|
||||||
|
b: 100
|
||||||
|
} as any)
|
||||||
|
expect(emitted).toBeDefined()
|
||||||
|
expect(emitted![0]).toContain('#0000ff')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Component Rendering', () => {
|
describe('Component Rendering', () => {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
import { hsbToRgb, parseToRgb, rgbToHex } from '@/utils/colorUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import {
|
import {
|
||||||
PANEL_EXCLUDED_PROPS,
|
PANEL_EXCLUDED_PROPS,
|
||||||
@@ -49,11 +50,58 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Use the composable for consistent widget value handling
|
// Use the composable for consistent widget value handling
|
||||||
|
function normalizeToHexWithHash(value: unknown): string {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const raw = value.trim()
|
||||||
|
// Bare hex without '#'
|
||||||
|
if (/^[0-9a-fA-F]{3}$/.test(raw) || /^[0-9a-fA-F]{6}$/.test(raw)) {
|
||||||
|
return `#${raw.toLowerCase()}`
|
||||||
|
}
|
||||||
|
// If starts with '#', ensure lower-case and valid length
|
||||||
|
if (raw.startsWith('#')) {
|
||||||
|
const hex = raw.toLowerCase()
|
||||||
|
if (hex.length === 4 || hex.length === 7) return hex
|
||||||
|
// Fallback: attempt parse via RGB and re-encode
|
||||||
|
}
|
||||||
|
// rgb(), rgba(), hsl(), hsla()
|
||||||
|
if (/^(rgb|rgba|hsl|hsla)\(/i.test(raw)) {
|
||||||
|
const rgb = parseToRgb(raw)
|
||||||
|
return rgbToHex(rgb).toLowerCase()
|
||||||
|
}
|
||||||
|
// hsb(h,s,b)
|
||||||
|
if (/^hsb\(/i.test(raw)) {
|
||||||
|
const nums = raw.match(/\d+(?:\.\d+)?/g)?.map(Number) || []
|
||||||
|
if (nums.length >= 3) {
|
||||||
|
const rgb = hsbToRgb({ h: nums[0], s: nums[1], b: nums[2] })
|
||||||
|
return rgbToHex(rgb).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// HSB object from PrimeVue
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
'h' in (value as any) &&
|
||||||
|
's' in (value as any) &&
|
||||||
|
('b' in (value as any) || 'v' in (value as any))
|
||||||
|
) {
|
||||||
|
const h = Number((value as any).h)
|
||||||
|
const s = Number((value as any).s)
|
||||||
|
const b = Number((value as any).b ?? (value as any).v)
|
||||||
|
const rgb = hsbToRgb({ h, s, b })
|
||||||
|
return rgbToHex(rgb).toLowerCase()
|
||||||
|
}
|
||||||
|
// Fallback to default black
|
||||||
|
return '#000000'
|
||||||
|
}
|
||||||
|
|
||||||
const { localValue, onChange } = useWidgetValue({
|
const { localValue, onChange } = useWidgetValue({
|
||||||
widget: props.widget,
|
widget: props.widget,
|
||||||
modelValue: props.modelValue,
|
// Normalize initial model value to ensure leading '#'
|
||||||
|
modelValue: normalizeToHexWithHash(props.modelValue),
|
||||||
defaultValue: '#000000',
|
defaultValue: '#000000',
|
||||||
emit
|
emit,
|
||||||
|
transform: (val: unknown) => normalizeToHexWithHash(val)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ColorPicker specific excluded props include panel/overlay classes
|
// ColorPicker specific excluded props include panel/overlay classes
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { memoize } from 'es-toolkit/compat'
|
import { memoize } from 'es-toolkit/compat'
|
||||||
|
|
||||||
type RGB = { r: number; g: number; b: number }
|
export type RGB = { r: number; g: number; b: number }
|
||||||
|
export type HSB = { h: number; s: number; b: number }
|
||||||
type HSL = { h: number; s: number; l: number }
|
type HSL = { h: number; s: number; l: number }
|
||||||
type HSLA = { h: number; s: number; l: number; a: number }
|
type HSLA = { h: number; s: number; l: number; a: number }
|
||||||
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
|
||||||
@@ -59,6 +60,61 @@ export function hexToRgb(hex: string): RGB {
|
|||||||
return { r, g, b }
|
return { r, g, b }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rgbToHex({ r, g, b }: RGB): string {
|
||||||
|
const toHex = (n: number) =>
|
||||||
|
Math.max(0, Math.min(255, Math.round(n)))
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, '0')
|
||||||
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hsbToRgb({ h, s, b }: HSB): RGB {
|
||||||
|
// Normalize
|
||||||
|
const hh = ((h % 360) + 360) % 360
|
||||||
|
const ss = Math.max(0, Math.min(100, s)) / 100
|
||||||
|
const vv = Math.max(0, Math.min(100, b)) / 100
|
||||||
|
|
||||||
|
const c = vv * ss
|
||||||
|
const x = c * (1 - Math.abs(((hh / 60) % 2) - 1))
|
||||||
|
const m = vv - c
|
||||||
|
|
||||||
|
let rp = 0,
|
||||||
|
gp = 0,
|
||||||
|
bp = 0
|
||||||
|
|
||||||
|
if (hh < 60) {
|
||||||
|
rp = c
|
||||||
|
gp = x
|
||||||
|
bp = 0
|
||||||
|
} else if (hh < 120) {
|
||||||
|
rp = x
|
||||||
|
gp = c
|
||||||
|
bp = 0
|
||||||
|
} else if (hh < 180) {
|
||||||
|
rp = 0
|
||||||
|
gp = c
|
||||||
|
bp = x
|
||||||
|
} else if (hh < 240) {
|
||||||
|
rp = 0
|
||||||
|
gp = x
|
||||||
|
bp = c
|
||||||
|
} else if (hh < 300) {
|
||||||
|
rp = x
|
||||||
|
gp = 0
|
||||||
|
bp = c
|
||||||
|
} else {
|
||||||
|
rp = c
|
||||||
|
gp = 0
|
||||||
|
bp = x
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: Math.round((rp + m) * 255),
|
||||||
|
g: Math.round((gp + m) * 255),
|
||||||
|
b: Math.round((bp + m) * 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function parseToRgb(color: string): RGB {
|
export function parseToRgb(color: string): RGB {
|
||||||
const format = identifyColorFormat(color)
|
const format = identifyColorFormat(color)
|
||||||
if (!format) return { r: 0, g: 0, b: 0 }
|
if (!format) return { r: 0, g: 0, b: 0 }
|
||||||
|
|||||||
Reference in New Issue
Block a user