From 3f8c4025935cff1892d1fa678b36970bbea8ba3a Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Thu, 23 May 2024 02:05:40 +0200 Subject: [PATCH] Refactor color functionality (#2905) * Fix hex color parsing * Refactor color functionality * Revert workaround --- src/renderer/colors.scss | 5 +- src/renderer/components/inputs/ColorInput.tsx | 16 +- .../inputs/elements/ColorPicker.tsx | 14 +- .../inputs/elements/ColorSlider.tsx | 49 ++--- .../inputs/elements/RgbHexInput.tsx | 31 +-- src/renderer/components/node/Node.tsx | 3 +- src/renderer/helpers/color.ts | 176 ++++++++++++++++++ src/renderer/helpers/colorTools.ts | 60 +----- src/renderer/helpers/colorUtil.ts | 80 -------- src/renderer/hooks/useColorModels.ts | 57 +++--- 10 files changed, 266 insertions(+), 225 deletions(-) create mode 100644 src/renderer/helpers/color.ts delete mode 100644 src/renderer/helpers/colorUtil.ts diff --git a/src/renderer/colors.scss b/src/renderer/colors.scss index efd141004a..e507964f0c 100644 --- a/src/renderer/colors.scss +++ b/src/renderer/colors.scss @@ -1,4 +1,3 @@ -/* stylelint-disable color-hex-length */ @use 'sass:color'; @use 'sass:map'; @@ -32,7 +31,7 @@ --type-color-number: #3182ce; --type-color-string: #10b52c; --type-color-bool: #319795; - --type-color-color: #ffffff; + --type-color-color: #fff; --type-color-torch: #dd6b20; --type-color-onnx: #63b3ed; --type-color-ncnn: #ed64a6; @@ -165,7 +164,7 @@ --controls-bg: var(--theme-300-a75); --controls-bg-hover: var(--theme-400); - --type-color-color: #000000; + --type-color-color: #000; } // Default theme (copied from Chakra UI) diff --git a/src/renderer/components/inputs/ColorInput.tsx b/src/renderer/components/inputs/ColorInput.tsx index 5d1c4be63a..c035563c87 100644 --- a/src/renderer/components/inputs/ColorInput.tsx +++ b/src/renderer/components/inputs/ColorInput.tsx @@ -13,7 +13,6 @@ import { VStack, } from '@chakra-ui/react'; import { ReactNode, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { RgbColor } from 'react-colorful'; import { useContext } from 'use-context-selector'; import { toCssColor, toKind, toRgb } from '../../../common/color-json-util'; import { @@ -25,6 +24,7 @@ import { } from '../../../common/common-types'; import { log } from '../../../common/log'; import { InputContext } from '../../contexts/InputContext'; +import { Color } from '../../helpers/color'; import { useColorModels } from '../../hooks/useColorModels'; import { TypeTags } from '../TypeTag'; import { ColorBoxButton } from './elements/ColorBoxButton'; @@ -41,21 +41,17 @@ const ALL_KINDS: ReadonlySet = new Set(['grayscale', 'rgb' const KIND_SELECTOR_HEIGHT = '2rem'; const COMPARE_BUTTON_HEIGHT = '3rem'; -const toRgbColor = (color: ColorJson): RgbColor => { +const toRgbColor = (color: ColorJson): Color => { if (color.kind === 'grayscale') { const l = Math.round(color.values[0] * 255); - return { r: l, g: l, b: l }; + return new Color(l, l, l); } const [r, g, b] = color.values; - return { - r: Math.round(r * 255), - g: Math.round(g * 255), - b: Math.round(b * 255), - }; + return new Color(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)); }; interface RgbOrRgbaPickerProps extends Omit, 'onChange'> { - onChange: (color: RgbColor) => void; + onChange: (color: Color) => void; alpha?: ReactNode; } const RgbOrRgbaPicker = memo( @@ -168,7 +164,7 @@ const RgbaPicker = memo( const originalAlpha = color.values[3]; const onChangeRgb = useCallback( - ({ r, g, b }: RgbColor): void => { + ({ r, g, b }: Color): void => { if (originalAlpha !== undefined) { onChange({ kind: 'rgba', values: [r / 255, g / 255, b / 255, originalAlpha] }); } else { diff --git a/src/renderer/components/inputs/elements/ColorPicker.tsx b/src/renderer/components/inputs/elements/ColorPicker.tsx index 142616ee50..b60a3d8ca9 100644 --- a/src/renderer/components/inputs/elements/ColorPicker.tsx +++ b/src/renderer/components/inputs/elements/ColorPicker.tsx @@ -3,17 +3,15 @@ import { memo, useMemo, useRef } from 'react'; import { HsvColorPicker as BuggedHsvColorPicker, RgbColorPicker as BuggedRgbColorPicker, - HsvColor, - RgbColor, } from 'react-colorful'; -import { hsvColorId, rgbColorId } from '../../../helpers/colorUtil'; +import { Color, HsvColor } from '../../../helpers/color'; interface PickerProps { color: T; onChange: (value: T) => void; } -export const RgbColorPicker = memo(({ color, onChange }: PickerProps) => { +export const RgbColorPicker = memo(({ color, onChange }: PickerProps) => { // The react-colorful color picker has a pretty major bug: rounding. // It uses HSV internally to represent color, but it seems to round those values. // This results in the picker sometimes immediately changing the given input color. @@ -24,7 +22,7 @@ export const RgbColorPicker = memo(({ color, onChange }: PickerProps) const pickerLastSet = useRef(0); // eslint-disable-next-line react-hooks/exhaustive-deps - const lastColorChange = useMemo(Date.now, [rgbColorId(color)]); + const lastColorChange = useMemo(Date.now, [color.id]); return ( ) (sinceLastSet < 200 || sinceLastChange > 200) ) { pickerLastSet.current = Date.now(); - onChange(value); + onChange(Color.from(value)); } }} /> @@ -61,7 +59,7 @@ export const HsvColorPicker = memo(({ color, onChange }: PickerProps) const pickerLastSet = useRef(0); // eslint-disable-next-line react-hooks/exhaustive-deps - const lastColorChange = useMemo(Date.now, [hsvColorId(color)]); + const lastColorChange = useMemo(Date.now, [color.id]); return ( ) (sinceLastSet < 200 || sinceLastChange > 200) ) { pickerLastSet.current = Date.now(); - onChange(value); + onChange(HsvColor.from(value)); } }} /> diff --git a/src/renderer/components/inputs/elements/ColorSlider.tsx b/src/renderer/components/inputs/elements/ColorSlider.tsx index 8891a46d4a..c5704042bf 100644 --- a/src/renderer/components/inputs/elements/ColorSlider.tsx +++ b/src/renderer/components/inputs/elements/ColorSlider.tsx @@ -1,7 +1,6 @@ import { HStack, Text } from '@chakra-ui/react'; import { memo, useEffect, useState } from 'react'; -import { HsvColor, RgbColor } from 'react-colorful'; -import { hsvToRgb, rgbToHex } from '../../../helpers/colorUtil'; +import { Color, HsvColor } from '../../../helpers/color'; import { LINEAR_SCALE } from '../../../helpers/sliderScale'; import { AdvancedNumberInput } from './AdvanceNumberInput'; import { SliderStyle, StyledSlider } from './StyledSlider'; @@ -78,13 +77,13 @@ export const ColorSlider = memo( } ); -const getRgbStyle = (color0: RgbColor, color255: RgbColor): SliderStyle => { - return { type: 'gradient', gradient: [rgbToHex(color0), rgbToHex(color255)] }; +const getRgbStyle = (color0: Color, color255: Color): SliderStyle => { + return { type: 'gradient', gradient: [color0.hex(), color255.hex()] }; }; interface RgbSlidersProps { - rgb: RgbColor; - onChange: (value: RgbColor) => void; + rgb: Color; + onChange: (value: Color) => void; } export const RgbSliders = memo(({ rgb, onChange }: RgbSlidersProps) => { return ( @@ -94,32 +93,36 @@ export const RgbSliders = memo(({ rgb, onChange }: RgbSlidersProps) => { label="R" max={255} min={0} - style={getRgbStyle({ ...rgb, r: 0 }, { ...rgb, r: 255 })} + style={getRgbStyle(rgb.with({ r: 0 }), rgb.with({ r: 255 }))} value={rgb.r} - onChange={(r) => onChange({ ...rgb, r })} + onChange={(r) => onChange(rgb.with({ r }))} /> onChange({ ...rgb, g })} + onChange={(g) => onChange(rgb.with({ g }))} /> onChange({ ...rgb, b })} + onChange={(b) => onChange(rgb.with({ b }))} /> ); }); +const getSvStyle = (color0: HsvColor, color255: HsvColor): SliderStyle => { + return { type: 'gradient', gradient: [color0.rgb().hex(), color255.rgb().hex()] }; +}; + interface HsvSlidersProps { hsv: HsvColor; onChange: (value: HsvColor) => void; @@ -145,37 +148,25 @@ export const HsvSliders = memo(({ hsv, onChange }: HsvSlidersProps) => { ], }} value={hsv.h} - onChange={(h) => onChange({ ...hsv, h })} + onChange={(h) => onChange(hsv.with({ h }))} /> onChange({ ...hsv, s })} + onChange={(s) => onChange(hsv.with({ s }))} /> onChange({ ...hsv, v })} + onChange={(v) => onChange(hsv.with({ v }))} /> ); diff --git a/src/renderer/components/inputs/elements/RgbHexInput.tsx b/src/renderer/components/inputs/elements/RgbHexInput.tsx index d7767c7341..5ea6814e17 100644 --- a/src/renderer/components/inputs/elements/RgbHexInput.tsx +++ b/src/renderer/components/inputs/elements/RgbHexInput.tsx @@ -1,21 +1,24 @@ import { HStack, Input, Spacer, Text } from '@chakra-ui/react'; import { memo, useEffect, useState } from 'react'; -import { RgbColor } from 'react-colorful'; -import { parseRgbHex, rgbToHex } from '../../../helpers/colorUtil'; +import { Color } from '../../../helpers/color'; interface RgbHexInputProps { - rgb: RgbColor; - onChange: (value: RgbColor) => void; + rgb: Color; + onChange: (value: Color) => void; } export const RgbHexInput = memo(({ rgb, onChange }: RgbHexInputProps) => { - const currentHex = rgbToHex(rgb); + const currentHex = rgb.hex(); - const [inputString, setInputString] = useState(currentHex); + const [inputString, setInputString] = useState(currentHex); useEffect(() => { setInputString((old) => { - const parsed = parseRgbHex(old); - if (parsed && rgbToHex(parsed) === currentHex) { - return old; + try { + const parsed = Color.fromHex(old); + if (parsed.hex() === currentHex) { + return old; + } + } catch { + // ignore error } return currentHex; }); @@ -24,9 +27,13 @@ export const RgbHexInput = memo(({ rgb, onChange }: RgbHexInputProps) => { const changeInputString = (s: string) => { setInputString(s); - const parsed = parseRgbHex(s); - if (parsed && rgbToHex(parsed) !== currentHex) { - onChange(parsed); + try { + const parsed = Color.fromHex(s); + if (parsed.hex() !== currentHex) { + onChange(parsed); + } + } catch { + // ignore error } }; diff --git a/src/renderer/components/node/Node.tsx b/src/renderer/components/node/Node.tsx index c1bba0e559..1d7c3859ec 100644 --- a/src/renderer/components/node/Node.tsx +++ b/src/renderer/components/node/Node.tsx @@ -30,7 +30,6 @@ import { } from '../../contexts/ExecutionContext'; import { GlobalContext, GlobalVolatileContext } from '../../contexts/GlobalNodeState'; import { getCategoryAccentColor, getTypeAccentColors } from '../../helpers/accentColors'; -import { shadeColor } from '../../helpers/colorTools'; import { getSingleFileWithExtension } from '../../helpers/dataTransfer'; import { NodeState, useNodeStateFromData } from '../../helpers/nodeState'; import { NO_DISABLED, UseDisabled, useDisabled } from '../../hooks/useDisabled'; @@ -105,7 +104,7 @@ export const NodeView = memo( const finalBorderColor = useMemo(() => { if (borderColor) return borderColor; const regularBorderColor = 'var(--node-border-color)'; - return selected ? shadeColor(accentColor, 0) : regularBorderColor; + return selected ? accentColor : regularBorderColor; }, [selected, accentColor, borderColor]); const isEnabled = disable.status === DisabledStatus.Enabled; diff --git a/src/renderer/helpers/color.ts b/src/renderer/helpers/color.ts new file mode 100644 index 0000000000..eb422269a7 --- /dev/null +++ b/src/renderer/helpers/color.ts @@ -0,0 +1,176 @@ +export interface RgbColorLike { + r: number; + g: number; + b: number; +} + +/** + * An 8-bit RGB color. + */ +export class Color { + r: number; + + g: number; + + b: number; + + get id() { + return this.r * 256 * 256 + this.g * 256 + this.b; + } + + hex(): `#${string}` { + const hex = (Math.round(this.r) * 256 * 256 + Math.round(this.g) * 256 + Math.round(this.b)) + .toString(16) + .padStart(6, '0'); + return `#${hex}`; + } + + constructor(r: number, g: number, b: number) { + this.r = Math.round(r); + this.g = Math.round(g); + this.b = Math.round(b); + } + + static from({ r, g, b }: Readonly): Color { + return new Color(r, g, b); + } + + static fromHex(hex: string): Color { + let h = hex; + if (h.startsWith('#')) { + h = h.slice(1); + } + if (/^[0-9a-f]+$/i.test(h)) { + if (h.length === 3) { + const r = Number.parseInt(h[0], 16) * 17; + const g = Number.parseInt(h[1], 16) * 17; + const b = Number.parseInt(h[2], 16) * 17; + return new Color(r, g, b); + } + if (h.length === 6) { + const r = Number.parseInt(h.slice(0, 2), 16); + const g = Number.parseInt(h.slice(2, 4), 16); + const b = Number.parseInt(h.slice(4, 6), 16); + return new Color(r, g, b); + } + } + throw new Error(`Invalid hex color: ${hex}`); + } + + equals(other: Readonly): boolean { + return this.r === other.r && this.g === other.g && this.b === other.b; + } + + with({ r, g, b }: Partial>): Color { + return new Color(r ?? this.r, g ?? this.g, b ?? this.b); + } + + hsv(): HsvColor { + const max = Math.max(this.r, this.g, this.b); + const min = Math.min(this.r, this.g, this.b); + const c = max - min; + + let h; + if (c === 0) { + h = 0; + } else if (max === this.r) { + h = (this.g - this.b) / c + 6; + } else if (max === this.g) { + h = (this.b - this.r) / c + 2; + } else { + h = (this.r - this.g) / c + 4; + } + + const v = max; + const s = v === 0 ? 0 : c / v; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new HsvColor( + Math.round(h * 60) % 360, + Math.round(s * 100), + Math.round((v / 255) * 100) + ); + } + + /** + * Returns a new color with the given operation applied to each channel. + */ + channelWise(operation: (channel: number) => number): Color { + return new Color(operation(this.r), operation(this.g), operation(this.b)); + } + + /** + * Returns a new color that is the linear interpolation between this color and the other color. + */ + lerp(other: Readonly, factor: number): Color { + return new Color( + Math.round(this.r + (other.r - this.r) * factor), + Math.round(this.g + (other.g - this.g) * factor), + Math.round(this.b + (other.b - this.b) * factor) + ); + } + + /** + * Returns a new color that is the linear interpolation between this color and the other color in linear RGB. + */ + lerpLinear(other: Readonly, factor: number): Color { + const gamma = 2.2; + const lerp = (a: number, b: number) => { + return Math.round((a ** gamma + factor * (b ** gamma - a ** gamma)) ** (1 / gamma)); + }; + return new Color(lerp(this.r, other.r), lerp(this.g, other.g), lerp(this.b, other.b)); + } +} + +export interface HsvColorLike { + h: number; + s: number; + v: number; +} + +/** + * An HSV color. + */ +export class HsvColor { + /** Range: 0-360 */ + h: number; + + /** Range: 0-100 */ + s: number; + + /** Range: 0-100 */ + v: number; + + get id() { + return this.h * 100 * 100 + this.s * 100 + this.v; + } + + constructor(h: number, s: number, v: number) { + this.h = h; + this.s = s; + this.v = v; + } + + static from({ h, s, v }: Readonly): HsvColor { + return new HsvColor(h, s, v); + } + + equals(other: Readonly): boolean { + return this.h === other.h && this.s === other.s && this.v === other.v; + } + + with({ h, s, v }: Partial>): HsvColor { + return new HsvColor(h ?? this.h, s ?? this.s, v ?? this.v); + } + + rgb(): Color { + const v = this.v / 100; + const s = this.s / 100; + + const f = (n: number) => { + const k = (n + this.h / 60) % 6; + return v - v * s * Math.max(0, Math.min(k, 4 - k, 1)); + }; + + return new Color(Math.round(f(5) * 255), Math.round(f(3) * 255), Math.round(f(1) * 255)); + } +} diff --git a/src/renderer/helpers/colorTools.ts b/src/renderer/helpers/colorTools.ts index 1990b26ed6..fddf3f15a6 100644 --- a/src/renderer/helpers/colorTools.ts +++ b/src/renderer/helpers/colorTools.ts @@ -1,62 +1,16 @@ -// From https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors +import { Color } from './color'; /** * Lightens (percentage > 0) or darkens (percentage < 0) the given hex color. - * - * The color has to be in the form `#RRGGBB`. */ -export const shadeColor = (color: string, percent: number): `#${string}` => { - let R = parseInt(color.substring(1, 3), 16); - let G = parseInt(color.substring(3, 5), 16); - let B = parseInt(color.substring(5, 7), 16); - - R = Math.min(Math.round(R * (1 + percent / 100)), 255); - G = Math.min(Math.round(G * (1 + percent / 100)), 255); - B = Math.min(Math.round(B * (1 + percent / 100)), 255); - - const RR = R.toString(16).padStart(2, '0'); - const GG = G.toString(16).padStart(2, '0'); - const BB = B.toString(16).padStart(2, '0'); - - return `#${RR}${GG}${BB}`; -}; - -// Modified from https://codepen.io/njmcode/pen/NWdYBy - -// Converts a #ffffff hex string into an [r,g,b] array -const hexToRgb = (hex: string): [number, number, number] => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - if (!result) { - throw new Error(`Invalid hex color: ${hex}`); - } - return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]; -}; - -// Inverse of the above -const rgbToHex = (rgb: [number, number, number]): string => - // eslint-disable-next-line no-bitwise - `#${((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1)}`; - -// Interpolates two [r,g,b] colors and returns an [r,g,b] of the result -// Taken from the awesome ROT.js roguelike dev library at -// https://github.com/ondras/rot.js -const interpolateColorImpl = ( - color1: [number, number, number], - color2: [number, number, number], - factor = 0.5 -): [number, number, number] => { - const result = color1.slice() as [number, number, number]; - for (let i = 0; i < 3; i += 1) { - const c1 = color1[i] ** 2.2; - const c2 = color2[i] ** 2.2; - const blend = c1 + factor * (c2 - c1); - result[i] = Math.round(blend ** (1 / 2.2)); - } - return result; +export const shadeColor = (color: string, percent: number) => { + return Color.fromHex(color) + .channelWise((c) => Math.min(Math.round(c * (1 + percent / 100)), 255)) + .hex(); }; -export const interpolateColor = (color1: string, color2: string, factor = 0.5): string => - rgbToHex(interpolateColorImpl(hexToRgb(color1), hexToRgb(color2), factor)); +export const interpolateColor = (color1: string, color2: string, factor = 0.5) => + Color.fromHex(color1).lerpLinear(Color.fromHex(color2), factor).hex(); export const createConicGradient = (colors: readonly string[]): string => { if (colors.length === 1) return colors[0]; diff --git a/src/renderer/helpers/colorUtil.ts b/src/renderer/helpers/colorUtil.ts deleted file mode 100644 index 7019f7abca..0000000000 --- a/src/renderer/helpers/colorUtil.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { HsvColor, RgbColor } from 'react-colorful'; - -export const rgbColorId = ({ r, g, b }: RgbColor): number => { - return r * 256 * 256 + g * 256 + b; -}; -export const hsvColorId = ({ h, s, v }: HsvColor): string => { - return `${h} ${s} ${v}`; -}; - -export const rgbEqual = (a: RgbColor, b: RgbColor): boolean => { - return a.r === b.r && a.g === b.g && a.b === b.b; -}; -export const hsvEqual = (a: HsvColor, b: HsvColor): boolean => { - return a.h === b.h && a.s === b.s && a.v === b.v; -}; - -export const rgbToHsv = ({ r, g, b }: RgbColor): HsvColor => { - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const c = max - min; - - let h; - if (c === 0) { - h = 0; - } else if (max === r) { - h = (g - b) / c + 6; - } else if (max === g) { - h = (b - r) / c + 2; - } else { - h = (r - g) / c + 4; - } - - const v = max; - const s = v === 0 ? 0 : c / v; - return { - h: Math.round(h * 60) % 360, - s: Math.round(s * 100), - v: Math.round((v / 255) * 100), - }; -}; -export const hsvToRgb = ({ h, s, v }: HsvColor): RgbColor => { - v /= 100; - s /= 100; - - const f = (n: number) => { - const k = (n + h / 60) % 6; - return v - v * s * Math.max(0, Math.min(k, 4 - k, 1)); - }; - - return { - r: Math.round(f(5) * 255), - g: Math.round(f(3) * 255), - b: Math.round(f(1) * 255), - }; -}; - -export const parseRgbHex = (hex: string): RgbColor | undefined => { - if (hex.startsWith('#')) { - hex = hex.slice(1); - } - if (!/^[0-9a-f]+$/i.test(hex)) return undefined; - if (hex.length === 3) { - const r = Number.parseInt(hex[0], 16) * 17; - const g = Number.parseInt(hex[1], 16) * 17; - const b = Number.parseInt(hex[2], 16) * 17; - return { r, g, b }; - } - if (hex.length === 6) { - const r = Number.parseInt(hex.slice(0, 2), 16); - const g = Number.parseInt(hex.slice(2, 4), 16); - const b = Number.parseInt(hex.slice(4, 6), 16); - return { r, g, b }; - } - return undefined; -}; -export const rgbToHex = ({ r, g, b }: RgbColor): string => { - const hex = (r * 256 * 256 + g * 256 + b).toString(16).padStart(6, '0').toUpperCase(); - return `#${hex}`; -}; diff --git a/src/renderer/hooks/useColorModels.ts b/src/renderer/hooks/useColorModels.ts index 0dab2e0182..9d98647613 100644 --- a/src/renderer/hooks/useColorModels.ts +++ b/src/renderer/hooks/useColorModels.ts @@ -1,27 +1,26 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { HsvColor, RgbColor } from 'react-colorful'; -import { hsvEqual, hsvToRgb, rgbEqual, rgbToHsv } from '../helpers/colorUtil'; +import { Color, HsvColor, HsvColorLike, RgbColorLike } from '../helpers/color'; -const updateRgbX = (o: RgbColor, n: RgbColor): RgbColor => { - return rgbEqual(o, n) ? o : n; +const updateRgbX = (o: Color, n: Color): Color => { + return o.equals(n) ? o : n; }; const updateHsvX = (o: HsvColor, n: HsvColor): HsvColor => { - return hsvEqual(o, n) ? o : n; + return o.equals(n) ? o : n; }; -const updateHsvFromRgbX = (oldHsv: HsvColor, newRgb: RgbColor): HsvColor => { - const oldRgb = hsvToRgb(oldHsv); - if (!rgbEqual(newRgb, oldRgb)) { - const newHsv = rgbToHsv(newRgb); +const updateHsvFromRgbX = (oldHsv: HsvColor, newRgb: Color): HsvColor => { + const oldRgb = oldHsv.rgb(); + if (!newRgb.equals(oldRgb)) { + const newHsv = newRgb.hsv(); // check to see whether we actually need to change H and S - if (rgbEqual(newRgb, hsvToRgb({ ...newHsv, h: oldHsv.h }))) { + if (newRgb.equals(newHsv.with({ h: oldHsv.h }).rgb())) { newHsv.h = oldHsv.h; } - if (rgbEqual(newRgb, hsvToRgb({ ...newHsv, s: oldHsv.s }))) { + if (newRgb.equals(newHsv.with({ s: oldHsv.s }).rgb())) { newHsv.s = oldHsv.s; } - if (!hsvEqual(newHsv, oldHsv)) { + if (!newHsv.equals(oldHsv)) { return newHsv; } } @@ -29,33 +28,33 @@ const updateHsvFromRgbX = (oldHsv: HsvColor, newRgb: RgbColor): HsvColor => { }; interface UseColorMode { - rgb: RgbColor; + rgb: Color; hsv: HsvColor; - changeRgb: (value: RgbColor) => void; - changeHsv: (value: HsvColor) => void; + changeRgb: (value: RgbColorLike) => void; + changeHsv: (value: HsvColorLike) => void; } export const useColorModels = ( color: T, - toRgbColor: (color: T) => RgbColor, - onChange: (value: RgbColor) => void + toRgbColor: (color: T) => Color, + onChange: (value: Color) => void ): UseColorMode => { - const [state, setState] = useState(() => { + const [state, setState] = useState(() => { const rgb = toRgbColor(color); - return [rgb, rgbToHsv(rgb)]; + return [rgb, rgb.hsv()]; }); const lastChangeRef = useRef(0); - const updateFromRgb = useCallback((newRgb: RgbColor): void => { + const updateFromRgb = useCallback((newRgb: Color): void => { setState((old) => { const [oldRgb, oldHsv] = old; - if (!rgbEqual(oldRgb, newRgb)) { + if (!oldRgb.equals(newRgb)) { return [newRgb, updateHsvFromRgbX(oldHsv, newRgb)]; } return old; }); }, []); - const updateFromHsv = useCallback((newHsv: HsvColor, newRgb: RgbColor): void => { + const updateFromHsv = useCallback((newHsv: HsvColor, newRgb: Color): void => { setState((old) => { const [oldRgb, oldHsv] = old; @@ -79,18 +78,20 @@ export const useColorModels = ( }, [color, updateFromRgb, toRgbColor]); const changeRgb = useCallback( - (newRgb: RgbColor): void => { + (newRgb: RgbColorLike): void => { lastChangeRef.current = Date.now(); - updateFromRgb(newRgb); - onChange(newRgb); + const rgb = Color.from(newRgb); + updateFromRgb(rgb); + onChange(rgb); }, [onChange, updateFromRgb] ); const changeHsv = useCallback( - (newHsv: HsvColor): void => { + (newHsv: HsvColorLike): void => { lastChangeRef.current = Date.now(); - const newRgb = hsvToRgb(newHsv); - updateFromHsv(newHsv, newRgb); + const hsv = HsvColor.from(newHsv); + const newRgb = hsv.rgb(); + updateFromHsv(hsv, newRgb); onChange(newRgb); }, [onChange, updateFromHsv]